ToolCall.tsx (3380B)
1 import { useState } from "react"; 2 import type { ToolCall as ToolCallType } from "../../store/chat"; 3 import { ToolResultRenderer, getOperationCategory } from "../ToolResult/ToolResultRenderer"; 4 5 interface ToolCallProps { 6 toolCall: ToolCallType; 7 } 8 9 const STATUS_STYLES: Record<ToolCallType["status"], string> = { 10 pending: "text-gray-400", 11 running: "text-yellow-400", 12 complete: "text-green-400", 13 error: "text-red-400", 14 }; 15 16 const STATUS_LABELS: Record<ToolCallType["status"], string> = { 17 pending: "Pending", 18 running: "Running…", 19 complete: "Done", 20 error: "Error", 21 }; 22 23 const OP_BADGE_COLORS: Record<string, string> = { 24 blue: "bg-blue-900/50 text-blue-300", 25 green: "bg-green-900/50 text-green-300", 26 yellow: "bg-yellow-900/50 text-yellow-300", 27 purple: "bg-purple-900/50 text-purple-300", 28 orange: "bg-orange-900/50 text-orange-300", 29 gray: "bg-gray-800 text-gray-400", 30 }; 31 32 /** Render a tool call result or arguments as JSON. */ 33 function formatJson(str: string): string { 34 try { 35 return JSON.stringify(JSON.parse(str), null, 2); 36 } catch { 37 return str; 38 } 39 } 40 41 export function ToolCall({ toolCall }: ToolCallProps) { 42 const [expanded, setExpanded] = useState(false); 43 const opCategory = getOperationCategory(toolCall.name); 44 45 return ( 46 <div className="my-1 rounded-lg border border-gray-700 bg-gray-900/50 text-sm"> 47 <button 48 type="button" 49 onClick={() => setExpanded(!expanded)} 50 className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-gray-800/50 transition-colors" 51 > 52 <svg 53 className={`h-4 w-4 text-gray-400 transition-transform ${expanded ? "rotate-90" : ""}`} 54 fill="none" 55 viewBox="0 0 24 24" 56 stroke="currentColor" 57 strokeWidth={2} 58 > 59 <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /> 60 </svg> 61 <span 62 className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase ${OP_BADGE_COLORS[opCategory.color] ?? OP_BADGE_COLORS.gray}`} 63 > 64 {opCategory.label} 65 </span> 66 <span className="font-mono text-gray-300">{toolCall.name}</span> 67 <span className={`ml-auto text-xs ${STATUS_STYLES[toolCall.status]}`}> 68 {toolCall.status === "running" && ( 69 <span className="mr-1 inline-block h-2 w-2 animate-pulse rounded-full bg-yellow-400" /> 70 )} 71 {STATUS_LABELS[toolCall.status]} 72 </span> 73 </button> 74 75 {expanded && ( 76 <div className="border-t border-gray-700 px-3 py-2"> 77 <div className="mb-2"> 78 <span className="text-xs font-semibold uppercase text-gray-500"> 79 Arguments 80 </span> 81 <pre className="mt-1 overflow-x-auto rounded bg-gray-950 p-2 text-xs text-gray-300"> 82 {formatJson(toolCall.args)} 83 </pre> 84 </div> 85 {toolCall.result !== null && ( 86 <div> 87 <span className="text-xs font-semibold uppercase text-gray-500"> 88 Result 89 </span> 90 <div className="mt-1 overflow-x-auto rounded bg-gray-950 p-2 text-xs text-gray-300"> 91 <ToolResultRenderer content={toolCall.result} toolName={toolCall.name} /> 92 </div> 93 </div> 94 )} 95 </div> 96 )} 97 </div> 98 ); 99 }