mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
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.
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/**
|
|
* The engine's entry point: one call takes the current canvas XML plus a list of
|
|
* structural operations and returns new canvas XML.
|
|
*
|
|
* current XML → parse → apply operations → check names → layout → render → new XML
|
|
*
|
|
* The tree is not stored anywhere between calls. It is re-derived from the canvas every
|
|
* time, so a user's manual edits — moving a shape into a different frame, recolouring a
|
|
* box, adding an annotation — are simply part of the input to the next layout. There is
|
|
* no second copy of the state, and therefore nothing to reconcile.
|
|
*/
|
|
|
|
import { checkNames, resolveStyle } from "./catalog"
|
|
import {
|
|
applyOperations,
|
|
collectNames,
|
|
type Operation,
|
|
outline,
|
|
} from "./operations"
|
|
import { parseDiagram } from "./parse"
|
|
import { renderDiagram } from "./render"
|
|
import type { DiagramTree } from "./types"
|
|
|
|
export interface RestructureResult {
|
|
/** New canvas XML, or null when the request could not be carried out. */
|
|
xml: string | null
|
|
/** Compact outline of the resulting structure, for the model to read back. */
|
|
outline: string
|
|
/** Operations that could not be applied, and invented stencil names. */
|
|
errors: string[]
|
|
/** Non-fatal notes: pages skipped, structure that could not be read cleanly. */
|
|
warnings: string[]
|
|
}
|
|
|
|
export interface RestructureOptions {
|
|
/** Which page of a multi-page document to work on. */
|
|
pageIndex?: number
|
|
/** Diagram-wide icon glyph size. */
|
|
iconSize?: number
|
|
}
|
|
|
|
/**
|
|
* Apply structural operations to whatever is on the canvas.
|
|
*
|
|
* `currentXml` may be empty — that is how a diagram gets built from scratch.
|
|
*
|
|
* An invented stencil name is a hard error, not a silent fallback: draw.io renders an
|
|
* unknown `resIcon` as a blank square, so a diagram that "worked" would be quietly
|
|
* missing icons. The error carries suggestions from the catalog so the model can fix it
|
|
* in one more turn.
|
|
*/
|
|
export function restructureDiagram(
|
|
currentXml: string,
|
|
ops: Operation[],
|
|
opts: RestructureOptions = {},
|
|
): RestructureResult {
|
|
const warnings: string[] = []
|
|
|
|
let tree: DiagramTree
|
|
if (currentXml.trim()) {
|
|
const parsed = parseDiagram(currentXml, opts.pageIndex ?? 0)
|
|
tree = parsed.tree
|
|
warnings.push(...parsed.warnings)
|
|
} else {
|
|
tree = { roots: [], links: [], foreign: [] }
|
|
}
|
|
|
|
const applied = applyOperations(tree, ops)
|
|
const errors = [...applied.errors]
|
|
|
|
// Catch invented names before rendering, so the model gets a correctable error
|
|
// instead of a diagram with blank squares in it.
|
|
for (const bad of checkNames(collectNames(applied.tree))) {
|
|
const hint = bad.suggestions.length
|
|
? ` Did you mean: ${bad.suggestions.join(", ")}?`
|
|
: ""
|
|
errors.push(
|
|
`"${bad.name}" (node ${bad.id}) is not in the stencil catalog.${hint}`,
|
|
)
|
|
}
|
|
|
|
if (errors.length > 0)
|
|
return { xml: null, outline: outline(applied.tree), errors, warnings }
|
|
|
|
const rendered = renderDiagram(applied.tree, {
|
|
resolveStyle,
|
|
iconSize: opts.iconSize,
|
|
})
|
|
if (rendered.danglingLinks.length)
|
|
warnings.push(
|
|
`Dropped edge(s) pointing at missing nodes: ${rendered.danglingLinks.join(", ")}.`,
|
|
)
|
|
|
|
return {
|
|
xml: rendered.xml,
|
|
outline: outline(applied.tree),
|
|
errors: [],
|
|
warnings,
|
|
}
|
|
}
|
|
|
|
/** Read the current canvas structure without changing it. */
|
|
export function describeDiagram(
|
|
currentXml: string,
|
|
pageIndex = 0,
|
|
): { outline: string; warnings: string[]; needsAdoption: boolean } {
|
|
if (!currentXml.trim())
|
|
return { outline: "(empty canvas)", warnings: [], needsAdoption: false }
|
|
const { tree, warnings, needsAdoption } = parseDiagram(
|
|
currentXml,
|
|
pageIndex,
|
|
)
|
|
return { outline: outline(tree), warnings, needsAdoption }
|
|
}
|
|
|
|
export { CATALOG_SIZE, lookupStencil, searchStencils } from "./catalog"
|
|
export { type Operation, OperationSchema } from "./operations"
|
|
export { parseDiagram } from "./parse"
|
|
export { renderDiagram } from "./render"
|
|
export type { DiagramNode, DiagramTree } from "./types"
|