import { ChevronDown, ChevronRight, Play, Check, AlertTriangle, Terminal, Maximize2, } from 'lucide-react'; import { useState, useMemo } from 'react'; import type { ChatMessage } from '../../types/protocol'; import { ToolDetailModal } from '../Chat/ToolDetailModal'; interface ToolPanelProps { messages: ChatMessage[]; } interface ToolCallItem { toolCallId: string; toolName: string; status: 'calling' | 'result' | 'pending'; arguments?: unknown; resultContent: string; callContent: string; durationMs?: number; } function formatDuration(ms: number): string { if (ms < 1000) { return `${ms}ms`; } if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; } const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); return `${minutes}m ${seconds}s`; } function formatResultText(content: string): string { if (!content) return ''; try { const parsed = JSON.parse(content); return JSON.stringify(parsed, null, 2); } catch { return content; } } function mergeToolMessages(messages: ChatMessage[]): ToolCallItem[] { const map = new Map(); for (const m of messages) { if (m.role !== 'tool' || !m.type?.startsWith('tool_')) continue; const key = m.toolCallId || m.id; let entry = map.get(key); if (!entry) { entry = { toolCallId: key, toolName: m.toolName || 'Unknown', status: 'calling', arguments: undefined, resultContent: '', callContent: '', }; map.set(key, entry); } if (m.type === 'tool_call') { entry.arguments = m.arguments; entry.callContent = m.content; } else if (m.type === 'tool_result') { entry.status = 'result'; entry.resultContent = m.content; entry.durationMs = m.durationMs; } else if (m.type === 'tool_pending') { entry.status = 'pending'; entry.resultContent = m.content; } } return Array.from(map.values()); } export function ToolPanel({ messages }: ToolPanelProps) { const [expandedTools, setExpandedTools] = useState>(new Set()); const [detailModalTool, setDetailModalTool] = useState(null); const toolCalls = useMemo(() => mergeToolMessages(messages), [messages]); const toggleExpand = (id: string) => { setExpandedTools((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const getStatusConfig = (status: ToolCallItem['status']) => { switch (status) { case 'calling': return { icon: Play, iconColor: 'text-amber-400', bgClass: 'bg-amber-400', borderClass: 'border-amber-500/30', label: '执行中', labelClass: 'text-amber-400', }; case 'result': return { icon: Check, iconColor: 'text-emerald-400', bgClass: 'bg-emerald-400', borderClass: 'border-emerald-500/30', label: '已完成', labelClass: 'text-emerald-400', }; case 'pending': return { icon: AlertTriangle, iconColor: 'text-orange-400', bgClass: 'bg-orange-400', borderClass: 'border-orange-500/30', label: '待确认', labelClass: 'text-orange-400', }; } }; if (toolCalls.length === 0) { return (
工具调用
暂无工具调用
); } return ( <>
工具调用 {toolCalls.length}
{toolCalls.map((tool) => { const config = getStatusConfig(tool.status); const StatusIcon = config.icon; const isExpanded = expandedTools.has(tool.toolCallId); const hasResult = tool.resultContent.length > 0; const displayContent = tool.resultContent || tool.callContent; const formattedContent = formatResultText(displayContent); const previewLines = displayContent.split('\n').slice(0, 2).join('\n'); const hasMore = displayContent.split('\n').length > 2 || displayContent.length > 200; return (
{isExpanded ? ( ) : ( )}
{/* 结果预览区 — 始终可见 */} {hasResult && (
toggleExpand(tool.toolCallId)} > {isExpanded ? (
                            {formattedContent}
                          
) : ( {previewLines} )}
{!isExpanded && hasMore && (
点击展开全部 ({displayContent.split('\n').length} 行)
)}
)} {/* 展开区域:参数 */} {isExpanded && tool.arguments ? (
参数:
                        {JSON.stringify(tool.arguments, null, 2)}
                      
) : null}
); })}
{detailModalTool && ( setDetailModalTool(null)} /> )} ); } const animStyles = ` @keyframes tool-result-in { from { max-height: 0; opacity: 0; } to { max-height: 80px; opacity: 1; } } .tool-card { transition: border-color 0.5s ease; } .tool-status-icon { transition: transform 0.3s ease; } .tool-result-enter { animation: tool-result-in 0.4s ease-out; } .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } `;