import { useState } from "react"; import type { ToolCallEntry, ToolCallData } from "../../src/lib/types"; import { cn } from "../../src/lib/utils"; import { ToolPermissionButtons } from "../ai-elements/permission-request"; // ============================================================================= // 工具调用折叠组 — Anthropic: subtle card, left-border accent, compact layout // ============================================================================= interface ToolCallGroupProps { entries: ToolCallEntry[]; onPermissionRespond?: (requestId: string, optionId: string | null, optionKind: string | null) => void; } export function ToolCallGroup({ entries, onPermissionRespond }: ToolCallGroupProps) { const [expanded, setExpanded] = useState(false); if (entries.length === 0) return null; // 单个工具调用 if (entries.length === 1) { return (
); } // 多个工具调用 — 折叠组 const summary = buildSummary(entries); return (
{/* 折叠头 */} {/* 展开内容 */} {expanded && (
{entries.map((entry, i) => ( ))}
)}
); } // ============================================================================= // 单个工具卡片 — compact, left-accent, inline status // ============================================================================= interface SingleToolCardProps { tool: ToolCallData; compact?: boolean; onPermissionRespond?: (requestId: string, optionId: string | null, optionKind: string | null) => void; } function SingleToolCard({ tool, compact, onPermissionRespond }: SingleToolCardProps) { const [expanded, setExpanded] = useState(!compact); const statusIcon = (() => { switch (tool.status) { case "running": return ; case "complete": return ; case "error": return ; case "waiting_for_confirmation": return ; case "canceled": return ; case "rejected": return ; default: return null; } })(); const hasOutput = tool.status !== "running" && tool.status !== "waiting_for_confirmation" && (tool.rawOutput || tool.content); return (
{/* 标题行 — 单行紧凑 */}
setExpanded(!expanded)} > {statusIcon} {tool.title} {tool.status === "running" && ( running )}
{/* 权限请求按钮 */} {tool.status === "waiting_for_confirmation" && tool.permissionRequest && (
{})} />
)} {/* 展开详情 */} {expanded && (
{tool.rawInput && Object.keys(tool.rawInput).length > 0 && (
                {truncate(JSON.stringify(tool.rawInput, null, 2), 2000)}
              
)} {hasOutput && (
                {formatOutput(tool)}
              
)}
)}
); } // ============================================================================= // 工具函数 // ============================================================================= /** 构建统计摘要 */ function buildSummary(entries: ToolCallEntry[]): string { const toolCounts = new Map(); for (const entry of entries) { const name = simplifyToolName(entry.toolCall.title); toolCounts.set(name, (toolCounts.get(name) || 0) + 1); } const parts: string[] = []; for (const [name, count] of toolCounts) { parts.push(count === 1 ? name : `${count} 次${name}`); } if (parts.length === 0) return `${entries.length} 个工具调用`; if (parts.length === 1) return parts[0]; return `${entries.length} 个工具: ${parts.join("、")}`; } /** 简化工具名称 */ function simplifyToolName(title: string): string { const match = title.match(/^(\w+)/); return match ? match[1] : title; } /** 格式化工具输出 */ function formatOutput(tool: ToolCallData): string { if (tool.content && tool.content.length > 0) { const texts = tool.content .filter((c): c is Extract => c.type === "content") .filter((c) => c.content.type === "text" && "text" in c.content) .map((c) => (c.content as { text: string }).text); if (texts.length > 0) return truncate(texts.join("\n"), 2000); } if (tool.rawOutput && Object.keys(tool.rawOutput).length > 0) { return truncate(JSON.stringify(tool.rawOutput, null, 2), 2000); } return ""; } function truncate(str: string, max: number): string { return str.length > max ? str.slice(0, max) + "..." : str; }