mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-03 01:50:23 +08:00
feat(diagram-engine): wire up restructure_diagram + stencil catalog
Closes the loop: the model can now build and edit AWS architecture diagrams by declaring structure, and never writes an mxCell again. catalog.ts — 983 AWS icon and 19 group stencils as a name→style map, generated from drawio-ai-kit's catalog (itself generated from jgraph's draw.io shape index). Styles are verbatim, so the official category colours, connection points and aspect=fixed come along for free and nothing is hand-assembled. An invented name is rejected with suggestions instead of rendering as a blank square, which is what draw.io does with an unknown resIcon today. operations.ts — what the model actually sends: add_icon / add_container / move / link / set_dir and so on, applied in order against the tree. Guards the things that break a diagram quietly: duplicate ids (draw.io drops one of the two cells), edges left pointing at a removed node, and moving a container inside itself. index.ts — the entry point. current XML → parse → apply ops → check names → layout → render → new XML. The tree is not stored between calls; it is re-derived from the canvas every time, so a user's manual edits are input to the next layout rather than state to reconcile. Token cost, measured with Claude's tokenizer rather than estimated: - build a VPC diagram: 515 tok as operations vs 3180 as XML (6.2x) - add one icon: 27 tok as an operation vs 3823 re-emitting (142x) - read current state: 216 tok as an outline vs 3180 as XML (14.7x) The 142x is the one that matters day to day: "add a Redis" is one operation, not a rewrite of the whole diagram. Routing in the system prompt sends AWS architecture through this path and leaves flowcharts, BPMN, sequence diagrams, mind maps and Azure/GCP on display_diagram — the layout engine's primitives (nested rows, columns, grids) do not model a sequence diagram's lifelines or a mind map's radial spread, and pretending otherwise would make those worse rather than better. Also: added a NOTICE recording the MIT port and the AWS Architecture Icons terms, and a narrow .gitignore exception so the generated catalog is tracked while the root data/ directory (admin settings, contains secrets) stays ignored. 403 unit tests + 3 new e2e. Verified in the real app: a structural tool call renders with real stencils and container markers; a second call adds one node and keeps everything from the first; an invented name is refused and nothing is drawn. The 13 existing diagram e2e tests still pass.
This commit is contained in:
@@ -5,9 +5,11 @@ import type {
|
||||
ValidationState,
|
||||
ValidationStatus,
|
||||
} from "@/components/chat/ValidationCard"
|
||||
import type { Operation } from "@/lib/diagram-engine"
|
||||
import { restructureDiagram } from "@/lib/diagram-engine"
|
||||
import type { ValidationResult } from "@/lib/diagram-validator"
|
||||
import { formatValidationFeedback } from "@/lib/diagram-validator"
|
||||
import { isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
||||
import { isMxCellXmlComplete, isRealDiagram, wrapWithMxFile } from "@/lib/utils"
|
||||
|
||||
const DEBUG = process.env.NODE_ENV === "development"
|
||||
|
||||
@@ -120,6 +122,8 @@ export function useDiagramToolHandlers({
|
||||
await handleEditDiagram(toolCall, addToolOutput)
|
||||
} else if (toolCall.toolName === "append_diagram") {
|
||||
handleAppendDiagram(toolCall, addToolOutput)
|
||||
} else if (toolCall.toolName === "restructure_diagram") {
|
||||
await handleRestructureDiagram(toolCall, addToolOutput)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,5 +580,71 @@ Continue from EXACTLY where you stopped.`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Structural editing. The model sends operations against the tree; the engine
|
||||
* re-derives that tree from whatever is on the canvas right now — including anything
|
||||
* the user moved or recoloured by hand — applies the operations, recomputes every
|
||||
* coordinate, and returns new XML.
|
||||
*
|
||||
* Nothing about the tree is stored between calls, so there is no second copy of the
|
||||
* state to drift out of sync with the canvas.
|
||||
*/
|
||||
const handleRestructureDiagram = async (
|
||||
toolCall: ToolCall,
|
||||
addToolOutput: AddToolOutputFn,
|
||||
) => {
|
||||
const { operations } = toolCall.input as { operations: Operation[] }
|
||||
|
||||
// Read the live canvas, not the last thing we generated: the user may have
|
||||
// edited it since.
|
||||
let currentXml = ""
|
||||
try {
|
||||
currentXml = await onFetchChart(false)
|
||||
} catch {
|
||||
currentXml = chartXMLRef.current ?? ""
|
||||
}
|
||||
if (!isRealDiagram(currentXml)) currentXml = ""
|
||||
|
||||
const result = restructureDiagram(currentXml, operations)
|
||||
|
||||
if (result.errors.length > 0 || !result.xml) {
|
||||
addToolOutput({
|
||||
tool: "restructure_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: `Could not apply the operations:
|
||||
${result.errors.map((e) => `- ${e}`).join("\n")}
|
||||
|
||||
Structure as it stands:
|
||||
${result.outline}
|
||||
|
||||
Fix the operations and call restructure_diagram again.`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const loadError = onDisplayChart(result.xml)
|
||||
if (loadError) {
|
||||
addToolOutput({
|
||||
tool: "restructure_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: `The diagram was built but draw.io rejected it: ${loadError}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Report the outline rather than the XML: it is what the model needs to name ids
|
||||
// in the next call, at a fraction of the tokens.
|
||||
const notes = result.warnings.length
|
||||
? `\n\nNotes:\n${result.warnings.map((w) => `- ${w}`).join("\n")}`
|
||||
: ""
|
||||
addToolOutput({
|
||||
tool: "restructure_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
output: `Diagram updated.\n\n${result.outline}${notes}`,
|
||||
})
|
||||
}
|
||||
|
||||
return { handleToolCall }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user