"use client" import { Check, ChevronDown, ChevronUp, Copy, Cpu } from "lucide-react" import type { Dispatch, SetStateAction } from "react" import { CodeBlock } from "@/components/code-block" import { isMxCellXmlComplete } from "@/lib/utils" import type { DiagramOperation, StructureOperation, ToolPartLike, } from "./types" interface ToolCallCardProps { part: ToolPartLike expandedTools: Record setExpandedTools: Dispatch>> onCopy: (callId: string, text: string, isToolCall: boolean) => void copiedToolCallId: string | null copyFailedToolCallId: string | null dict: { tools: { complete: string } chat: { copied: string; failedToCopy: string; copyResponse: string } } } /** * Colour an operation by what it does to the diagram: removes, adds, or changes. * * Takes an unknown rather than a string because this renders DURING streaming: the tool * input arrives character by character, so an operation is briefly `{}` or `{"op": "add_c` * before it is whole. A missing name is the normal mid-stream state, not an error. */ function opColour(op: unknown): string { if (typeof op !== "string") return "text-muted-foreground" if (op === "delete" || op === "remove" || op === "unlink" || op === "clear") return "text-red-600" if (op.startsWith("add") || op === "link") return "text-green-600" return "text-blue-600" } /** * The arguments worth showing beside an operation's name. * * A whitelist rather than "everything except op and id", because some operations carry a * whole nested graph (add_graph's nodes and edges) and dumping that turns one line into a * screenful. The excluded keys are summarised instead. */ const SHOWN_KEYS = [ "label", "name", "parent", "dir", "class", "role", "group", "shape", "cols", "lanes", "aspect", "source", "target", "title", ] as const function summarise(op: StructureOperation | undefined | null): string { if (!op || typeof op !== "object") return "" const parts: string[] = [] for (const key of SHOWN_KEYS) { const v = op[key] if (v === undefined || v === null || v === "") continue parts.push( `${key}=${Array.isArray(v) ? v.join("/") : String(v).slice(0, 60)}`, ) } // A graph carries its own nodes and edges; report the size, not the contents. const nodes = op.nodes const edges = op.edges if (Array.isArray(nodes)) parts.push( `${nodes.length} node${nodes.length === 1 ? "" : "s"}${ Array.isArray(edges) ? `, ${edges.length} edge${edges.length === 1 ? "" : "s"}` : "" }`, ) return parts.join(" ") } /** * `restructure_diagram`'s operations: structural steps, not XML patches. * * Written to survive PARTIAL data. This renders while the tool input is still streaming, so * an entry may be `{}`, or `{op: "add_contai"}`, or — because a JSON array is repaired as it * arrives — `undefined`. Every field is therefore treated as possibly absent rather than * validated up front: dropping incomplete entries would make rows appear and disappear as * the text arrives, and asserting on them crashes the whole message. */ function StructureOperationsDisplay({ operations, }: { operations: StructureOperation[] }) { return (
{operations.map((op, index) => (
{op?.op ?? "…"} {op?.id && ( {op.id} )} {summarise(op)}
))}
) } /** `edit_diagram`'s operations. Also streamed, so also written for partial entries. */ function OperationsDisplay({ operations }: { operations: DiagramOperation[] }) { return (
{operations.map((op, index) => (
{op?.operation ?? "…"} {op?.cell_id && ( cell_id: {op.cell_id} )}
{op?.new_xml && (
                                {op.new_xml}
                            
)}
))}
) } export function ToolCallCard({ part, expandedTools, setExpandedTools, onCopy, copiedToolCallId, copyFailedToolCallId, dict, }: ToolCallCardProps) { const callId = part.toolCallId const { state, input, output } = part // Default to expanded for all states (user can manually collapse if needed) const isExpanded = expandedTools[callId] ?? true const toolName = part.type?.replace("tool-", "") const isCopied = copiedToolCallId === callId const toggleExpanded = () => { setExpandedTools((prev) => ({ ...prev, [callId]: !isExpanded, })) } const getToolDisplayName = (name: string) => { switch (name) { case "restructure_diagram": return "Build Diagram" case "edit_diagram": return "Edit Diagram" case "search_stencils": return "Find Icons" // Only ever arrives from the server's cache-hit path now; the model cannot // call it. See createCachedStreamResponse in app/api/chat/route.ts. case "display_diagram": return "Generate Diagram" default: return name } } const handleCopy = () => { let textToCopy = "" if (input && typeof input === "object") { if (input.xml) { textToCopy = input.xml } else if (input.operations && Array.isArray(input.operations)) { textToCopy = JSON.stringify(input.operations, null, 2) } else if (Object.keys(input).length > 0) { textToCopy = JSON.stringify(input, null, 2) } } if (textToCopy) { onCopy(callId, textToCopy, true) } } return (
{getToolDisplayName(toolName)}
{state === "input-streaming" && (
)} {state === "output-available" && ( <> {dict.tools.complete} {isExpanded && ( )} )} {state === "output-error" && (() => { // Truncation only applies to a tool that streams raw XML, which // is now just the cached-answer replay. The engine tools send // structured operations, so a failure there is a real error. const isTruncated = toolName === "display_diagram" && !isMxCellXmlComplete(input?.xml) return isTruncated ? ( Truncated ) : ( Error ) })()} {input && Object.keys(input).length > 0 && ( )}
{input && isExpanded && (
{typeof input === "object" && input.xml ? ( state === "input-streaming" || state === "input-available" ? (
                                {input.xml}
                            
) : ( ) ) : typeof input === "object" && input.operations && Array.isArray(input.operations) ? ( // Dispatch by TOOL, not by whether an `operations` key exists: both // tools call their argument that, but the items have different shapes // (op/id versus operation/cell_id), and reading one as the other // printed a row of blank `cell_id:` labels. toolName === "restructure_diagram" ? ( ) : ( ) ) : typeof input === "object" && Object.keys(input).length > 0 ? ( ) : null}
)} {output && state === "output-error" && (() => { const isTruncated = toolName === "display_diagram" && !isMxCellXmlComplete(input?.xml) return (
{isTruncated ? "Output truncated due to length limits. Try a simpler request or increase the maxOutputLength." : output}
) })()} {/* What the tool actually returned. Worth showing on success, not only on error: restructure_diagram answers with an outline of the structure it built plus any notes about classes it could not honour, and that is the same text the model reads to name ids in its next call. */} {output && state === "output-available" && isExpanded && (
                        {typeof output === "string"
                            ? output.length > 4000
                                ? `${output.slice(0, 4000)}\n…`
                                : output
                            : String(output)}
                    
)}
) }