From f88ada378e79a29dedb1c17b40a72ea491f1bbfb Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 7 Jul 2026 15:42:54 +0800 Subject: [PATCH] feat: add canvas agent progress timeline --- components/canvas/CanvasChatPanel.tsx | 165 ++++++++++++++---- .../canvas/chat-panel/canvas-agent-stream.ts | 111 +++++++++++- tests/routes/canvas-chat-helpers.test.ts | 40 +++++ 3 files changed, 282 insertions(+), 34 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 0c1439f39..d5e82e3d0 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -144,15 +144,16 @@ import { type CanvasToolbarItemKey, } from "@/lib/canvas-toolbar-config"; import { - AGENT_PROGRESS_STEPS, type AgentProgressStreamPayload, type AgentTargetLockRefPreview, type AgentTargetLockStreamPayload, + buildAgentProgressNodeSteps, type CanvasOperationStreamPayload, type CanvasViewOperationStreamPayload, type CanvasMediaStreamPayload, filterCanvasMediaPayloadForUnhandledItems, getAgentProgressFallbackTitle, + getAgentProgressStepLabel, getAgentTargetLockTitle, getAgentTargetResolutionLabel, getCanvasMediaPayloadHandledItemIds, @@ -1623,6 +1624,130 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) { ); } +function CanvasAgentProgressTimeline({ + progress, +}: { + progress?: AgentProgressStreamPayload | null; +}) { + const steps = buildAgentProgressNodeSteps(progress); + const title = progress?.title?.trim() || getAgentProgressFallbackTitle(progress); + const isFailed = progress?.stage === "failed"; + const isCompleted = progress?.stage === "completed"; + const isClarify = progress?.stage === "clarify"; + + return ( +
+
+
+ + {isFailed ? ( + + ) : isCompleted ? ( + + ) : ( + + )} + + + {title} + +
+ +
+
+ {steps.map((step, index) => { + const isStepCompleted = step.state === "completed"; + const isStepCurrent = step.state === "current"; + const isStepFailed = step.state === "failed"; + const lineIsActive = + isStepCompleted || isStepCurrent || isStepFailed; + + return ( +
+
+ + {isStepCompleted ? ( + + ) : isStepFailed ? ( + + ) : isStepCurrent ? ( + + ) : ( + + )} + {isStepCurrent ? ( + + ) : null} + + + {step.label} + +
+ {index < steps.length - 1 ? ( + + ) : null} +
+ ); + })} +
+
+
+
+ ); +} + function CanvasComposerUploadProgressDialog({ progress, }: { @@ -12318,26 +12443,10 @@ export function CanvasChatPanel({ activeAgentProgress?.title?.trim() || getAgentProgressFallbackTitle(activeAgentProgress); const agentProgressMessage = activeAgentProgress?.message?.trim() || "我正在根据当前上下文持续推进这一步。"; - const agentProgressSteps = useMemo(() => { - const currentStepIndex = Math.max( - 0, - AGENT_PROGRESS_STEPS.findIndex((step) => step.key === activeAgentProgress?.currentStep) - ); - return AGENT_PROGRESS_STEPS.map((step, index) => { - let state: "upcoming" | "current" | "completed" | "failed" = "upcoming"; - if (activeAgentProgress?.stage === "failed") { - state = index < currentStepIndex ? "completed" : index === currentStepIndex ? "failed" : "upcoming"; - } else if (activeAgentProgress?.stage === "completed") { - state = "completed"; - } else { - state = index < currentStepIndex ? "completed" : index === currentStepIndex ? "current" : "upcoming"; - } - return { - ...step, - state, - }; - }); - }, [activeAgentProgress]); + const agentProgressSteps = useMemo( + () => buildAgentProgressNodeSteps(activeAgentProgress), + [activeAgentProgress] + ); const renderModelSelectContent = () => ( -
- - 分析中 -
- + )}
@@ -14660,9 +14764,10 @@ export function CanvasChatPanel({ {agentProgressTitle} - {AGENT_PROGRESS_STEPS.find( - (step) => step.key === activeAgentProgress.currentStep - )?.label || "处理中"} + {getAgentProgressStepLabel( + activeAgentProgress.currentStep || "lock_target", + activeAgentProgress + )}
diff --git a/components/canvas/chat-panel/canvas-agent-stream.ts b/components/canvas/chat-panel/canvas-agent-stream.ts index 3e2480894..e7a6246a2 100644 --- a/components/canvas/chat-panel/canvas-agent-stream.ts +++ b/components/canvas/chat-panel/canvas-agent-stream.ts @@ -206,14 +206,26 @@ export type AgentProgressStreamPayload = { capability?: string; }; +export type AgentProgressStepState = + | "upcoming" + | "current" + | "completed" + | "failed"; + +export type AgentProgressNodeStep = { + key: NonNullable; + label: string; + state: AgentProgressStepState; +}; + export const AGENT_PROGRESS_STEPS: Array<{ key: NonNullable; label: string; }> = [ - { key: "lock_target", label: "整理素材" }, - { key: "analyze", label: "理解需求" }, - { key: "select_tool", label: "优化提示" }, - { key: "execute", label: "提交生成" }, + { key: "lock_target", label: "理解用户需求" }, + { key: "analyze", label: "分析使用工具" }, + { key: "select_tool", label: "确认执行工具" }, + { key: "execute", label: "执行中" }, { key: "finalize", label: "整理结果" }, ]; @@ -260,3 +272,94 @@ export function getAgentProgressFallbackTitle(payload?: AgentProgressStreamPaylo return "正在理解需求"; } } + +export function getAgentProgressActionLabel( + payload?: AgentProgressStreamPayload | null +) { + switch (payload?.capability) { + case "analyze": + return "图片分析"; + case "generic_edit": + return "修图"; + case "remove_background": + return "抠图"; + case "remove_watermark": + return "去水印"; + case "enhance": + return "图像增强"; + case "resize": + return "尺寸调整"; + case "extend": + return "扩图"; + case "angle_variant": + return "多视角"; + case "local_edit": + return "局部编辑"; + case "box_cutout": + return "框选抠图"; + case "generate_image": + return "生图"; + case "generate_video": + return "视频生成"; + default: + return "工具"; + } +} + +export function getAgentProgressStepLabel( + stepKey: NonNullable, + payload?: AgentProgressStreamPayload | null +) { + const actionLabel = getAgentProgressActionLabel(payload); + switch (stepKey) { + case "lock_target": + return "理解用户需求"; + case "analyze": + return payload?.capability === "analyze" ? "分析图片" : "分析使用工具"; + case "select_tool": + return actionLabel === "工具" ? "确认执行工具" : `确认${actionLabel}工具`; + case "execute": + return actionLabel === "工具" ? "执行中" : `${actionLabel}中`; + case "finalize": + return "整理结果"; + default: + return "处理中"; + } +} + +export function buildAgentProgressNodeSteps( + payload?: AgentProgressStreamPayload | null +): AgentProgressNodeStep[] { + const currentStep = payload?.currentStep || "lock_target"; + const currentStepIndex = Math.max( + 0, + AGENT_PROGRESS_STEPS.findIndex((step) => step.key === currentStep) + ); + + return AGENT_PROGRESS_STEPS.map((step, index) => { + let state: AgentProgressStepState = "upcoming"; + if (payload?.stage === "failed") { + state = + index < currentStepIndex + ? "completed" + : index === currentStepIndex + ? "failed" + : "upcoming"; + } else if (payload?.stage === "completed") { + state = "completed"; + } else { + state = + index < currentStepIndex + ? "completed" + : index === currentStepIndex + ? "current" + : "upcoming"; + } + + return { + key: step.key, + label: getAgentProgressStepLabel(step.key, payload), + state, + }; + }); +} diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 1ed8f6a5a..2504c581c 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -9,8 +9,10 @@ import { } from "@/components/canvas/chat-panel/canvas-chat-media"; import { filterCanvasMediaPayloadForUnhandledItems, + buildAgentProgressNodeSteps, getCanvasMediaPayloadHandledItemIds, getCanvasOperationHandledMediaItemIds, + getAgentProgressStepLabel, isCanvasAgentViewOperationClientEnabled, normalizeCanvasAgentClientBoolean, shouldRetryCanvasViewOperationAfterRefresh, @@ -1210,6 +1212,44 @@ test.describe("canvas chat pure helpers", () => { expect(editText).toContain("替换背景"); }); + test("builds capability-aware agent progress node labels", () => { + const selectingSteps = buildAgentProgressNodeSteps({ + stage: "planning", + currentStep: "select_tool", + capability: "remove_background", + }); + + expect(selectingSteps.map((step) => step.label)).toEqual([ + "理解用户需求", + "分析使用工具", + "确认抠图工具", + "抠图中", + "整理结果", + ]); + expect(selectingSteps.map((step) => step.state)).toEqual([ + "completed", + "completed", + "current", + "upcoming", + "upcoming", + ]); + + const executingSteps = buildAgentProgressNodeSteps({ + stage: "executing", + currentStep: "execute", + capability: "remove_background", + }); + + expect(executingSteps[2]?.state).toBe("completed"); + expect(executingSteps[3]).toMatchObject({ + label: "抠图中", + state: "current", + }); + expect(getAgentProgressStepLabel("execute", { capability: "generate_video" })).toBe( + "视频生成中" + ); + }); + test("resolves deictic canvas edit requests to latest results and direct tools", () => { const latestResultUrl = "https://cdn.example.com/latest-cat-result.png"; const requestPolicy = resolveCanvasAgentRequestPolicy({