"use client"; import { useCallback, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { ToolConfirmationCard } from "@/components/tools/ToolConfirmationCard"; import { Badge, type BadgeTone } from "@/components/ui/Badge"; import { cn } from "@/lib/utils/cn"; import { useChatStore } from "@/lib/stores/chat-store"; import type { ToolCallDisplay } from "@/lib/stores/chat-store"; /** 结果文本展示截断长度(超过则默认折叠) */ const RESULT_COLLAPSE_LENGTH = 280; /** 工具调用状态 → 徽章色调 + 中文文案 */ const STATUS_META: Record< ToolCallDisplay["status"], { label: string; tone: BadgeTone; pulse?: boolean } > = { running: { label: "执行中", tone: "warning", pulse: true }, confirming: { label: "待确认", tone: "warning", pulse: true }, success: { label: "已完成", tone: "success" }, error: { label: "失败", tone: "danger" }, denied: { label: "已拒绝", tone: "danger" }, skipped: { label: "已跳过", tone: "neutral" }, }; export interface ToolCallBlockProps { toolCall: ToolCallDisplay; /** 消息所属 run_id(确认端点调用需要) */ runId?: string; onApprove?: ( runId: string, confirmationId: string, toolCallId: string, ) => void; onDeny?: (runId: string, confirmationId: string, toolCallId: string) => void; } /** * 工具调用块(对话流 tool.* 事件可视化)。结构与 ThoughtBlock 对齐: * 可折叠头(工具名 + 状态徽章)+ 展开态结果文本(截断 + 展开)。 * * 状态为 confirming 时,在结果下方嵌入 ToolConfirmationCard(复用)。 * 样式遵循琉璃镜(玻璃 + 矿物色),与 ThoughtBlock 视觉一致。 * * 状态机: * running(金,脉冲)→ success(铜绿)/ error(朱砂)/ denied(朱砂)/ skipped(灰) * 中间可能进入 confirming(金,脉冲)展示确认卡片,用户操作后回 running。 */ export function ToolCallBlock({ toolCall, runId, onApprove, onDeny, }: ToolCallBlockProps) { const meta = STATUS_META[toolCall.status] ?? STATUS_META.running; // running / confirming 时默认展开让用户看到实时状态;终态默认折叠 const isActive = toolCall.status === "running" || toolCall.status === "confirming"; const [open, setOpen] = useState(isActive); // 确认卡片需要 confirmation_id;同时需要本地 loading 态(用户点击后等待后端事件) const [resolving, setResolving] = useState(false); const handleApprove = () => { if (!runId || !toolCall.confirmationId || !onApprove) return; setResolving(true); onApprove(runId, toolCall.confirmationId, toolCall.toolCallId); // 不在此处清 resolving——后端推 tool.completed 时 toolCall.status 会变化, // 此组件卸载或重渲染。若后端迟迟不推(如 410/503),用户可在错误提示后重试。 // 保守起见,等几秒自动解除 loading,避免按钮永久转圈。 setTimeout(() => setResolving(false), 4000); }; const handleDeny = () => { if (!runId || !toolCall.confirmationId || !onDeny) return; setResolving(true); onDeny(runId, toolCall.confirmationId, toolCall.toolCallId); setTimeout(() => setResolving(false), 4000); }; const hasResult = toolCall.result !== undefined && toolCall.result !== ""; const resultLong = (toolCall.result?.length ?? 0) > RESULT_COLLAPSE_LENGTH; const [resultExpanded, setResultExpanded] = useState(false); return (
{open && (
{/* 调用参数:confirming 态由确认卡片承载 args,避免重复 */} {toolCall.args && toolCall.status !== "confirming" && ( )} {/* 结果文本(success/error/denied/skipped 才有) */} {hasResult && (
                    {toolCall.result}
                  
{resultLong && ( )}
)} {/* 文件预览:write_file 成功时提供独立面板预览入口 */} {toolCall.status === "success" && toolCall.toolName === "write_file" && ( )} {/* 执行中无结果时显示轻量提示 */} {!hasResult && toolCall.status === "running" && (

正在执行…

)} {/* 确认卡片:仅在 confirming 态且有 confirmationId 时展示 */} {toolCall.status === "confirming" && toolCall.confirmationId && toolCall.riskLevel && ( )}
)}
); } function WrenchIcon() { return ( ); } /** * 打开全局面板预览沙箱文件。 */ function FilePreviewButton({ args, runId, }: { args: string | undefined; runId: string | undefined; }) { const openPreviewFile = useChatStore((s) => s.openPreviewFile); const filePath = extractFilePath(args); const handleOpen = useCallback(() => { openPreviewFile({ path: filePath, runId }); }, [filePath, runId, openPreviewFile]); if (!filePath) return null; return ( ); } /** * 从 write_file 的 args JSON 中提取文件路径。 * args 格式:``{"path": "output/report.html", "content": "..."}`` */ function extractFilePath(args: string | undefined): string { if (!args) return ""; try { const parsed = JSON.parse(args) as Record; const p = parsed.path; return typeof p === "string" && p.trim() ? p.trim() : ""; } catch { return ""; } } /** 单个入参值的截断长度:超过则折叠为摘要(write_file 的 content 可能极长) */ const ARG_VALUE_PREVIEW = 120; /** 解析工具调用入参 JSON 字符串;失败或非对象时回退到原始文本展示。 */ function parseToolArgs( args: string | undefined, ): Record | null { if (!args) return null; try { const parsed = JSON.parse(args); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : null; } catch { return null; } } /** 把入参值格式化为可读字符串;过长的字符串折叠为「前缀…(共 N 字符)」。 */ function previewToolArgValue(value: unknown): string { if (typeof value === "string") { return value.length > ARG_VALUE_PREVIEW ? `${value.slice(0, ARG_VALUE_PREVIEW)}…(共 ${value.length} 字符)` : value; } if (value === null) return "null"; if (typeof value === "object") { try { return JSON.stringify(value); } catch { return String(value); } } return String(value); } /** * 入参键值对展示(dl/dt/dd)。长值经 previewToolArgValue 折叠。 */ function ToolArgKeyValue({ entries }: { entries: [string, unknown][] }) { return (
{entries.map(([k, v]) => (
{k}:
{previewToolArgValue(v)}
))}
); } /** * 工具调用入参展示。所有状态(只要有 args)都显示,让用户能判断 AI 实际调用了什么—— * 尤其 shell 工具的 command,否则仅看执行结果无法判断命令是否正确。 * * shell:终端样式突出 command(用户最关心的「命令」),其余键(timeout_ms/stdin)附在下方; * 其它工具:键值对形式,长值折叠。 */ function ToolCallArgs({ toolName, args, }: { toolName: string; args: string | undefined; }) { const parsed = parseToolArgs(args); // 解析失败:回退为原始文本(args 为空则不渲染) if (!parsed) { if (!args) return null; return (
调用参数
          {args}
        
); } const entries = Object.entries(parsed); if (entries.length === 0) return null; // shell:突出命令行,其余参数作为附属键值对 if (toolName === "shell" && typeof parsed.command === "string") { const extras = entries.filter(([k]) => k !== "command"); return (
命令
          $ 
          {parsed.command}
        
{extras.length > 0 && }
); } return (
调用参数
); }