mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 12:09:53 +08:00
feat(diagram): named styles and compact cells for shorter model output (#969)
The model defines reused styles once as <mxStyle name="..." value="..."/> and refers to them by name, and writes shapes as one self-closing mxCell with x, y, w, h and edges with source and target. style-classes.ts and compact-cells.ts expand both back into standard draw.io XML after validation and add draw.io's html=1 / whiteSpace=wrap defaults; the diagram shown to the model is folded into the same notation. Prompts, tool descriptions and the MCP drawing guide teach the notation with shared examples. Measured on the five start-screen examples against main: gpt-6-luna 35% fewer output tokens, Claude Opus 5.5 41% fewer. Two five-model review rounds fixed quote-aware cell matching, attribute escaping, edge/vertex inference, root-id and layer handling, and several auto-fix gaps.
This commit is contained in:
+13
-3
@@ -2,6 +2,8 @@ import { type ClassValue, clsx } from "clsx"
|
||||
import * as pako from "pako"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { hasCells } from "@/packages/mcp-server/src/pages.ts"
|
||||
import { readStyleClasses } from "@/packages/mcp-server/src/style-classes.ts"
|
||||
import { repairQuoteBeforeSlash } from "@/packages/mcp-server/src/xml-validation.ts"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -41,7 +43,12 @@ export function isRealDiagram(xml: string | undefined | null): boolean {
|
||||
* @returns true if XML appears complete, false if truncated or empty
|
||||
*/
|
||||
export function isMxCellXmlComplete(xml: string | undefined | null): boolean {
|
||||
const trimmed = xml?.trim() || ""
|
||||
// Named style definitions before the cells are not cells: output cut off
|
||||
// right after them is incomplete. A compact cell whose last quote is
|
||||
// missing is complete; prepareNewDiagram repairs it.
|
||||
const trimmed = repairQuoteBeforeSlash(
|
||||
readStyleClasses(xml || "").xml,
|
||||
).trim()
|
||||
if (!trimmed) return false
|
||||
|
||||
// Find position of last complete mxCell ending (either /> or </mxCell>)
|
||||
@@ -89,7 +96,9 @@ export function extractCompleteMxCells(xml: string | undefined | null): string {
|
||||
// Match self-closing <mxCell ... /> or <mxCell ...>...</mxCell>, in document order.
|
||||
// The lazy [^>]*? tries "/>" first, so a self-closing cell never swallows
|
||||
// the following cells up to the next </mxCell>.
|
||||
const cellPattern = /<mxCell\b[^>]*?(?:\/>|>[\s\S]*?<\/mxCell>)/g
|
||||
// Quoted values may hold ">", so the tag ends at the first ">" outside them
|
||||
const cellPattern =
|
||||
/<mxCell\b(?:[^<>"']|"[^"]*"|'[^']*')*?(?:\/>|>[\s\S]*?<\/mxCell>)/g
|
||||
|
||||
return (xml.match(cellPattern) || []).join("\n")
|
||||
}
|
||||
@@ -152,7 +161,8 @@ export function formatXML(xml: string, indent: string = " "): string {
|
||||
export function convertToLegalXml(xmlString: string): string {
|
||||
// This regex will match either self-closing <mxCell .../> or a block element
|
||||
// <mxCell ...> ... </mxCell>. Unfinished ones are left out because they don't match.
|
||||
const regex = /<mxCell\b[^>]*(?:\/>|>([\s\S]*?)<\/mxCell>)/g
|
||||
const regex =
|
||||
/<mxCell\b(?:[^<>"']|"[^"]*"|'[^']*')*?(?:\/>|>([\s\S]*?)<\/mxCell>)/g
|
||||
let match: RegExpExecArray | null
|
||||
let result = "<root>\n"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user