refactor: simplify LLM XML format to output bare mxCells only (#254)

* refactor: simplify LLM XML format to output bare mxCells only

- Update wrapWithMxFile() to always add root cells (id=0, id=1) automatically
- LLM now generates only mxCell elements starting from id=2 (no wrapper tags)
- Update system prompts and tool descriptions with new format instructions
- Update cached responses to remove root cells and wrapper tags
- Update truncation detection to check for complete mxCell endings
- Update documentation in xml_guide.md

* fix: address PR review issues for XML format refactor

- Fix critical bug: inconsistent truncation check using old </root> pattern
- Fix stale error message referencing </root> tag
- Add isMxCellXmlComplete() helper for consistent truncation detection
- Improve regex patterns to handle any attribute order in root cells
- Update wrapWithMxFile JSDoc to document root cell removal behavior

* fix: handle non-self-closing root cells in wrapWithMxFile regex
This commit is contained in:
Dayuan Jiang
2025-12-14 14:04:44 +09:00
committed by GitHub
parent 2e24071539
commit 0851b32b67
7 changed files with 143 additions and 144 deletions

View File

@@ -367,36 +367,32 @@ ${userInputText}
tools: { tools: {
// Client-side tool that will be executed on the client // Client-side tool that will be executed on the client
display_diagram: { display_diagram: {
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags. description: `Display a diagram on draw.io. Pass ONLY the mxCell elements - wrapper tags and root cells are added automatically.
VALIDATION RULES (XML will be rejected if violated): VALIDATION RULES (XML will be rejected if violated):
1. All mxCell elements must be DIRECT children of <root> - never nested 1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
2. Every mxCell needs a unique id 2. Do NOT include root cells (id="0" or id="1") - they are added automatically
3. Every mxCell (except id="0") needs a valid parent attribute 3. All mxCell elements must be siblings - never nested
4. Edge source/target must reference existing cell IDs 4. Every mxCell needs a unique id (start from "2")
5. Escape special chars in values: &lt; &gt; &amp; &quot; 5. Every mxCell needs a valid parent attribute (use "1" for top-level)
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/> 6. Escape special chars in values: &lt; &gt; &amp; &quot;
Example with swimlanes and edges (note: all mxCells are siblings): Example (generate ONLY this - no wrapper tags):
<root> <mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/> <mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1"> <mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/> <mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1"> <mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/> <mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2"> <mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/> <mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2"> <mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
</root>
Notes: Notes:
- For AWS diagrams, use **AWS 2025 icons**. - For AWS diagrams, use **AWS 2025 icons**.
@@ -444,9 +440,9 @@ IMPORTANT: Keep edits concise:
WHEN TO USE: Only call this tool after display_diagram was truncated (you'll see an error message about truncation). WHEN TO USE: Only call this tool after display_diagram was truncated (you'll see an error message about truncation).
CRITICAL INSTRUCTIONS: CRITICAL INSTRUCTIONS:
1. Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> - they already exist in the partial 1. Do NOT include any wrapper tags - just continue the mxCell elements
2. Continue from EXACTLY where your previous output stopped 2. Continue from EXACTLY where your previous output stopped
3. End with the closing </root> tag to complete the diagram 3. Complete the remaining mxCell elements
4. If still truncated, call append_diagram again with the next fragment 4. If still truncated, call append_diagram again with the next fragment
Example: If previous output ended with '<mxCell id="x" style="rounded=1', continue with ';" vertex="1">...' and complete the remaining elements.`, Example: If previous output ended with '<mxCell id="x" style="rounded=1', continue with ';" vertex="1">...' and complete the remaining elements.`,

View File

@@ -81,16 +81,15 @@ Contains the actual diagram data.
## Root Cell Container: `<root>` ## Root Cell Container: `<root>`
Contains all the cells in the diagram. Contains all the cells in the diagram. **Note:** When generating diagrams, you only need to provide the mxCell elements - the root container and root cells (id="0", id="1") are added automatically.
**Example:** **Internal structure (auto-generated):**
```xml ```xml
<root> <root>
<mxCell id="0"/> <mxCell id="0"/> <!-- Auto-added -->
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/> <!-- Auto-added -->
<!-- Your mxCell elements go here (start from id="2") -->
<!-- Other cells go here -->
</root> </root>
``` ```
@@ -203,15 +202,15 @@ Draw.io files contain two special cells that are always present:
1. **Root Cell** (id = "0"): The parent of all cells 1. **Root Cell** (id = "0"): The parent of all cells
2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells 2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells
## Tips for Manually Creating Draw.io XML ## Tips for Creating Draw.io XML
1. Start with the basic structure (`mxfile`, `diagram`, `mxGraphModel`, `root`) 1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically
2. Always include the two special cells (id = "0" and id = "1") 2. Start IDs from "2" (id="0" and id="1" are reserved for root cells)
3. Assign unique and sequential IDs to all cells 3. Assign unique and sequential IDs to all cells
4. Define parent relationships correctly 4. Define parent relationships correctly (use parent="1" for top-level shapes)
5. Use `mxGeometry` elements to position shapes 5. Use `mxGeometry` elements to position shapes
6. For connectors, specify `source` and `target` attributes 6. For connectors, specify `source` and `target` attributes
7. **CRITICAL: All mxCell elements must be DIRECT children of `<root>`. NEVER nest mxCell inside another mxCell.** 7. **CRITICAL: All mxCell elements must be siblings. NEVER nest mxCell inside another mxCell.**
## Common Patterns ## Common Patterns

View File

@@ -29,7 +29,12 @@ import {
ReasoningTrigger, ReasoningTrigger,
} from "@/components/ai-elements/reasoning" } from "@/components/ai-elements/reasoning"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { convertToLegalXml, replaceNodes, validateAndFixXml } from "@/lib/utils" import {
convertToLegalXml,
isMxCellXmlComplete,
replaceNodes,
validateAndFixXml,
} from "@/lib/utils"
import ExamplePanel from "./chat-example-panel" import ExamplePanel from "./chat-example-panel"
import { CodeBlock } from "./code-block" import { CodeBlock } from "./code-block"
@@ -457,8 +462,7 @@ export function ChatMessageDisplay({
const isTruncated = const isTruncated =
(toolName === "display_diagram" || (toolName === "display_diagram" ||
toolName === "append_diagram") && toolName === "append_diagram") &&
(!input?.xml || !isMxCellXmlComplete(input?.xml)
!input.xml.includes("</root>"))
return isTruncated ? ( return isTruncated ? (
<span className="text-xs font-medium text-yellow-600 bg-yellow-50 px-2 py-0.5 rounded-full"> <span className="text-xs font-medium text-yellow-600 bg-yellow-50 px-2 py-0.5 rounded-full">
Truncated Truncated
@@ -507,7 +511,7 @@ export function ChatMessageDisplay({
const isTruncated = const isTruncated =
(toolName === "display_diagram" || (toolName === "display_diagram" ||
toolName === "append_diagram") && toolName === "append_diagram") &&
(!input?.xml || !input.xml.includes("</root>")) !isMxCellXmlComplete(input?.xml)
return ( return (
<div <div
className={`px-4 py-3 border-t border-border/40 text-sm ${isTruncated ? "text-yellow-600" : "text-red-600"}`} className={`px-4 py-3 border-t border-border/40 text-sm ${isTruncated ? "text-yellow-600" : "text-red-600"}`}

View File

@@ -26,7 +26,7 @@ import { findCachedResponse } from "@/lib/cached-responses"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
import { useQuotaManager } from "@/lib/use-quota-manager" import { useQuotaManager } from "@/lib/use-quota-manager"
import { formatXML, wrapWithMxFile } from "@/lib/utils" import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
import { ChatMessageDisplay } from "./chat-message-display" import { ChatMessageDisplay } from "./chat-message-display"
// localStorage keys for persistence // localStorage keys for persistence
@@ -222,9 +222,8 @@ export default function ChatPanel({
if (toolCall.toolName === "display_diagram") { if (toolCall.toolName === "display_diagram") {
const { xml } = toolCall.input as { xml: string } const { xml } = toolCall.input as { xml: string }
// Check if XML is truncated (missing </root> indicates incomplete output) // Check if XML is truncated (incomplete mxCell indicates truncated output)
const isTruncated = const isTruncated = !isMxCellXmlComplete(xml)
!xml.includes("</root>") && !xml.trim().endsWith("/>")
if (isTruncated) { if (isTruncated) {
// Store the partial XML for continuation via append_diagram // Store the partial XML for continuation via append_diagram
@@ -244,9 +243,9 @@ ${partialEnding}
\`\`\` \`\`\`
NEXT STEP: Call append_diagram with the continuation XML. NEXT STEP: Call append_diagram with the continuation XML.
- Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> (they already exist) - Do NOT include wrapper tags or root cells (id="0", id="1")
- Start from EXACTLY where you stopped - Start from EXACTLY where you stopped
- End with the closing </root> tag to complete the diagram`, - Complete all remaining mxCell elements`,
}) })
return return
} }
@@ -376,17 +375,21 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
const { xml } = toolCall.input as { xml: string } const { xml } = toolCall.input as { xml: string }
// Detect if LLM incorrectly started fresh instead of continuing // Detect if LLM incorrectly started fresh instead of continuing
// LLM should only output bare mxCells now, so wrapper tags indicate error
const trimmed = xml.trim()
const isFreshStart = const isFreshStart =
xml.trim().startsWith("<mxGraphModel") || trimmed.startsWith("<mxGraphModel") ||
xml.trim().startsWith("<root") || trimmed.startsWith("<root") ||
xml.trim().startsWith('<mxCell id="0"') trimmed.startsWith("<mxfile") ||
trimmed.startsWith('<mxCell id="0"') ||
trimmed.startsWith('<mxCell id="1"')
if (isFreshStart) { if (isFreshStart) {
addToolOutput({ addToolOutput({
tool: "append_diagram", tool: "append_diagram",
toolCallId: toolCall.toolCallId, toolCallId: toolCall.toolCallId,
state: "output-error", state: "output-error",
errorText: `ERROR: You started fresh with wrapper tags. Do NOT include <mxGraphModel>, <root>, or <mxCell id="0">. errorText: `ERROR: You started fresh with wrapper tags. Do NOT include wrapper tags or root cells (id="0", id="1").
Continue from EXACTLY where the partial ended: Continue from EXACTLY where the partial ended:
\`\`\` \`\`\`
@@ -401,8 +404,8 @@ Start your continuation with the NEXT character after where it stopped.`,
// Append to accumulated XML // Append to accumulated XML
partialXmlRef.current += xml partialXmlRef.current += xml
// Check if XML is now complete // Check if XML is now complete (last mxCell is complete)
const isComplete = partialXmlRef.current.includes("</root>") const isComplete = isMxCellXmlComplete(partialXmlRef.current)
if (isComplete) { if (isComplete) {
// Wrap and display the complete diagram // Wrap and display the complete diagram
@@ -439,7 +442,7 @@ Please use display_diagram with corrected XML.`,
tool: "append_diagram", tool: "append_diagram",
toolCallId: toolCall.toolCallId, toolCallId: toolCall.toolCallId,
state: "output-error", state: "output-error",
errorText: `XML still incomplete (missing </root>). Call append_diagram again to continue. errorText: `XML still incomplete (mxCell not closed). Call append_diagram again to continue.
Current ending: Current ending:
\`\`\` \`\`\`

View File

@@ -9,12 +9,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
promptText: promptText:
"Give me a **animated connector** diagram of transformer's architecture", "Give me a **animated connector** diagram of transformer's architecture",
hasImage: false, hasImage: false,
xml: `<root> xml: `<mxCell id="title" value="Transformer Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="title" value="Transformer Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="20" width="250" height="30" as="geometry"/> <mxGeometry x="300" y="20" width="250" height="30" as="geometry"/>
</mxCell> </mxCell>
@@ -254,18 +249,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="output_label" value="Outputs&#xa;(shifted right)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;" vertex="1" parent="1"> <mxCell id="output_label" value="Outputs&#xa;(shifted right)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="660" y="530" width="100" height="30" as="geometry"/> <mxGeometry x="660" y="530" width="100" height="30" as="geometry"/>
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Replicate this in aws style", promptText: "Replicate this in aws style",
hasImage: true, hasImage: true,
xml: `<root> xml: `<mxCell id="2" value="AWS" style="sketch=0;outlineConnect=0;gradientColor=none;html=1;whiteSpace=wrap;fontSize=12;fontStyle=0;container=1;pointerEvents=0;collapsible=0;recursiveResize=0;shape=mxgraph.aws4.group;grIcon=mxgraph.aws4.group_aws_cloud;strokeColor=#232F3E;fillColor=none;verticalAlign=top;align=left;spacingLeft=30;fontColor=#232F3E;dashed=0;rounded=1;arcSize=5;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="AWS" style="sketch=0;outlineConnect=0;gradientColor=none;html=1;whiteSpace=wrap;fontSize=12;fontStyle=0;container=1;pointerEvents=0;collapsible=0;recursiveResize=0;shape=mxgraph.aws4.group;grIcon=mxgraph.aws4.group_aws_cloud;strokeColor=#232F3E;fillColor=none;verticalAlign=top;align=left;spacingLeft=30;fontColor=#232F3E;dashed=0;rounded=1;arcSize=5;" vertex="1" parent="1">
<mxGeometry x="340" y="40" width="880" height="520" as="geometry"/> <mxGeometry x="340" y="40" width="880" height="520" as="geometry"/>
</mxCell> </mxCell>
@@ -324,18 +313,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxPoint x="700" y="350" as="sourcePoint"/> <mxPoint x="700" y="350" as="sourcePoint"/>
<mxPoint x="750" y="300" as="targetPoint"/> <mxPoint x="750" y="300" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Replicate this flowchart.", promptText: "Replicate this flowchart.",
hasImage: true, hasImage: true,
xml: `<root> xml: `<mxCell id="2" value="Lamp doesn't work" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffcccc;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Lamp doesn't work" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffcccc;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="140" y="40" width="180" height="60" as="geometry"/> <mxGeometry x="140" y="40" width="180" height="60" as="geometry"/>
</mxCell> </mxCell>
@@ -391,16 +374,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="12" value="Repair lamp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#99ff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1"> <mxCell id="12" value="Repair lamp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#99ff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="130" y="650" width="200" height="60" as="geometry"/> <mxGeometry x="130" y="650" width="200" height="60" as="geometry"/>
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Summarize this paper as a diagram", promptText: "Summarize this paper as a diagram",
hasImage: true, hasImage: true,
xml: ` <root> xml: `<mxCell id="title_bg" parent="1"
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title_bg" parent="1"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1a237e;strokeColor=none;arcSize=8;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1a237e;strokeColor=none;arcSize=8;"
value="" vertex="1"> value="" vertex="1">
<mxGeometry height="80" width="720" x="40" y="20" as="geometry" /> <mxGeometry height="80" width="720" x="40" y="20" as="geometry" />
@@ -751,18 +730,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
value="Foundational technique for modern LLM reasoning - inspired many follow-up works including Self-Consistency, Tree-of-Thought, etc." value="Foundational technique for modern LLM reasoning - inspired many follow-up works including Self-Consistency, Tree-of-Thought, etc."
vertex="1"> vertex="1">
<mxGeometry height="55" width="230" x="530" y="600" as="geometry" /> <mxGeometry height="55" width="230" x="530" y="600" as="geometry" />
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Draw a cat for me", promptText: "Draw a cat for me",
hasImage: false, hasImage: false,
xml: `<root> xml: `<mxCell id="2" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="300" y="150" width="120" height="120" as="geometry"/> <mxGeometry x="300" y="150" width="120" height="120" as="geometry"/>
</mxCell> </mxCell>
@@ -902,9 +875,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxPoint x="235" y="290"/> <mxPoint x="235" y="290"/>
</Array> </Array>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>`,
</root>`,
}, },
] ]

View File

@@ -104,22 +104,21 @@ When using edit_diagram tool:
## Draw.io XML Structure Reference ## Draw.io XML Structure Reference
Basic structure: **IMPORTANT:** You only generate the mxCell elements. The wrapper structure and root cells (id="0", id="1") are added automatically.
Example - generate ONLY this:
\`\`\`xml \`\`\`xml
<mxGraphModel> <mxCell id="2" value="Label" style="rounded=1;" vertex="1" parent="1">
<root> <mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
<mxCell id="0"/> </mxCell>
<mxCell id="1" parent="0"/>
</root>
</mxGraphModel>
\`\`\` \`\`\`
Note: All other mxCell elements go as siblings after id="1".
CRITICAL RULES: CRITICAL RULES:
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/> 1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell 2. Do NOT include root cells (id="0" or id="1") - they are added automatically
3. Use unique sequential IDs for all cells (start from "2" for user content) 3. ALL mxCell elements must be siblings - NEVER nest mxCell inside another mxCell
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements 4. Use unique sequential IDs starting from "2"
5. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
Shape (vertex) example: Shape (vertex) example:
\`\`\`xml \`\`\`xml
@@ -151,34 +150,30 @@ const EXTENDED_ADDITIONS = `
### display_diagram Details ### display_diagram Details
**VALIDATION RULES** (XML will be rejected if violated): **VALIDATION RULES** (XML will be rejected if violated):
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements 1. Generate ONLY mxCell elements - wrapper tags and root cells are added automatically
2. Every mxCell needs a unique id attribute 2. All mxCell elements must be siblings - never nested inside other mxCell elements
3. Every mxCell (except id="0") needs a valid parent attribute referencing an existing cell 3. Every mxCell needs a unique id attribute (start from "2")
4. Edge source/target attributes must reference existing cell IDs 4. Every mxCell needs a valid parent attribute (use "1" for top-level, or container-id for grouped)
5. Escape special characters in values: &lt; for <, &gt; for >, &amp; for &, &quot; for " 5. Edge source/target attributes must reference existing cell IDs
6. Always start with the two root cells: <mxCell id="0"/><mxCell id="1" parent="0"/> 6. Escape special characters in values: &lt; for <, &gt; for >, &amp; for &, &quot; for "
**Example with swimlanes and edges** (note: all mxCells are siblings under <root>): **Example with swimlanes and edges** (generate ONLY this - no wrapper tags):
\`\`\`xml \`\`\`xml
<root> <mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/> <mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1"> <mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/> <mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1"> <mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/> <mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2"> <mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/> <mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2"> <mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
</root>
\`\`\` \`\`\`
### append_diagram Details ### append_diagram Details
@@ -186,9 +181,9 @@ const EXTENDED_ADDITIONS = `
**WHEN TO USE:** Only call this tool when display_diagram output was truncated (you'll see an error message about truncation). **WHEN TO USE:** Only call this tool when display_diagram output was truncated (you'll see an error message about truncation).
**CRITICAL RULES:** **CRITICAL RULES:**
1. Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> - they already exist in the partial 1. Do NOT include any wrapper tags - just continue the mxCell elements
2. Continue from EXACTLY where your previous output stopped 2. Continue from EXACTLY where your previous output stopped
3. End with the closing </root> tag to complete the diagram 3. Complete the remaining mxCell elements
4. If still truncated, call append_diagram again with the next fragment 4. If still truncated, call append_diagram again with the next fragment
**Example:** If previous output ended with \`<mxCell id="x" style="rounded=1\`, continue with \`;" vertex="1">...\` and complete the remaining elements. **Example:** If previous output ended with \`<mxCell id="x" style="rounded=1\`, continue with \`;" vertex="1">...\` and complete the remaining elements.

View File

@@ -29,6 +29,22 @@ const STRUCTURAL_ATTRS = [
/** Valid XML entity names */ /** Valid XML entity names */
const VALID_ENTITIES = new Set(["lt", "gt", "amp", "quot", "apos"]) const VALID_ENTITIES = new Set(["lt", "gt", "amp", "quot", "apos"])
// ============================================================================
// mxCell XML Helpers
// ============================================================================
/**
* Check if mxCell XML output is complete (not truncated).
* Complete XML ends with a self-closing tag (/>) or closing mxCell tag.
* @param xml - The XML string to check (can be undefined/null)
* @returns true if XML appears complete, false if truncated or empty
*/
export function isMxCellXmlComplete(xml: string | undefined | null): boolean {
const trimmed = xml?.trim() || ""
if (!trimmed) return false
return trimmed.endsWith("/>") || trimmed.endsWith("</mxCell>")
}
// ============================================================================ // ============================================================================
// XML Parsing Helpers // XML Parsing Helpers
// ============================================================================ // ============================================================================
@@ -197,13 +213,17 @@ export function convertToLegalXml(xmlString: string): string {
/** /**
* Wrap XML content with the full mxfile structure required by draw.io. * Wrap XML content with the full mxfile structure required by draw.io.
* Handles cases where XML is just <root>, <mxGraphModel>, or already has <mxfile>. * Always adds root cells (id="0" and id="1") automatically.
* @param xml - The XML string (may be partial or complete) * If input already contains root cells, they are removed to avoid duplication.
* @returns Full mxfile-wrapped XML string * LLM should only generate mxCell elements starting from id="2".
* @param xml - The XML string (bare mxCells, <root>, <mxGraphModel>, or full <mxfile>)
* @returns Full mxfile-wrapped XML string with root cells included
*/ */
export function wrapWithMxFile(xml: string): string { export function wrapWithMxFile(xml: string): string {
if (!xml) { const ROOT_CELLS = '<mxCell id="0"/><mxCell id="1" parent="0"/>'
return `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
if (!xml || !xml.trim()) {
return `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root>${ROOT_CELLS}</root></mxGraphModel></diagram></mxfile>`
} }
// Already has full structure // Already has full structure
@@ -216,9 +236,20 @@ export function wrapWithMxFile(xml: string): string {
return `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>` return `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`
} }
// Just <root> content - extract inner content and wrap fully // Has <root> wrapper - extract inner content
const rootContent = xml.replace(/<\/?root>/g, "").trim() let content = xml
return `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root>${rootContent}</root></mxGraphModel></diagram></mxfile>` if (xml.includes("<root>")) {
content = xml.replace(/<\/?root>/g, "").trim()
}
// Remove any existing root cells from content (LLM shouldn't include them, but handle it gracefully)
// Use flexible patterns that match both self-closing (/>) and non-self-closing (></mxCell>) formats
content = content
.replace(/<mxCell[^>]*\bid=["']0["'][^>]*(?:\/>|><\/mxCell>)/g, "")
.replace(/<mxCell[^>]*\bid=["']1["'][^>]*(?:\/>|><\/mxCell>)/g, "")
.trim()
return `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root>${ROOT_CELLS}${content}</root></mxGraphModel></diagram></mxfile>`
} }
/** /**