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 63eb25d46..729cb7885 100644 --- a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx +++ b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx @@ -3,8 +3,8 @@ import { Button } from "antd"; import { Tooltip } from "@/components/ui/base/tooltip"; 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,8 +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, agentToolName, agentToolRetryLabel, agentToolStatus, friendlyAgentToolSummary } from "@/lib/canvas/agent-tool-presentation"; -import { agentOperationFailed, agentOperationLabel } from "@/lib/canvas/agent-operation-feed"; +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 }; @@ -39,6 +39,12 @@ export type CloudAgentChatMessage = { text: string; streaming?: boolean; reasoning?: boolean; + /** + * 本轮最终答复(服务端收尾闸门的结论)。false 表示这是过程正文——模型边做边说的那一段, + * 不是结论:运行还在继续,或者这次收尾被闸门拦下了。缺省按 true 处理(老后端没有这个字段)。 + * 界面不再为它单独出角标(用户口径),但标记仍如实保存。 + */ + final?: boolean; planItems?: CloudAgentPlanItem[]; question?: CloudAgentUserQuestion; meta?: string; @@ -47,6 +53,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 }; /** @@ -174,8 +208,7 @@ export function AgentChatMessage({ } return (
- {!isUser ? : null} -
+
{item.interjection ? ( 180 ? `${text.slice(0, 180)}…` : text; - const categoryIcon = category === "read" ? : category === "create" ? : ; + const categoryIcon = agentToolCategoryIcon(category); const retry = agentToolRetry(detail); const attempts = objectField(detail, "retryAttempts"); if (retry && Array.isArray(attempts)) { @@ -474,7 +507,7 @@ export function AgentToolCard({ ))} {isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? ( ) : null}
@@ -502,46 +535,67 @@ export function AgentToolCard({ } /** - * Agent 的操作记录(连续的 `role === "tool"` 消息)折成一行:只报最新一步,点击展开完整记录。 + * Agent 的操作记录(连续的 `role === "tool"` 消息)折成一行:报最新一步,点击展开完整记录。 + * + * 语义要点:折叠态整段只有这一行可见,所以**类别徽标必须在折叠行上**(清单 / 查看画面 / + * 修改画布),否则默认状态看不出刚才到底是读了清单还是真看了画面。 * - * 为什么默认收起:一轮运行的十几条工具记录会把正文挤出可视区,用户真正要读的是结论; * 展开状态用「用户覆盖 + 失败时默认展开」两段决定 —— 失败的操作不能被折进一行里看不见, - * 但用户手动收起之后就不再自动弹开(跑动中新步骤只会追加,不会重置展开状态)。 + * 但用户手动收起之后就不再自动弹开(跑动中新步骤只会追加,不会重置状态)。 + * `live` 由面板按"这一段是不是对话末尾且在跑"给出:只有还在推进时才流光。 */ export function AgentOperationFeed({ items, theme, references = [], onFocusNode, + live = false, }: { items: CloudAgentChatMessage[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; references?: CanvasResourceReference[]; onFocusNode?: (nodeId: string) => void; + live?: boolean; }) { const [userExpanded, setUserExpanded] = useState(null); + const reducedMotion = useReducedMotion(); const listId = useId(); const latest = items[items.length - 1]; if (!latest) return null; - const latestState = toolCardState(latest.title || "工具执行", latest.text, latest.detail); - const latestLabel = agentOperationLabel(latest); + const category = agentOperationCategory(latest); + const categoryLabel = agentToolCategoryLabel(agentToolName(latest.title || "工具执行", latest.detail), category); + const label = agentOperationSegmentLabel(items); const failed = items.some((item) => agentOperationFailed(item)); const expanded = userExpanded ?? failed; + const shimmering = live && !failed; return ( -
+
@@ -1171,12 +1225,17 @@ export function AgentPanelTabs({ ); } -function AgentTimelineMarker({ theme, tone, icon }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; tone: "agent" | "muted" | "tool" | "approval" | "error"; icon?: ReactNode }) { - const color = tone === "error" ? "#ef4444" : tone === "approval" ? "#f97316" : tone === "tool" ? "#4f7cff" : tone === "agent" ? theme.accent.primary : theme.node.muted; +/** + * 时间线标记:图标由调用方给(系统行给 Sparkles、错误与审批给 CircleAlert…),不再有默认 + * 的模型品牌 glyph —— 那个圆环标志会被读成"这条是模型/OpenAI 出品",与它实际表达的 + * "这是谁的一行"无关。 + */ +function AgentTimelineMarker({ theme, tone, icon }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; tone: "agent" | "muted" | "approval" | "error"; icon: ReactNode }) { + const color = tone === "error" ? "#ef4444" : tone === "approval" ? "#f97316" : tone === "agent" ? theme.accent.primary : theme.node.muted; return (