Files
next-ai-draw-io/components/chat/ToolCallCard.tsx

384 lines
16 KiB
TypeScript
Raw Normal View History

"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"
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
import type {
DiagramOperation,
StructureOperation,
ToolPartLike,
} from "./types"
interface ToolCallCardProps {
part: ToolPartLike
expandedTools: Record<string, boolean>
setExpandedTools: Dispatch<SetStateAction<Record<string, boolean>>>
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 }
}
}
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
/**
* 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 (
<div className="space-y-1">
{operations.map((op, index) => (
<div
key={`${op?.op ?? "pending"}-${op?.id ?? index}-${index}`}
className="flex items-baseline gap-2 px-2 py-1 rounded bg-background/50 border border-border/40"
>
<span
className={`text-[10px] font-medium uppercase tracking-wide shrink-0 ${opColour(op?.op)}`}
>
{op?.op ?? "…"}
</span>
{op?.id && (
<span className="text-xs font-mono text-foreground/80 shrink-0">
{op.id}
</span>
)}
<span className="text-[11px] text-muted-foreground font-mono break-all">
{summarise(op)}
</span>
</div>
))}
</div>
)
}
/** `edit_diagram`'s operations. Also streamed, so also written for partial entries. */
function OperationsDisplay({ operations }: { operations: DiagramOperation[] }) {
return (
<div className="space-y-3">
{operations.map((op, index) => (
<div
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
key={`${op?.operation ?? "pending"}-${op?.cell_id ?? index}-${index}`}
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
>
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
<span
className={`text-[10px] font-medium uppercase tracking-wide ${
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
op?.operation === "delete"
? "text-red-600"
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
: op?.operation === "add"
? "text-green-600"
: "text-blue-600"
}`}
>
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
{op?.operation ?? "…"}
</span>
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
{op?.cell_id && (
<span className="text-xs text-muted-foreground">
cell_id: {op.cell_id}
</span>
)}
</div>
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
{op?.new_xml && (
<div className="px-3 py-2">
<pre className="text-[11px] font-mono text-foreground/80 bg-muted/30 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
{op.new_xml}
</pre>
</div>
)}
</div>
))}
</div>
)
}
export function ToolCallCard({
part,
expandedTools,
setExpandedTools,
onCopy,
copiedToolCallId,
copyFailedToolCallId,
dict,
}: ToolCallCardProps) {
const callId = part.toolCallId
const { state, input, output } = part
Add VLM-based diagram validation (#602) * [Feature] Add VLM-based diagram validation Add automatic VLM (Vision Language Model) validation after display_diagram tool execution. The system captures a screenshot of the rendered diagram, sends it to a VLM for visual analysis, and uses feedback to improve diagram quality through the existing retry mechanism. Changes: - Add /api/validate-diagram endpoint for VLM validation - Add diagram-validator.ts for client-side validation orchestration - Add validation-prompts.ts for VLM system prompts - Add ValidationCard component to display validation status in chat - Add PNG capture functionality to diagram context - Integrate validation into tool handlers with retry support (max 3) - Add "Improve with Suggestions" button for manual regeneration - Add settings toggle to enable/disable VLM validation - Add getValidationModel() helper in ai-providers.ts * refactor(validation): use AI SDK structured outputs and address review feedback - Replace generateText + manual JSON parsing with generateObject and Zod schema for type-safe structured validation output - Use AbortSignal.timeout() instead of Promise.race for cleaner timeout handling - Add timeout validation with minimum 1000ms to handle malformed env values - Remove unused xml parameter from validateRenderedDiagram API - Remove parseValidationResponse function (now handled by schema) - Clear validationStates on session switch and new chat to prevent memory leak - Update 100ms render delay comment to clarify best-effort heuristic - Remove unused useEffect import from ValidationCard - Fix optional chaining lint warning in ValidationCard - Add unit tests for formatValidationFeedback function * refactor(validation): use AI SDK experimental_useObject hook instead of raw fetch - Change API endpoint from generateObject to streamObject for useObject compatibility - Create useValidateDiagram hook using AI SDK's experimental_useObject for reactive validation - Update useDiagramToolHandlers to accept validation function as parameter - Update chat-panel to use new useValidateDiagram hook - Remove validateRenderedDiagram function from lib/diagram-validator.ts (now in hook) - Export ValidationResultSchema from API route for client-side use * fix(validation): extract schema to shared file for client/server compatibility Move ValidationResultSchema to lib/validation-schema.ts to avoid importing server-side modules (ai-providers) into client-side code. This fixes the Turbopack build error caused by the hook importing from the API route. * fix(validation): use 'Valid' instead of 'Complete' for validation success Change ValidationCard success label from 'Complete' to 'Valid' to avoid conflicting with ToolCallCard's 'Complete' badge in E2E tests. This fixes the diagram-generation E2E test that expects a specific count of 'Complete' badges. * fix(validation): add aria-hidden to icons to prevent duplicate ID warning * fix: improve VLM validation with bug fixes and i18n - Fix race condition in pendingValidationRef (reject previous pending validation) - Fix response format consistency (use streaming for all responses) - Remove dead code (unused lastRequestRef and ValidationRequest interface) - Consolidate duplicate types (re-export from validation-schema.ts) - Add 'success_with_warnings' status for valid diagrams with warnings - Fix tool card auto-collapse (only collapse once, respect user toggle) - Set VLM validation default to disabled - Add i18n support for diagram validation settings (en/zh/ja) - Mark feature as experimental in settings UI * fix: resolve TypeScript errors in electron-standalone - Add forwardRef support to ChatInput component with ChatInputRef type - Copy electron.d.ts to electron-standalone/electron folder - Exclude electron-standalone from root tsconfig type checking * fix: return empty string for valid result with no issues in formatValidationFeedback * feat(i18n): add validation strings for ValidationCard component - Add validation section to en.json, zh.json, ja.json dictionaries - Update ValidationCard to use useDictionary hook - Replace all hardcoded English strings with i18n keys --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-01-20 20:52:04 +09:00
// 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) {
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
case "restructure_diagram":
return "Build Diagram"
case "edit_diagram":
return "Edit Diagram"
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
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 (
<div className="my-3 rounded-xl border border-border/60 bg-muted/30 overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 bg-muted/50">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-md bg-primary/10 flex items-center justify-center">
<Cpu className="w-3.5 h-3.5 text-primary" />
</div>
<span className="text-sm font-medium text-foreground/80">
{getToolDisplayName(toolName)}
</span>
</div>
<div className="flex items-center gap-2">
{state === "input-streaming" && (
<div className="h-4 w-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
)}
{state === "output-available" && (
<>
<span className="text-xs font-medium text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
{dict.tools.complete}
</span>
{isExpanded && (
<button
type="button"
onClick={handleCopy}
className="p-1 rounded hover:bg-muted transition-colors"
title={
copiedToolCallId === callId
? dict.chat.copied
: copyFailedToolCallId === callId
? dict.chat.failedToCopy
: dict.chat.copyResponse
}
>
{isCopied ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<Copy className="w-4 h-4 text-muted-foreground" />
)}
</button>
)}
</>
)}
{state === "output-error" &&
(() => {
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
// 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 =
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
toolName === "display_diagram" &&
!isMxCellXmlComplete(input?.xml)
return isTruncated ? (
<span className="text-xs font-medium text-yellow-600 bg-yellow-50 px-2 py-0.5 rounded-full">
Truncated
</span>
) : (
<span className="text-xs font-medium text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
Error
</span>
)
})()}
{input && Object.keys(input).length > 0 && (
<button
type="button"
onClick={toggleExpanded}
className="p-1 rounded hover:bg-muted transition-colors"
>
{isExpanded ? (
<ChevronUp className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
)}
</button>
)}
</div>
</div>
{input && isExpanded && (
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
{typeof input === "object" && input.xml ? (
state === "input-streaming" ||
state === "input-available" ? (
<pre
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all whitespace-pre-wrap"
style={{
fontFamily:
"var(--font-mono), ui-monospace, monospace",
margin: 0,
padding: 0,
}}
>
{input.xml}
</pre>
) : (
<CodeBlock code={input.xml} language="xml" />
)
) : typeof input === "object" &&
input.operations &&
Array.isArray(input.operations) ? (
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
// 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" ? (
<StructureOperationsDisplay
operations={
input.operations as StructureOperation[]
}
/>
) : (
<OperationsDisplay
operations={
input.operations as DiagramOperation[]
}
/>
)
) : typeof input === "object" &&
Object.keys(input).length > 0 ? (
<CodeBlock
code={JSON.stringify(input, null, 2)}
language="json"
/>
) : null}
</div>
)}
{output &&
state === "output-error" &&
(() => {
const isTruncated =
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
toolName === "display_diagram" &&
!isMxCellXmlComplete(input?.xml)
return (
<div
className={`px-4 py-3 border-t border-border/40 text-sm ${isTruncated ? "text-yellow-600" : "text-red-600"}`}
>
{isTruncated
? "Output truncated due to length limits. Try a simpler request or increase the maxOutputLength."
: output}
</div>
)
})()}
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
{/* 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 && (
<div className="px-4 py-3 border-t border-border/40">
<pre className="text-[11px] font-mono text-muted-foreground bg-muted/40 rounded-md p-2 overflow-auto max-h-64 whitespace-pre-wrap break-all">
{typeof output === "string"
? output.length > 4000
? `${output.slice(0, 4000)}\n…`
: output
: String(output)}
</pre>
</div>
)}
</div>
)
}