1
This commit is contained in:
582
src/components/creative-agent/creativeAgentViewModel.ts
Normal file
582
src/components/creative-agent/creativeAgentViewModel.ts
Normal file
@@ -0,0 +1,582 @@
|
||||
import type {
|
||||
CreativeAgentSessionSnapshot,
|
||||
CreativeAgentSseEvent,
|
||||
CreativeAgentStage,
|
||||
CreativeTargetSessionBinding,
|
||||
} from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
import type {
|
||||
PuzzleCreativeTemplateProtocol,
|
||||
PuzzleCreativeTemplateSelection,
|
||||
PuzzleLevelGenerationMode,
|
||||
PuzzleSupportedLevelMode,
|
||||
} from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
|
||||
|
||||
export const CREATIVE_AGENT_STAGE_LABEL: Record<CreativeAgentStage, string> = {
|
||||
idle: '等待输入',
|
||||
perceiving: '正在理解素材',
|
||||
thinking: '正在构思',
|
||||
remembering: '正在整理上下文',
|
||||
selecting_puzzle_template: '正在选择拼图模板',
|
||||
waiting_template_confirmation: '等待确认',
|
||||
planning_puzzle_levels: '正在规划关卡',
|
||||
acting: '正在生成草稿',
|
||||
reflecting: '正在检查结果',
|
||||
collaborating: '正在协作收口',
|
||||
target_ready: '草稿已就绪',
|
||||
waiting_user: '等待补充',
|
||||
failed: '生成失败',
|
||||
};
|
||||
|
||||
const CREATIVE_AGENT_STAGE_DONE_LABEL: Partial<
|
||||
Record<CreativeAgentStage, string>
|
||||
> = {
|
||||
perceiving: '素材已理解',
|
||||
thinking: '构思已完成',
|
||||
remembering: '上下文已整理',
|
||||
selecting_puzzle_template: '模板已选择',
|
||||
planning_puzzle_levels: '关卡已规划',
|
||||
acting: '草稿已生成',
|
||||
reflecting: '结果已检查',
|
||||
collaborating: '协作已收口',
|
||||
target_ready: '草稿已就绪',
|
||||
};
|
||||
|
||||
const CREATIVE_AGENT_STAGE_WAITING_LABEL: Partial<
|
||||
Record<CreativeAgentStage, string>
|
||||
> = {
|
||||
idle: '等待输入',
|
||||
waiting_template_confirmation: '等待确认',
|
||||
waiting_user: '等待补充',
|
||||
failed: '生成失败',
|
||||
};
|
||||
|
||||
const CREATIVE_AGENT_STAGE_IDLE_LABEL: Record<CreativeAgentStage, string> = {
|
||||
idle: '等待输入',
|
||||
perceiving: '理解素材',
|
||||
thinking: '构思方向',
|
||||
remembering: '整理上下文',
|
||||
selecting_puzzle_template: '选择拼图模板',
|
||||
waiting_template_confirmation: '等待确认',
|
||||
planning_puzzle_levels: '规划关卡',
|
||||
acting: '生成草稿',
|
||||
reflecting: '检查结果',
|
||||
collaborating: '协作收口',
|
||||
target_ready: '草稿就绪',
|
||||
waiting_user: '等待补充',
|
||||
failed: '生成失败',
|
||||
};
|
||||
|
||||
export const CREATIVE_AGENT_TIMELINE: CreativeAgentStage[] = [
|
||||
'perceiving',
|
||||
'thinking',
|
||||
'remembering',
|
||||
'selecting_puzzle_template',
|
||||
'planning_puzzle_levels',
|
||||
'acting',
|
||||
'reflecting',
|
||||
'collaborating',
|
||||
'target_ready',
|
||||
];
|
||||
|
||||
export type CreativeAgentTargetSelectionStage =
|
||||
| 'puzzle-agent-workspace'
|
||||
| 'puzzle-result';
|
||||
|
||||
export function resolveCreativeAgentTargetSelectionStage(
|
||||
targetStage: CreativeTargetSessionBinding['targetStage'],
|
||||
): CreativeAgentTargetSelectionStage {
|
||||
return targetStage === 'puzzle-agent-workspace'
|
||||
? 'puzzle-agent-workspace'
|
||||
: 'puzzle-result';
|
||||
}
|
||||
|
||||
export function createCreativeAgentClientMessageId(prefix = 'creative-agent') {
|
||||
return `${prefix}-${Date.now().toString(36)}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function resolveTemplateLevelMode(
|
||||
supportedLevelMode: PuzzleSupportedLevelMode,
|
||||
defaultLevelCount: number,
|
||||
): PuzzleLevelGenerationMode {
|
||||
if (supportedLevelMode === 'single') {
|
||||
return 'single_level';
|
||||
}
|
||||
|
||||
if (supportedLevelMode === 'multi') {
|
||||
return 'multi_level';
|
||||
}
|
||||
|
||||
return defaultLevelCount > 1 ? 'multi_level' : 'single_level';
|
||||
}
|
||||
|
||||
function resolveTemplatePlannedLevelCount(
|
||||
supportedLevelMode: PuzzleSupportedLevelMode,
|
||||
defaultLevelCount: number,
|
||||
) {
|
||||
if (supportedLevelMode === 'single') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (supportedLevelMode === 'multi') {
|
||||
return Math.max(2, defaultLevelCount);
|
||||
}
|
||||
|
||||
return Math.max(1, defaultLevelCount);
|
||||
}
|
||||
|
||||
export function buildPuzzleTemplateSelectionFromProtocol(
|
||||
template: PuzzleCreativeTemplateProtocol,
|
||||
): PuzzleCreativeTemplateSelection {
|
||||
const plannedLevelCount = resolveTemplatePlannedLevelCount(
|
||||
template.supportedLevelMode,
|
||||
template.defaultLevelCount,
|
||||
);
|
||||
|
||||
return {
|
||||
templateId: template.templateId,
|
||||
title: template.title,
|
||||
reason: template.summary,
|
||||
costRange: template.costRange,
|
||||
supportedLevelMode: template.supportedLevelMode,
|
||||
selectedLevelMode: resolveTemplateLevelMode(
|
||||
template.supportedLevelMode,
|
||||
plannedLevelCount,
|
||||
),
|
||||
plannedLevelCount,
|
||||
requiresUserConfirmation: true,
|
||||
};
|
||||
}
|
||||
|
||||
export type CreativeAgentStageDisplayStatus =
|
||||
| 'active'
|
||||
| 'done'
|
||||
| 'waiting'
|
||||
| 'idle';
|
||||
|
||||
export function getCreativeAgentStageDisplayLabel(
|
||||
stage: CreativeAgentStage,
|
||||
status: CreativeAgentStageDisplayStatus,
|
||||
) {
|
||||
if (status === 'done') {
|
||||
return CREATIVE_AGENT_STAGE_DONE_LABEL[stage] ?? CREATIVE_AGENT_STAGE_LABEL[stage];
|
||||
}
|
||||
if (status === 'waiting') {
|
||||
return CREATIVE_AGENT_STAGE_WAITING_LABEL[stage] ?? CREATIVE_AGENT_STAGE_LABEL[stage];
|
||||
}
|
||||
if (status === 'idle') {
|
||||
return CREATIVE_AGENT_STAGE_IDLE_LABEL[stage];
|
||||
}
|
||||
return CREATIVE_AGENT_STAGE_LABEL[stage];
|
||||
}
|
||||
|
||||
export type CreativeAgentProcessTone =
|
||||
| 'active'
|
||||
| 'done'
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'danger';
|
||||
|
||||
export type CreativeAgentProcessItem = {
|
||||
id: string;
|
||||
meta: string;
|
||||
title: string;
|
||||
detail: string | null;
|
||||
detailLines: string[];
|
||||
tone: CreativeAgentProcessTone;
|
||||
};
|
||||
|
||||
const CREATIVE_AGENT_TOOL_LABEL: Record<string, string> = {
|
||||
retrieve_puzzle_template_catalog: '读取拼图模板',
|
||||
select_puzzle_template: '选择拼图模板',
|
||||
confirm_puzzle_template: '确认模板',
|
||||
create_puzzle_agent_session: '创建拼图草稿',
|
||||
compile_puzzle_creative_draft: '编译拼图草稿',
|
||||
plan_puzzle_level_images: '规划关卡图片',
|
||||
generate_puzzle_level_images: '生成关卡图片',
|
||||
apply_puzzle_draft_natural_language_edit: '写回草稿修改',
|
||||
validate_puzzle_result_preview: '校验草稿预览',
|
||||
start_puzzle_draft_test_run: '启动拼图试玩',
|
||||
};
|
||||
|
||||
type ProcessBuildContext = {
|
||||
activeStageEventIndex: number;
|
||||
eventLog: CreativeAgentSseEvent[];
|
||||
isComplete: boolean;
|
||||
};
|
||||
|
||||
function formatPuzzleLevelMode(mode: PuzzleLevelGenerationMode) {
|
||||
return mode === 'single_level' ? '单关卡' : '多关卡';
|
||||
}
|
||||
|
||||
function formatTargetStage(stage: CreativeTargetSessionBinding['targetStage']) {
|
||||
return stage === 'puzzle-agent-workspace'
|
||||
? '拼图工作区'
|
||||
: stage === 'puzzle-runtime'
|
||||
? '拼图运行态'
|
||||
: '拼图结果页';
|
||||
}
|
||||
|
||||
function resolveToolLabel(toolName: string) {
|
||||
return CREATIVE_AGENT_TOOL_LABEL[toolName] ?? toolName;
|
||||
}
|
||||
|
||||
function buildStageProcessItem(
|
||||
event: Extract<CreativeAgentSseEvent, { event: 'stage' }>,
|
||||
index: number,
|
||||
context: ProcessBuildContext,
|
||||
): CreativeAgentProcessItem {
|
||||
const stage = event.data.stage;
|
||||
const isWaiting =
|
||||
stage === 'waiting_template_confirmation' || stage === 'waiting_user';
|
||||
const isActive =
|
||||
index === context.activeStageEventIndex &&
|
||||
!context.isComplete &&
|
||||
!isWaiting &&
|
||||
stage !== 'target_ready' &&
|
||||
stage !== 'failed';
|
||||
const tone: CreativeAgentProcessTone =
|
||||
stage === 'failed'
|
||||
? 'danger'
|
||||
: isWaiting
|
||||
? 'warning'
|
||||
: isActive
|
||||
? 'active'
|
||||
: 'done';
|
||||
return {
|
||||
id: `${index}-stage-${stage}`,
|
||||
meta: '阶段',
|
||||
title: getCreativeAgentStageDisplayLabel(
|
||||
stage,
|
||||
isActive ? 'active' : isWaiting ? 'waiting' : 'done',
|
||||
),
|
||||
detail: '阶段切换',
|
||||
detailLines: [],
|
||||
tone,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEventProcessItem(
|
||||
event: CreativeAgentSseEvent,
|
||||
index: number,
|
||||
context: ProcessBuildContext,
|
||||
): CreativeAgentProcessItem | null {
|
||||
switch (event.event) {
|
||||
case 'stage':
|
||||
return buildStageProcessItem(event, index, context);
|
||||
case 'tool_started': {
|
||||
const toolLabel = resolveToolLabel(event.data.toolName);
|
||||
const isToolResolved =
|
||||
context.isComplete ||
|
||||
context.eventLog.slice(index + 1).some(
|
||||
(nextEvent) =>
|
||||
nextEvent.event === 'tool_completed' &&
|
||||
(nextEvent.data.toolCallId === event.data.toolCallId ||
|
||||
nextEvent.data.toolName === event.data.toolName),
|
||||
);
|
||||
return {
|
||||
id: `${index}-tool-started-${event.data.toolCallId}`,
|
||||
meta: '工具调用',
|
||||
title: `开始:${toolLabel}`,
|
||||
detail: event.data.summary ?? event.data.toolName,
|
||||
detailLines: [],
|
||||
tone: isToolResolved ? 'done' : 'active',
|
||||
};
|
||||
}
|
||||
case 'tool_completed': {
|
||||
const toolLabel = resolveToolLabel(event.data.toolName);
|
||||
return {
|
||||
id: `${index}-tool-completed-${event.data.toolCallId}`,
|
||||
meta: '工具完成',
|
||||
title: `完成:${toolLabel}`,
|
||||
detail: event.data.summary ?? event.data.toolName,
|
||||
detailLines: [],
|
||||
tone: 'done',
|
||||
};
|
||||
}
|
||||
case 'puzzle_template_selection':
|
||||
return {
|
||||
id: `${index}-template-${event.data.selection.templateId}`,
|
||||
meta: '模板',
|
||||
title: `选择 ${event.data.selection.title}`,
|
||||
detail: event.data.selection.reason,
|
||||
detailLines: [
|
||||
`${formatPuzzleLevelMode(event.data.selection.selectedLevelMode)} · ${event.data.selection.plannedLevelCount} 关`,
|
||||
],
|
||||
tone: 'info',
|
||||
};
|
||||
case 'puzzle_template_catalog':
|
||||
return {
|
||||
id: `${index}-template-catalog`,
|
||||
meta: '模板',
|
||||
title: `读取 ${event.data.templates.length} 个模板`,
|
||||
detail:
|
||||
event.data.templates
|
||||
.slice(0, 3)
|
||||
.map((template) => template.title)
|
||||
.join('、') || null,
|
||||
detailLines: [],
|
||||
tone: 'info',
|
||||
};
|
||||
case 'puzzle_cost_range':
|
||||
return {
|
||||
id: `${index}-cost-${event.data.costRange.minPoints}-${event.data.costRange.maxPoints}`,
|
||||
meta: '消耗',
|
||||
title: `预计 ${event.data.costRange.minPoints}-${event.data.costRange.maxPoints} 光点`,
|
||||
detail: event.data.costRange.reason,
|
||||
detailLines: [],
|
||||
tone: 'info',
|
||||
};
|
||||
case 'puzzle_level_plan': {
|
||||
const candidateCount = event.data.plan.levels.reduce(
|
||||
(total, level) => total + level.candidateCount,
|
||||
0,
|
||||
);
|
||||
return {
|
||||
id: `${index}-level-plan-${event.data.plan.templateId}`,
|
||||
meta: '关卡',
|
||||
title: `规划 ${event.data.plan.levels.length} 个关卡`,
|
||||
detail: `${formatPuzzleLevelMode(event.data.plan.mode)} · ${candidateCount} 张候选图`,
|
||||
detailLines: event.data.plan.levels.slice(0, 4).map((level) =>
|
||||
[
|
||||
level.levelName,
|
||||
level.pictureDescription,
|
||||
level.pictureReference ? `参考图:${level.pictureReference}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
),
|
||||
tone: 'info',
|
||||
};
|
||||
}
|
||||
case 'reflection':
|
||||
return {
|
||||
id: `${index}-reflection`,
|
||||
meta: '检查',
|
||||
title: event.data.pass ? '检查通过' : '需要调整',
|
||||
detail: event.data.summary,
|
||||
detailLines: event.data.warnings,
|
||||
tone: event.data.pass ? 'done' : 'warning',
|
||||
};
|
||||
case 'target_session':
|
||||
return {
|
||||
id: `${index}-target-${event.data.binding.targetSessionId}`,
|
||||
meta: '目标',
|
||||
title: '拼图草稿已绑定',
|
||||
detail: formatTargetStage(event.data.binding.targetStage),
|
||||
detailLines: [`目标会话:${event.data.binding.targetSessionId}`],
|
||||
tone: 'done',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
id: `${index}-error-${event.data.code}`,
|
||||
meta: '异常',
|
||||
title: event.data.message,
|
||||
detail: event.data.code,
|
||||
detailLines: [],
|
||||
tone: 'danger',
|
||||
};
|
||||
case 'done':
|
||||
return {
|
||||
id: `${index}-done`,
|
||||
meta: '完成',
|
||||
title: '本轮完成',
|
||||
detail: null,
|
||||
detailLines: [],
|
||||
tone: 'done',
|
||||
};
|
||||
case 'agent_message_delta':
|
||||
case 'thought_summary_delta':
|
||||
case 'session':
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildThoughtSummaryItems(
|
||||
eventLog: CreativeAgentSseEvent[],
|
||||
isComplete: boolean,
|
||||
): CreativeAgentProcessItem[] {
|
||||
const thoughtMap = new Map<string, string>();
|
||||
let latestThoughtId: string | null = null;
|
||||
|
||||
for (const event of eventLog) {
|
||||
if (event.event !== 'thought_summary_delta') {
|
||||
continue;
|
||||
}
|
||||
const currentText = thoughtMap.get(event.data.thoughtId) ?? '';
|
||||
thoughtMap.set(event.data.thoughtId, `${currentText}${event.data.textDelta}`);
|
||||
latestThoughtId = event.data.thoughtId;
|
||||
}
|
||||
|
||||
const items: CreativeAgentProcessItem[] = [];
|
||||
for (const [thoughtId, text] of thoughtMap.entries()) {
|
||||
const detail = text.trim();
|
||||
if (!detail) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: `thought-${thoughtId}`,
|
||||
meta: '思考',
|
||||
title: '思考摘要',
|
||||
detail,
|
||||
detailLines: [],
|
||||
tone: !isComplete && thoughtId === latestThoughtId ? 'active' : 'done',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function buildSessionFallbackItems(
|
||||
session: CreativeAgentSessionSnapshot | null,
|
||||
eventLog: CreativeAgentSseEvent[],
|
||||
): CreativeAgentProcessItem[] {
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: CreativeAgentProcessItem[] = [];
|
||||
if (
|
||||
session.puzzleTemplateCatalog.length > 0 &&
|
||||
!eventLog.some((event) => event.event === 'puzzle_template_catalog')
|
||||
) {
|
||||
items.push({
|
||||
id: `session-template-catalog-${session.puzzleTemplateCatalog.length}`,
|
||||
meta: '模板',
|
||||
title: `读取 ${session.puzzleTemplateCatalog.length} 个模板`,
|
||||
detail: session.puzzleTemplateCatalog
|
||||
.slice(0, 3)
|
||||
.map((template) => template.title)
|
||||
.join('、'),
|
||||
detailLines: [],
|
||||
tone: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
session.puzzleTemplateSelection &&
|
||||
!eventLog.some((event) => event.event === 'puzzle_template_selection')
|
||||
) {
|
||||
items.push({
|
||||
id: `session-template-${session.puzzleTemplateSelection.templateId}`,
|
||||
meta: '模板',
|
||||
title: `选择 ${session.puzzleTemplateSelection.title}`,
|
||||
detail: session.puzzleTemplateSelection.reason,
|
||||
detailLines: [
|
||||
`${formatPuzzleLevelMode(session.puzzleTemplateSelection.selectedLevelMode)} · ${session.puzzleTemplateSelection.plannedLevelCount} 关`,
|
||||
],
|
||||
tone: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
session.puzzleImageGenerationPlan &&
|
||||
!eventLog.some((event) => event.event === 'puzzle_level_plan')
|
||||
) {
|
||||
const plan = session.puzzleImageGenerationPlan;
|
||||
items.push({
|
||||
id: `session-level-plan-${plan.templateId}`,
|
||||
meta: '关卡',
|
||||
title: `规划 ${plan.levels.length} 个关卡`,
|
||||
detail: `${formatPuzzleLevelMode(plan.mode)} · ${plan.estimatedCostRange.minPoints}-${plan.estimatedCostRange.maxPoints} 光点`,
|
||||
detailLines: plan.levels.slice(0, 4).map((level) =>
|
||||
[
|
||||
level.levelName,
|
||||
level.pictureDescription,
|
||||
level.pictureReference ? `参考图:${level.pictureReference}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
),
|
||||
tone: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
session.targetBinding &&
|
||||
!eventLog.some((event) => event.event === 'target_session')
|
||||
) {
|
||||
items.push({
|
||||
id: `session-target-${session.targetBinding.targetSessionId}`,
|
||||
meta: '目标',
|
||||
title: '拼图草稿已绑定',
|
||||
detail: formatTargetStage(session.targetBinding.targetStage),
|
||||
detailLines: [`目标会话:${session.targetBinding.targetSessionId}`],
|
||||
tone: 'done',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
items.length === 0 &&
|
||||
session.stage !== 'idle' &&
|
||||
!eventLog.some((event) => event.event === 'stage')
|
||||
) {
|
||||
const isWaiting =
|
||||
session.stage === 'waiting_template_confirmation' ||
|
||||
session.stage === 'waiting_user';
|
||||
const isDone = session.stage === 'target_ready';
|
||||
const tone: CreativeAgentProcessTone =
|
||||
session.stage === 'failed'
|
||||
? 'danger'
|
||||
: isWaiting
|
||||
? 'warning'
|
||||
: isDone
|
||||
? 'done'
|
||||
: 'active';
|
||||
items.push({
|
||||
id: `session-stage-${session.stage}`,
|
||||
meta: '阶段',
|
||||
title: getCreativeAgentStageDisplayLabel(
|
||||
session.stage,
|
||||
tone === 'active' ? 'active' : tone === 'done' ? 'done' : 'waiting',
|
||||
),
|
||||
detail: '当前状态',
|
||||
detailLines: [],
|
||||
tone,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function buildCreativeAgentProcessItems(
|
||||
eventLog: CreativeAgentSseEvent[],
|
||||
session: CreativeAgentSessionSnapshot | null,
|
||||
) {
|
||||
const terminalStageSeen = eventLog.some(
|
||||
(event) =>
|
||||
event.event === 'stage' &&
|
||||
(event.data.stage === 'waiting_template_confirmation' ||
|
||||
event.data.stage === 'target_ready' ||
|
||||
event.data.stage === 'waiting_user' ||
|
||||
event.data.stage === 'failed'),
|
||||
);
|
||||
const isComplete =
|
||||
eventLog.some((event) => event.event === 'done') ||
|
||||
terminalStageSeen ||
|
||||
session?.stage === 'waiting_template_confirmation' ||
|
||||
session?.stage === 'target_ready' ||
|
||||
session?.stage === 'waiting_user' ||
|
||||
session?.stage === 'failed';
|
||||
const activeStageEventIndex = eventLog.reduce(
|
||||
(latestIndex, event, index) => (event.event === 'stage' ? index : latestIndex),
|
||||
-1,
|
||||
);
|
||||
const context: ProcessBuildContext = {
|
||||
activeStageEventIndex,
|
||||
eventLog,
|
||||
isComplete,
|
||||
};
|
||||
|
||||
return [
|
||||
...buildThoughtSummaryItems(eventLog, isComplete),
|
||||
...eventLog
|
||||
.map((event, index) => buildEventProcessItem(event, index, context))
|
||||
.filter((item): item is CreativeAgentProcessItem => Boolean(item)),
|
||||
...buildSessionFallbackItems(session, eventLog),
|
||||
].slice(-24);
|
||||
}
|
||||
Reference in New Issue
Block a user