"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 (
{toolCall.result}
{resultLong && (
)}
正在执行…
)} {/* 确认卡片:仅在 confirming 态且有 confirmationId 时展示 */} {toolCall.status === "confirming" && toolCall.confirmationId && toolCall.riskLevel && (
{args}
$
{parsed.command}
{extras.length > 0 &&