import { AlertCircle, Check, ChevronDown, Copy, Library, PenTool, } from "lucide-react" import { useState } from "react" import { Shimmer } from "@/components/ai-elements/shimmer" import { VersionCard as VersionCardView } from "@/components/canvas/version-card" import { CodeBlock } from "@/components/code-block" import { useDictionary } from "@/hooks/use-dictionary" import { useCopy } from "@/lib/clipboard" import { cn, isMxCellXmlComplete } from "@/lib/utils" import { type DiagramVersion, useVersionsStore } from "@/stores/versions-store" import { useChatEngine } from "./chat-engine" import type { DiagramOperation, ToolPartLike } from "./types" export function ToolInputDetails({ part }: { part: ToolPartLike }) { const { input, state } = part if (!input || typeof input !== "object") return null if (typeof input.xml === "string" && input.xml) { // Plain text while streaming; highlighting re-runs on every chunk return state === "input-streaming" || state === "input-available" ? (
                {input.xml}
            
) : ( ) } if (Array.isArray(input.operations)) { // Streamed or invalid input can hold anything: show only what React // can render (an object in place of a string would crash the chat) const shown = (input.operations as DiagramOperation[]).filter( (op) => typeof (op as { operation?: unknown })?.operation === "string", ) const text = (value: unknown) => typeof value === "string" ? value : "" return (
{shown.map((op, index) => (
{op.operation} {text(op.cell_id)}
{text(op.new_xml) && (
                                {text(op.new_xml)}
                            
)}
))}
) } if (Object.keys(input).length === 0) return null return } function toolText(part: ToolPartLike): string { const { input } = part if (!input || typeof input !== "object") return "" if (typeof input.xml === "string" && input.xml) return input.xml if (Array.isArray(input.operations)) { return JSON.stringify(input.operations, null, 2) } return JSON.stringify(input, null, 2) } /** * Card for one diagram version produced by a tool call: the shared card * with a link to what the AI wrote (shown in a panel under the card) */ function VersionCard({ part, version, isLatest, }: { part: ToolPartLike version: DiagramVersion isLatest: boolean }) { const dict = useDictionary() const t = dict.versions const [showCode, setShowCode] = useState(false) const { copied, copy } = useCopy() const isEdit = part.type === "tool-edit_diagram" return ( · } > {showCode && (
)}
) } /** Compact row for tool calls that are running, failed or produced no version */ function ToolRow({ part, icon, label, tone = "default", detail, }: { part: ToolPartLike icon: React.ReactNode label: React.ReactNode tone?: "default" | "error" | "warning" detail?: string }) { const dict = useDictionary() const [open, setOpen] = useState(false) const { copied, copy } = useCopy() const hasInput = !!part.input && Object.keys(part.input).length > 0 return (
{open && (
{hasInput && ( )} {detail && (

{detail}

)} {hasInput && }
)}
) } export function ToolActivity({ part, isLastMessage, }: { part: ToolPartLike isLastMessage: boolean }) { const dict = useDictionary() const t = dict.versions const engine = useChatEngine() const versions = useVersionsStore((s) => s.versions) const toolName = part.type?.replace("tool-", "") const { state, errorText, output } = part const versionIndex = versions.findIndex( (v) => v.toolCallId === part.toolCallId, ) if (state === "output-available" && versionIndex >= 0) { return ( ) } // The answer ended before the call did (stopped, network error) const running = isLastMessage && engine.isBusy if (toolName === "get_shape_library" && state !== "output-error") { const isDone = state === "output-available" return ( } label={ isDone ? ( t.libraryLoaded ) : running ? ( {t.loadingLibrary} ) : ( t.stopped ) } detail={ isDone && typeof output === "string" ? `${output.slice(0, 600)}${output.length > 600 ? "…" : ""}` : undefined } /> ) } // Stop turns a call still streaming into "Stopped by user" (right // away, or when the answer has ended for one not on screen yet): one // left like this was cut off (a closed connection, an error, a closed // tab) if (state === "input-streaming" || state === "input-available") { const failed = !running && !(isLastMessage && engine.stoppedRef.current) return ( : } tone={failed ? "error" : "default"} label={ running ? ( {toolName === "edit_diagram" ? t.editing : t.drawing} ) : failed ? ( t.failed ) : ( t.stopped ) } /> ) } if (state === "output-error") { const stopped = errorText === "Stopped by user" // Incomplete XML means the output hit the length limit. Without an // input the JSON was broken (the server repairs JSON cut short by // the limit). const truncated = !stopped && (toolName === "display_diagram" || toolName === "append_diagram") && typeof part.input?.xml === "string" && !isMxCellXmlComplete(part.input.xml) return ( } tone={stopped ? "default" : truncated ? "warning" : "error"} label={ stopped ? t.stopped : truncated ? dict.tools.truncated : isLastMessage && engine.isBusy ? t.failedRetrying : t.failed } detail={ stopped ? undefined : truncated ? t.truncatedHint : (errorText ?? (typeof output === "string" ? output : "")) } /> ) } // Finished without a version (sessions saved before versions existed) return ( } label={toolName === "edit_diagram" ? t.edited : t.created} /> ) }