import { AlertCircle, Check, ChevronDown, Code2, Copy, Library, PenTool, Redo2, RotateCcw, Undo2, } from "lucide-react" import { useState } from "react" import { Shimmer } from "@/components/ai-elements/shimmer" import { CodeBlock } from "@/components/code-block" import { useDictionary } from "@/hooks/use-dictionary" import { useCopy } from "@/lib/clipboard" import { formatMessage } from "@/lib/i18n/utils" import { cn, isMxCellXmlComplete } from "@/lib/utils" import { describeChanges, describeTotals } from "@/lib/version-text" import { useUiStore } from "@/stores/ui-store" 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) } /** Picture of a version (SVG data URL), or what stands in for it */ export function VersionThumb({ svg, dimmed, className, }: { /** undefined or null: still being made; "": none could be made */ svg: string | null | undefined dimmed?: boolean className?: string }) { const t = useDictionary().versions if (svg) { return ( // biome-ignore lint/performance/noImgElement: data URL thumbnail ) } if (svg === "") { return {t.noPreview} } return ( {t.rendering} ) } /** * Card for one diagram version produced by a tool call. The latest shows * its picture; older ones fold into one row (the picture opens Compare). */ function VersionCard({ part, version, isLatest, }: { part: ToolPartLike version: DiagramVersion isLatest: boolean }) { const dict = useDictionary() const t = dict.versions const engine = useChatEngine() const undoneVersionId = useVersionsStore((s) => s.undoneVersionId) const onCanvasVersionId = useVersionsStore((s) => s.onCanvasVersionId) const openCompare = useUiStore((s) => s.openCompare) const [showCode, setShowCode] = useState(false) const { copied, copy } = useCopy() const isUndone = undoneVersionId === version.id const isOnCanvas = onCanvasVersionId === version.id const isEdit = part.type === "tool-edit_diagram" // Undo/redo belongs to the newest change while it is (or was) on the // canvas; after an older version was restored, this one is restored too const canUndo = isLatest && version.beforeXml !== undefined && (isOnCanvas || isUndone) const title = isUndone ? t.undone : isEdit ? t.edited : t.created const summary = version.fromScratch ? describeTotals(version.summary, t) : describeChanges(version.summary, t) const badge = ( v{version.number} ) const actionClass = "inline-flex h-7 shrink-0 items-center gap-1 rounded-lg px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40" const restoreButton = ( ) // What the AI wrote, on every card (main showed it on every tool call) const codeButton = ( ) const codePanel = showCode && (
) if (!isLatest) { return (
{badge}
{title}
{summary}
{codeButton} {restoreButton}
{codePanel}
) } return (
{badge}
{title}
{summary}
{codeButton} {canUndo ? ( ) : ( restoreButton )}
{codePanel}
) } /** 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} /> ) }