diff --git a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx index d9a43d030..35a34f211 100644 --- a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx +++ b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx @@ -1,10 +1,10 @@ import { agentCanvasActions, agentCanvasActionLabel } from "@/lib/canvas/agent-canvas-actions"; import { Button } from "antd"; import { Tooltip } from "@/components/ui/base/tooltip"; -import { useCallback, useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; +import { useCallback, useEffect, useId, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; -import { motion, useReducedMotion } from "motion/react"; -import { ArrowLeft, ArrowUp, AtSign, Bookmark, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, ListChecks, LoaderCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, X, XCircle } from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { ArrowLeft, ArrowUp, AtSign, Bookmark, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, List, ListChecks, LoaderCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, Wrench, X, XCircle } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown"; @@ -13,7 +13,8 @@ import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textare import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; import type { Skill, SkillPreset } from "@/services/api/skills"; import { buildSkillMentionReferences } from "@/services/skill-runtime"; -import { agentToolCategory, agentToolCategoryLabel, agentToolErrorClassLabel, agentToolStatus, friendlyAgentToolSummary } from "@/lib/canvas/agent-tool-presentation"; +import { agentToolCategory, agentToolCategoryLabel, agentToolErrorClassLabel, agentToolName, agentToolRetryLabel, agentToolStatus, friendlyAgentToolSummary, type AgentToolCategory } from "@/lib/canvas/agent-tool-presentation"; +import { agentOperationCategory, agentOperationFailed, agentOperationSegmentLabel } from "@/lib/canvas/agent-operation-feed"; import { agentToolRetry, type AgentToolRetryAttempt } from "@/lib/canvas/agent-tool-retry"; export type CloudAgentChatAttachment = { id: string; name: string; url: string }; @@ -38,6 +39,12 @@ export type CloudAgentChatMessage = { text: string; streaming?: boolean; reasoning?: boolean; + /** + * 本轮最终答复(服务端收尾闸门的结论)。false 表示这是过程正文——模型边做边说的那一段, + * 不是结论:运行还在继续,或者这次收尾被闸门拦下了。缺省按 true 处理(老后端没有这个字段)。 + * 界面不再为它单独出角标(用户口径),但标记仍如实保存。 + */ + final?: boolean; planItems?: CloudAgentPlanItem[]; /** 运行已进入终态,但计划项仍未全部完成;用于历史恢复时停止显示 loading。 */ planTerminal?: boolean; @@ -48,6 +55,34 @@ export type CloudAgentChatMessage = { interjection?: "sent" | "undelivered"; }; +/** + * 助手正文的收尾语义(工作项 A):final=false 是过程说明,true / 缺省是结论。 + * 缺省按"结论"处理是为了兼容升级前的后端事件——那时所有正文都按最终正文展示。 + */ +export function agentAssistantFinality(payload: Record): boolean { + return payload.final !== false; +} + +/** + * 服务端控制消息(例如运行时的收尾闸门 `completion_blocked`)在时间线里的表示: + * 它是服务端说明,不是用户发言,所以走 system 行——绝不能渲染成真人 user 气泡。 + */ +export function agentControlMessage(id: string, text: string, meta?: string): CloudAgentChatMessage { + return { id, role: "system", text, meta }; +} + +/** + * 类别的图标语言:三档互不串味 —— 清单用 List(读到存在与规模)、画面用 Eye(图片字节真的 + * 交给了模型)、写画布用 Pencil / 创建用 Plus;未登记的工具走中性的 Wrench。 + */ +function agentToolCategoryIcon(category: AgentToolCategory) { + if (category === "vision") return ; + if (category === "read") return ; + if (category === "create") return ; + if (category === "operate") return ; + return ; +} + export type CloudAgentQuickAction = { label: string; prompt: string }; /** @@ -175,8 +210,7 @@ export function AgentChatMessage({ } return (
- {!isUser ? : null} -
+
{item.interjection ? ( 0 || state.isError; const conciseError = text.length > 180 ? `${text.slice(0, 180)}…` : text; - const categoryIcon = category === "read" ? : category === "create" ? : ; - const header = ( -
- - {categoryIcon} - {categoryLabel} - - {summary} - {state.label !== "已完成" ? ( - - {state.label} - - ) : null} -
- ); - const detailBody = ( - <> - {actions.length ? ( -
- {visibleActions.map((action) => ( - - ))} - {isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? ( - - ) : null} -
- ) : null} - {state.isError && text && text !== summary ? ( - - {conciseError} - - ) : null} - {state.isError && text.length > 180 ? ( -
- 查看完整错误详情 -

{text}

-
- ) : null} - {state.isError && objectField(objectField(detail, "result"), "taskId") ? ( - - 任务 ID:{String(objectField(objectField(detail, "result"), "taskId"))} - - ) : null} - - ); - if (hasDetails) { + const categoryIcon = agentToolCategoryIcon(category); + const retry = agentToolRetry(detail); + const attempts = objectField(detail, "retryAttempts"); + if (retry && Array.isArray(attempts)) { + const label = agentToolRetryLabel(retry); return ( -
- - -
- {header} -
-