mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
4 Commits
v0.4.1
...
fix/trunca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fbd3fe842 | ||
|
|
9068f608bf | ||
|
|
43139a5ef0 | ||
|
|
62e07f5f9c |
@@ -207,7 +207,7 @@ All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
|||||||
|
|
||||||
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
|
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
|
||||||
|
|
||||||
Note that `claude` series has trained on draw.io diagrams with cloud architecture logos like AWS, Azure, GCP. So if you want to create cloud architecture diagrams, this is the best choice.
|
Note that `claude` series has trained on draw.io diagrams with cloud architecture logos like AWS, Azue, GCP. So if you want to create cloud architecture diagrams, this is the best choice.
|
||||||
|
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|||||||
@@ -202,9 +202,6 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
modelId: req.headers.get("x-ai-model"),
|
modelId: req.headers.get("x-ai-model"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read minimal style preference from header
|
|
||||||
const minimalStyle = req.headers.get("x-minimal-style") === "true"
|
|
||||||
|
|
||||||
// Get AI model with optional client overrides
|
// Get AI model with optional client overrides
|
||||||
const { model, providerOptions, headers, modelId } =
|
const { model, providerOptions, headers, modelId } =
|
||||||
getAIModel(clientOverrides)
|
getAIModel(clientOverrides)
|
||||||
@@ -216,7 +213,7 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
||||||
const systemMessage = getSystemPrompt(modelId, minimalStyle)
|
const systemMessage = getSystemPrompt(modelId)
|
||||||
|
|
||||||
// Extract file parts (images) from the last message
|
// Extract file parts (images) from the last message
|
||||||
const fileParts =
|
const fileParts =
|
||||||
@@ -370,32 +367,36 @@ ${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 ONLY the mxCell elements - wrapper tags and root cells are added automatically.
|
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
|
||||||
|
|
||||||
VALIDATION RULES (XML will be rejected if violated):
|
VALIDATION RULES (XML will be rejected if violated):
|
||||||
1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
|
1. All mxCell elements must be DIRECT children of <root> - never nested
|
||||||
2. Do NOT include root cells (id="0" or id="1") - they are added automatically
|
2. Every mxCell needs a unique id
|
||||||
3. All mxCell elements must be siblings - never nested
|
3. Every mxCell (except id="0") needs a valid parent attribute
|
||||||
4. Every mxCell needs a unique id (start from "2")
|
4. Edge source/target must reference existing cell IDs
|
||||||
5. Every mxCell needs a valid parent attribute (use "1" for top-level)
|
5. Escape special chars in values: < > & "
|
||||||
6. Escape special chars in values: < > & "
|
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
Example (generate ONLY this - no wrapper tags):
|
Example with swimlanes and edges (note: all mxCells are siblings):
|
||||||
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
|
<root>
|
||||||
|
<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**.
|
||||||
@@ -443,9 +444,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 include any wrapper tags - just continue the mxCell elements
|
1. Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> - they already exist in the partial
|
||||||
2. Continue from EXACTLY where your previous output stopped
|
2. Continue from EXACTLY where your previous output stopped
|
||||||
3. Complete the remaining mxCell elements
|
3. End with the closing </root> tag to complete the diagram
|
||||||
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.`,
|
||||||
|
|||||||
@@ -81,15 +81,16 @@ Contains the actual diagram data.
|
|||||||
|
|
||||||
## Root Cell Container: `<root>`
|
## Root Cell Container: `<root>`
|
||||||
|
|
||||||
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.
|
Contains all the cells in the diagram.
|
||||||
|
|
||||||
**Internal structure (auto-generated):**
|
**Example:**
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<root>
|
<root>
|
||||||
<mxCell id="0"/> <!-- Auto-added -->
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/> <!-- Auto-added -->
|
<mxCell id="1" parent="0"/>
|
||||||
<!-- Your mxCell elements go here (start from id="2") -->
|
|
||||||
|
<!-- Other cells go here -->
|
||||||
</root>
|
</root>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -202,15 +203,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 Creating Draw.io XML
|
## Tips for Manually Creating Draw.io XML
|
||||||
|
|
||||||
1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically
|
1. Start with the basic structure (`mxfile`, `diagram`, `mxGraphModel`, `root`)
|
||||||
2. Start IDs from "2" (id="0" and id="1" are reserved for root cells)
|
2. Always include the two special cells (id = "0" and id = "1")
|
||||||
3. Assign unique and sequential IDs to all cells
|
3. Assign unique and sequential IDs to all cells
|
||||||
4. Define parent relationships correctly (use parent="1" for top-level shapes)
|
4. Define parent relationships correctly
|
||||||
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 siblings. NEVER nest mxCell inside another mxCell.**
|
7. **CRITICAL: All mxCell elements must be DIRECT children of `<root>`. NEVER nest mxCell inside another mxCell.**
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,7 @@ import { HistoryDialog } from "@/components/history-dialog"
|
|||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SaveDialog } from "@/components/save-dialog"
|
import { SaveDialog } from "@/components/save-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Switch } from "@/components/ui/switch"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip"
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
import { FilePreviewList } from "./file-preview-list"
|
import { FilePreviewList } from "./file-preview-list"
|
||||||
@@ -135,8 +129,6 @@ interface ChatInputProps {
|
|||||||
onToggleHistory?: (show: boolean) => void
|
onToggleHistory?: (show: boolean) => void
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
error?: Error | null
|
error?: Error | null
|
||||||
minimalStyle?: boolean
|
|
||||||
onMinimalStyleChange?: (value: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -152,8 +144,6 @@ export function ChatInput({
|
|||||||
onToggleHistory = () => {},
|
onToggleHistory = () => {},
|
||||||
sessionId,
|
sessionId,
|
||||||
error = null,
|
error = null,
|
||||||
minimalStyle = false,
|
|
||||||
onMinimalStyleChange = () => {},
|
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
@@ -353,32 +343,6 @@ export function ChatInput({
|
|||||||
showHistory={showHistory}
|
showHistory={showHistory}
|
||||||
onToggleHistory={onToggleHistory}
|
onToggleHistory={onToggleHistory}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<Switch
|
|
||||||
id="minimal-style"
|
|
||||||
checked={minimalStyle}
|
|
||||||
onCheckedChange={onMinimalStyleChange}
|
|
||||||
className="scale-75"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="minimal-style"
|
|
||||||
className={`text-xs cursor-pointer select-none ${
|
|
||||||
minimalStyle
|
|
||||||
? "text-primary font-medium"
|
|
||||||
: "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{minimalStyle ? "Minimal" : "Styled"}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">
|
|
||||||
Use minimal for faster generation (no colors)
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right actions */}
|
{/* Right actions */}
|
||||||
|
|||||||
@@ -29,12 +29,7 @@ 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 {
|
import { convertToLegalXml, replaceNodes, validateAndFixXml } from "@/lib/utils"
|
||||||
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"
|
||||||
|
|
||||||
@@ -193,14 +188,6 @@ export function ChatMessageDisplay({
|
|||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const previousXML = useRef<string>("")
|
const previousXML = useRef<string>("")
|
||||||
const processedToolCalls = processedToolCallsRef
|
const processedToolCalls = processedToolCallsRef
|
||||||
// Track the last processed XML per toolCallId to skip redundant processing during streaming
|
|
||||||
const lastProcessedXmlRef = useRef<Map<string, string>>(new Map())
|
|
||||||
// Debounce streaming diagram updates - store pending XML and timeout
|
|
||||||
const pendingXmlRef = useRef<string | null>(null)
|
|
||||||
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
const STREAMING_DEBOUNCE_MS = 150 // Only update diagram every 150ms during streaming
|
|
||||||
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
@@ -292,31 +279,24 @@ export function ChatMessageDisplay({
|
|||||||
|
|
||||||
const handleDisplayChart = useCallback(
|
const handleDisplayChart = useCallback(
|
||||||
(xml: string, showToast = false) => {
|
(xml: string, showToast = false) => {
|
||||||
console.time("perf:handleDisplayChart")
|
|
||||||
const currentXml = xml || ""
|
const currentXml = xml || ""
|
||||||
const convertedXml = convertToLegalXml(currentXml)
|
const convertedXml = convertToLegalXml(currentXml)
|
||||||
if (convertedXml !== previousXML.current) {
|
if (convertedXml !== previousXML.current) {
|
||||||
// Parse and validate XML BEFORE calling replaceNodes
|
// Parse and validate XML BEFORE calling replaceNodes
|
||||||
console.time("perf:DOMParser")
|
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const testDoc = parser.parseFromString(convertedXml, "text/xml")
|
const testDoc = parser.parseFromString(convertedXml, "text/xml")
|
||||||
console.timeEnd("perf:DOMParser")
|
|
||||||
const parseError = testDoc.querySelector("parsererror")
|
const parseError = testDoc.querySelector("parsererror")
|
||||||
|
|
||||||
if (parseError) {
|
if (parseError) {
|
||||||
// Use console.warn instead of console.error to avoid triggering
|
|
||||||
// Next.js dev mode error overlay for expected streaming states
|
|
||||||
// (partial XML during streaming is normal and will be fixed by subsequent updates)
|
|
||||||
if (showToast) {
|
|
||||||
// Only log as error and show toast if this is the final XML
|
|
||||||
console.error(
|
console.error(
|
||||||
"[ChatMessageDisplay] Malformed XML detected in final output",
|
"[ChatMessageDisplay] Malformed XML detected - skipping update",
|
||||||
)
|
)
|
||||||
|
// Only show toast if this is the final XML (not during streaming)
|
||||||
|
if (showToast) {
|
||||||
toast.error(
|
toast.error(
|
||||||
"AI generated invalid diagram XML. Please try regenerating.",
|
"AI generated invalid diagram XML. Please try regenerating.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
return // Skip this update
|
return // Skip this update
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,14 +306,10 @@ export function ChatMessageDisplay({
|
|||||||
const baseXML =
|
const baseXML =
|
||||||
chartXML ||
|
chartXML ||
|
||||||
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
||||||
console.time("perf:replaceNodes")
|
|
||||||
const replacedXML = replaceNodes(baseXML, convertedXml)
|
const replacedXML = replaceNodes(baseXML, convertedXml)
|
||||||
console.timeEnd("perf:replaceNodes")
|
|
||||||
|
|
||||||
// Validate and auto-fix the XML
|
// Validate and auto-fix the XML
|
||||||
console.time("perf:validateAndFixXml")
|
|
||||||
const validation = validateAndFixXml(replacedXML)
|
const validation = validateAndFixXml(replacedXML)
|
||||||
console.timeEnd("perf:validateAndFixXml")
|
|
||||||
if (validation.valid) {
|
if (validation.valid) {
|
||||||
previousXML.current = convertedXml
|
previousXML.current = convertedXml
|
||||||
// Use fixed XML if available, otherwise use original
|
// Use fixed XML if available, otherwise use original
|
||||||
@@ -370,9 +346,6 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
} else {
|
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[chartXML, onDisplayChart],
|
[chartXML, onDisplayChart],
|
||||||
@@ -391,17 +364,7 @@ export function ChatMessageDisplay({
|
|||||||
}, [editingMessageId])
|
}, [editingMessageId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.time("perf:message-display-useEffect")
|
messages.forEach((message) => {
|
||||||
let processedCount = 0
|
|
||||||
let skippedCount = 0
|
|
||||||
let debouncedCount = 0
|
|
||||||
|
|
||||||
// Only process the last message for streaming performance
|
|
||||||
// Previous messages are already processed and won't change
|
|
||||||
const messagesToProcess =
|
|
||||||
messages.length > 0 ? [messages[messages.length - 1]] : []
|
|
||||||
|
|
||||||
messagesToProcess.forEach((message) => {
|
|
||||||
if (message.parts) {
|
if (message.parts) {
|
||||||
message.parts.forEach((part) => {
|
message.parts.forEach((part) => {
|
||||||
if (part.type?.startsWith("tool-")) {
|
if (part.type?.startsWith("tool-")) {
|
||||||
@@ -420,82 +383,25 @@ export function ChatMessageDisplay({
|
|||||||
input?.xml
|
input?.xml
|
||||||
) {
|
) {
|
||||||
const xml = input.xml as string
|
const xml = input.xml as string
|
||||||
|
|
||||||
// Skip if XML hasn't changed since last processing
|
|
||||||
const lastXml =
|
|
||||||
lastProcessedXmlRef.current.get(toolCallId)
|
|
||||||
if (lastXml === xml) {
|
|
||||||
skippedCount++
|
|
||||||
return // Skip redundant processing
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
state === "input-streaming" ||
|
state === "input-streaming" ||
|
||||||
state === "input-available"
|
state === "input-available"
|
||||||
) {
|
) {
|
||||||
// Debounce streaming updates - queue the XML and process after delay
|
// During streaming, don't show toast (XML may be incomplete)
|
||||||
pendingXmlRef.current = xml
|
handleDisplayChart(xml, false)
|
||||||
|
|
||||||
if (!debounceTimeoutRef.current) {
|
|
||||||
// No pending timeout - set one up
|
|
||||||
debounceTimeoutRef.current = setTimeout(
|
|
||||||
() => {
|
|
||||||
const pendingXml =
|
|
||||||
pendingXmlRef.current
|
|
||||||
debounceTimeoutRef.current = null
|
|
||||||
pendingXmlRef.current = null
|
|
||||||
if (pendingXml) {
|
|
||||||
console.log(
|
|
||||||
"perf:debounced-handleDisplayChart executing",
|
|
||||||
)
|
|
||||||
handleDisplayChart(
|
|
||||||
pendingXml,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
lastProcessedXmlRef.current.set(
|
|
||||||
toolCallId,
|
|
||||||
pendingXml,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
STREAMING_DEBOUNCE_MS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
debouncedCount++
|
|
||||||
} else if (
|
} else if (
|
||||||
state === "output-available" &&
|
state === "output-available" &&
|
||||||
!processedToolCalls.current.has(toolCallId)
|
!processedToolCalls.current.has(toolCallId)
|
||||||
) {
|
) {
|
||||||
// Final output - process immediately (clear any pending debounce)
|
|
||||||
if (debounceTimeoutRef.current) {
|
|
||||||
clearTimeout(debounceTimeoutRef.current)
|
|
||||||
debounceTimeoutRef.current = null
|
|
||||||
pendingXmlRef.current = null
|
|
||||||
}
|
|
||||||
// Show toast only if final XML is malformed
|
// Show toast only if final XML is malformed
|
||||||
handleDisplayChart(xml, true)
|
handleDisplayChart(xml, true)
|
||||||
processedToolCalls.current.add(toolCallId)
|
processedToolCalls.current.add(toolCallId)
|
||||||
// Clean up the ref entry - tool is complete, no longer needed
|
|
||||||
lastProcessedXmlRef.current.delete(toolCallId)
|
|
||||||
processedCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log(
|
|
||||||
`perf:message-display-useEffect processed=${processedCount} skipped=${skippedCount} debounced=${debouncedCount}`,
|
|
||||||
)
|
|
||||||
console.timeEnd("perf:message-display-useEffect")
|
|
||||||
|
|
||||||
// Cleanup: clear any pending debounce timeout on unmount
|
|
||||||
return () => {
|
|
||||||
if (debounceTimeoutRef.current) {
|
|
||||||
clearTimeout(debounceTimeoutRef.current)
|
|
||||||
debounceTimeoutRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [messages, handleDisplayChart])
|
}, [messages, handleDisplayChart])
|
||||||
|
|
||||||
const renderToolPart = (part: ToolPartLike) => {
|
const renderToolPart = (part: ToolPartLike) => {
|
||||||
@@ -551,7 +457,8 @@ export function ChatMessageDisplay({
|
|||||||
const isTruncated =
|
const isTruncated =
|
||||||
(toolName === "display_diagram" ||
|
(toolName === "display_diagram" ||
|
||||||
toolName === "append_diagram") &&
|
toolName === "append_diagram") &&
|
||||||
!isMxCellXmlComplete(input?.xml)
|
(!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
|
||||||
@@ -600,7 +507,7 @@ export function ChatMessageDisplay({
|
|||||||
const isTruncated =
|
const isTruncated =
|
||||||
(toolName === "display_diagram" ||
|
(toolName === "display_diagram" ||
|
||||||
toolName === "append_diagram") &&
|
toolName === "append_diagram") &&
|
||||||
!isMxCellXmlComplete(input?.xml)
|
(!input?.xml || !input.xml.includes("</root>"))
|
||||||
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"}`}
|
||||||
|
|||||||
@@ -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, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
import { formatXML, wrapWithMxFile } from "@/lib/utils"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display"
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
|
|
||||||
// localStorage keys for persistence
|
// localStorage keys for persistence
|
||||||
@@ -40,7 +40,6 @@ interface MessagePart {
|
|||||||
type: string
|
type: string
|
||||||
state?: string
|
state?: string
|
||||||
toolName?: string
|
toolName?: string
|
||||||
input?: { xml?: string; [key: string]: unknown }
|
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +145,6 @@ export default function ChatPanel({
|
|||||||
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
||||||
const [tpmLimit, setTpmLimit] = useState(0)
|
const [tpmLimit, setTpmLimit] = useState(0)
|
||||||
const [showNewChatDialog, setShowNewChatDialog] = useState(false)
|
const [showNewChatDialog, setShowNewChatDialog] = useState(false)
|
||||||
const [minimalStyle, setMinimalStyle] = useState(false)
|
|
||||||
|
|
||||||
// Check config on mount
|
// Check config on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -202,15 +200,6 @@ export default function ChatPanel({
|
|||||||
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
|
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
|
||||||
const processedToolCallsRef = useRef<Set<string>>(new Set())
|
const processedToolCallsRef = useRef<Set<string>>(new Set())
|
||||||
|
|
||||||
// Debounce timeout for localStorage writes (prevents blocking during streaming)
|
|
||||||
const localStorageDebounceRef = useRef<ReturnType<
|
|
||||||
typeof setTimeout
|
|
||||||
> | null>(null)
|
|
||||||
const xmlStorageDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -233,16 +222,9 @@ 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 }
|
||||||
|
|
||||||
// DEBUG: Log raw input to diagnose false truncation detection
|
// Check if XML is truncated (missing </root> indicates incomplete output)
|
||||||
console.log(
|
const isTruncated =
|
||||||
"[display_diagram] XML ending (last 100 chars):",
|
!xml.includes("</root>") && !xml.trim().endsWith("/>")
|
||||||
xml.slice(-100),
|
|
||||||
)
|
|
||||||
console.log("[display_diagram] XML length:", xml.length)
|
|
||||||
|
|
||||||
// Check if XML is truncated (incomplete mxCell indicates truncated output)
|
|
||||||
const isTruncated = !isMxCellXmlComplete(xml)
|
|
||||||
console.log("[display_diagram] isTruncated:", isTruncated)
|
|
||||||
|
|
||||||
if (isTruncated) {
|
if (isTruncated) {
|
||||||
// Store the partial XML for continuation via append_diagram
|
// Store the partial XML for continuation via append_diagram
|
||||||
@@ -262,9 +244,9 @@ ${partialEnding}
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
NEXT STEP: Call append_diagram with the continuation XML.
|
NEXT STEP: Call append_diagram with the continuation XML.
|
||||||
- Do NOT include wrapper tags or root cells (id="0", id="1")
|
- Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> (they already exist)
|
||||||
- Start from EXACTLY where you stopped
|
- Start from EXACTLY where you stopped
|
||||||
- Complete all remaining mxCell elements`,
|
- End with the closing </root> tag to complete the diagram`,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -394,21 +376,17 @@ 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 =
|
||||||
trimmed.startsWith("<mxGraphModel") ||
|
xml.trim().startsWith("<mxGraphModel") ||
|
||||||
trimmed.startsWith("<root") ||
|
xml.trim().startsWith("<root") ||
|
||||||
trimmed.startsWith("<mxfile") ||
|
xml.trim().startsWith('<mxCell id="0"')
|
||||||
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 wrapper tags or root cells (id="0", id="1").
|
errorText: `ERROR: You started fresh with wrapper tags. Do NOT include <mxGraphModel>, <root>, or <mxCell id="0">.
|
||||||
|
|
||||||
Continue from EXACTLY where the partial ended:
|
Continue from EXACTLY where the partial ended:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -423,8 +401,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 (last mxCell is complete)
|
// Check if XML is now complete
|
||||||
const isComplete = isMxCellXmlComplete(partialXmlRef.current)
|
const isComplete = partialXmlRef.current.includes("</root>")
|
||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
// Wrap and display the complete diagram
|
// Wrap and display the complete diagram
|
||||||
@@ -461,7 +439,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 (mxCell not closed). Call append_diagram again to continue.
|
errorText: `XML still incomplete (missing </root>). Call append_diagram again to continue.
|
||||||
|
|
||||||
Current ending:
|
Current ending:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -523,10 +501,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
// DEBUG: Log finish reason to diagnose truncation
|
|
||||||
console.log("[onFinish] finishReason:", metadata?.finishReason)
|
|
||||||
console.log("[onFinish] metadata:", metadata)
|
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
|
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
|
||||||
const inputTokens = Number.isFinite(metadata.inputTokens)
|
const inputTokens = Number.isFinite(metadata.inputTokens)
|
||||||
@@ -632,10 +606,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to restore from localStorage:", error)
|
console.error("Failed to restore from localStorage:", error)
|
||||||
// On complete failure, clear storage to allow recovery
|
|
||||||
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
|
||||||
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
|
||||||
toast.error("Session data was corrupted. Starting fresh.")
|
|
||||||
}
|
}
|
||||||
}, [setMessages])
|
}, [setMessages])
|
||||||
|
|
||||||
@@ -680,71 +650,32 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
}, 500)
|
}, 500)
|
||||||
}, [isDrawioReady, onDisplayChart])
|
}, [isDrawioReady, onDisplayChart])
|
||||||
|
|
||||||
// Save messages to localStorage whenever they change (debounced to prevent blocking during streaming)
|
// Save messages to localStorage whenever they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasRestoredRef.current) return
|
if (!hasRestoredRef.current) return
|
||||||
|
|
||||||
// Clear any pending save
|
|
||||||
if (localStorageDebounceRef.current) {
|
|
||||||
clearTimeout(localStorageDebounceRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce: save after 1 second of no changes
|
|
||||||
localStorageDebounceRef.current = setTimeout(() => {
|
|
||||||
try {
|
try {
|
||||||
console.time("perf:localStorage-messages")
|
localStorage.setItem(STORAGE_MESSAGES_KEY, JSON.stringify(messages))
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_MESSAGES_KEY,
|
|
||||||
JSON.stringify(messages),
|
|
||||||
)
|
|
||||||
console.timeEnd("perf:localStorage-messages")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save messages to localStorage:", error)
|
console.error("Failed to save messages to localStorage:", error)
|
||||||
}
|
}
|
||||||
}, LOCAL_STORAGE_DEBOUNCE_MS)
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
return () => {
|
|
||||||
if (localStorageDebounceRef.current) {
|
|
||||||
clearTimeout(localStorageDebounceRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
// Save diagram XML to localStorage whenever it changes (debounced)
|
// Save diagram XML to localStorage whenever it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canSaveDiagram) return
|
if (!canSaveDiagram) return
|
||||||
if (!chartXML || chartXML.length <= 300) return
|
if (chartXML && chartXML.length > 300) {
|
||||||
|
|
||||||
// Clear any pending save
|
|
||||||
if (xmlStorageDebounceRef.current) {
|
|
||||||
clearTimeout(xmlStorageDebounceRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce: save after 1 second of no changes
|
|
||||||
xmlStorageDebounceRef.current = setTimeout(() => {
|
|
||||||
console.time("perf:localStorage-xml")
|
|
||||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||||
console.timeEnd("perf:localStorage-xml")
|
|
||||||
}, LOCAL_STORAGE_DEBOUNCE_MS)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (xmlStorageDebounceRef.current) {
|
|
||||||
clearTimeout(xmlStorageDebounceRef.current)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [chartXML, canSaveDiagram])
|
}, [chartXML, canSaveDiagram])
|
||||||
|
|
||||||
// Save XML snapshots to localStorage whenever they change
|
// Save XML snapshots to localStorage whenever they change
|
||||||
const saveXmlSnapshots = useCallback(() => {
|
const saveXmlSnapshots = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
console.time("perf:localStorage-snapshots")
|
|
||||||
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_XML_SNAPSHOTS_KEY,
|
STORAGE_XML_SNAPSHOTS_KEY,
|
||||||
JSON.stringify(snapshotsArray),
|
JSON.stringify(snapshotsArray),
|
||||||
)
|
)
|
||||||
console.timeEnd("perf:localStorage-snapshots")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"Failed to save XML snapshots to localStorage:",
|
"Failed to save XML snapshots to localStorage:",
|
||||||
@@ -1001,9 +932,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
}),
|
}),
|
||||||
...(config.aiModel && { "x-ai-model": config.aiModel }),
|
...(config.aiModel && { "x-ai-model": config.aiModel }),
|
||||||
}),
|
}),
|
||||||
...(minimalStyle && {
|
|
||||||
"x-minimal-style": "true",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1189,7 +1117,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
style: {
|
style: {
|
||||||
maxWidth: "480px",
|
maxWidth: "480px",
|
||||||
},
|
},
|
||||||
duration: 2000,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -1319,8 +1246,6 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
onToggleHistory={setShowHistory}
|
onToggleHistory={setShowHistory}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
error={error}
|
error={error}
|
||||||
minimalStyle={minimalStyle}
|
|
||||||
onMinimalStyleChange={setMinimalStyle}
|
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -86,20 +86,16 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
chart: string,
|
chart: string,
|
||||||
skipValidation?: boolean,
|
skipValidation?: boolean,
|
||||||
): string | null => {
|
): string | null => {
|
||||||
console.time("perf:loadDiagram")
|
|
||||||
let xmlToLoad = chart
|
let xmlToLoad = chart
|
||||||
|
|
||||||
// Validate XML structure before loading (unless skipped for internal use)
|
// Validate XML structure before loading (unless skipped for internal use)
|
||||||
if (!skipValidation) {
|
if (!skipValidation) {
|
||||||
console.time("perf:loadDiagram-validation")
|
|
||||||
const validation = validateAndFixXml(chart)
|
const validation = validateAndFixXml(chart)
|
||||||
console.timeEnd("perf:loadDiagram-validation")
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[loadDiagram] Validation error:",
|
"[loadDiagram] Validation error:",
|
||||||
validation.error,
|
validation.error,
|
||||||
)
|
)
|
||||||
console.timeEnd("perf:loadDiagram")
|
|
||||||
return validation.error
|
return validation.error
|
||||||
}
|
}
|
||||||
// Use fixed XML if auto-fix was applied
|
// Use fixed XML if auto-fix was applied
|
||||||
@@ -116,14 +112,11 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setChartXML(xmlToLoad)
|
setChartXML(xmlToLoad)
|
||||||
|
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
console.time("perf:drawio-iframe-load")
|
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: xmlToLoad,
|
xml: xmlToLoad,
|
||||||
})
|
})
|
||||||
console.timeEnd("perf:drawio-iframe-load")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.timeEnd("perf:loadDiagram")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,20 +138,14 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setLatestSvg(data.data)
|
setLatestSvg(data.data)
|
||||||
|
|
||||||
// Only add to history if this was a user-initiated export
|
// Only add to history if this was a user-initiated export
|
||||||
// Limit to 20 entries to prevent memory leaks during long sessions
|
|
||||||
const MAX_HISTORY_SIZE = 20
|
|
||||||
if (expectHistoryExportRef.current) {
|
if (expectHistoryExportRef.current) {
|
||||||
setDiagramHistory((prev) => {
|
setDiagramHistory((prev) => [
|
||||||
const newHistory = [
|
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
svg: data.data,
|
svg: data.data,
|
||||||
xml: extractedXML,
|
xml: extractedXML,
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
// Keep only the last MAX_HISTORY_SIZE entries (circular buffer)
|
|
||||||
return newHistory.slice(-MAX_HISTORY_SIZE)
|
|
||||||
})
|
|
||||||
expectHistoryExportRef.current = false
|
expectHistoryExportRef.current = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ 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: `<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">
|
xml: `<root>
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -249,12 +254,18 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
|
|
||||||
<mxCell id="output_label" value="Outputs
(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
(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: `<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">
|
xml: `<root>
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -313,12 +324,18 @@ 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: `<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">
|
xml: `<root>
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -374,12 +391,16 @@ 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: `<mxCell id="title_bg" parent="1"
|
xml: ` <root>
|
||||||
|
<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" />
|
||||||
@@ -730,12 +751,18 @@ 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: `<mxCell id="2" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
|
xml: `<root>
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -875,7 +902,9 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
<mxPoint x="235" y="290"/>
|
<mxPoint x="235" y="290"/>
|
||||||
</Array>
|
</Array>
|
||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>`,
|
</mxCell>
|
||||||
|
|
||||||
|
</root>`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -104,21 +104,22 @@ When using edit_diagram tool:
|
|||||||
|
|
||||||
## Draw.io XML Structure Reference
|
## Draw.io XML Structure Reference
|
||||||
|
|
||||||
**IMPORTANT:** You only generate the mxCell elements. The wrapper structure and root cells (id="0", id="1") are added automatically.
|
Basic structure:
|
||||||
|
|
||||||
Example - generate ONLY this:
|
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="2" value="Label" style="rounded=1;" vertex="1" parent="1">
|
<mxGraphModel>
|
||||||
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
<root>
|
||||||
</mxCell>
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
Note: All other mxCell elements go as siblings after id="1".
|
||||||
|
|
||||||
CRITICAL RULES:
|
CRITICAL RULES:
|
||||||
1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
|
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/>
|
||||||
2. Do NOT include root cells (id="0" or id="1") - they are added automatically
|
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell
|
||||||
3. ALL mxCell elements must be siblings - NEVER nest mxCell inside another mxCell
|
3. Use unique sequential IDs for all cells (start from "2" for user content)
|
||||||
4. Use unique sequential IDs starting from "2"
|
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
|
||||||
5. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
|
|
||||||
|
|
||||||
Shape (vertex) example:
|
Shape (vertex) example:
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
@@ -132,90 +133,12 @@ Connector (edge) example:
|
|||||||
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
|
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
|
|
||||||
### Edge Routing Rules:
|
|
||||||
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
|
|
||||||
|
|
||||||
**Rule 1: NEVER let multiple edges share the same path**
|
|
||||||
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
|
|
||||||
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
|
|
||||||
|
|
||||||
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
|
|
||||||
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
|
|
||||||
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
|
|
||||||
|
|
||||||
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
|
|
||||||
- Every edge MUST have these 4 attributes set in the style
|
|
||||||
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
|
|
||||||
|
|
||||||
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
|
|
||||||
- Before creating an edge, identify ALL shapes positioned between source and target
|
|
||||||
- If any shape is in the direct path, you MUST use waypoints to route around it
|
|
||||||
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
|
|
||||||
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
|
|
||||||
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
|
|
||||||
- NEVER draw a line that visually crosses over another shape's bounding box
|
|
||||||
|
|
||||||
**Rule 5: Plan layout strategically BEFORE generating XML**
|
|
||||||
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
|
|
||||||
- Space shapes 150-200px apart to create clear routing channels for edges
|
|
||||||
- Mentally trace each edge: "What shapes are between source and target?"
|
|
||||||
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
|
|
||||||
|
|
||||||
**Rule 6: Use multiple waypoints for complex routing**
|
|
||||||
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
|
|
||||||
- Each direction change needs a waypoint (corner point)
|
|
||||||
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
|
|
||||||
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
|
|
||||||
|
|
||||||
**Rule 7: Choose NATURAL connection points based on flow direction**
|
|
||||||
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
|
|
||||||
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
|
|
||||||
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
|
|
||||||
- For DIAGONAL connections: use the side closest to the target, not corners
|
|
||||||
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
|
|
||||||
|
|
||||||
**Before generating XML, mentally verify:**
|
|
||||||
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
|
|
||||||
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
|
|
||||||
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
|
|
||||||
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
|
|
||||||
|
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
// Style instructions - only included when minimalStyle is false
|
|
||||||
const STYLE_INSTRUCTIONS = `
|
|
||||||
Common styles:
|
Common styles:
|
||||||
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
||||||
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
||||||
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
||||||
`
|
|
||||||
|
|
||||||
// Minimal style instruction - skip styling and focus on layout (prepended to prompt for emphasis)
|
|
||||||
const MINIMAL_STYLE_INSTRUCTION = `
|
|
||||||
## ⚠️ MINIMAL STYLE MODE ACTIVE ⚠️
|
|
||||||
|
|
||||||
### No Styling - Plain Black/White Only
|
|
||||||
- NO fillColor, NO strokeColor, NO rounded, NO fontSize, NO fontStyle
|
|
||||||
- NO color attributes (no hex colors like #ff69b4)
|
|
||||||
- Style: "whiteSpace=wrap;html=1;" for shapes, "html=1;endArrow=classic;" for edges
|
|
||||||
- IGNORE all color/style examples below
|
|
||||||
|
|
||||||
### Container/Group Shapes - MUST be Transparent
|
|
||||||
- For container shapes (boxes that contain other shapes): use "fillColor=none;" to make background transparent
|
|
||||||
- This prevents containers from covering child elements
|
|
||||||
- Example: style="whiteSpace=wrap;html=1;fillColor=none;" for container rectangles
|
|
||||||
|
|
||||||
### Focus on Layout Quality
|
|
||||||
Since we skip styling, STRICTLY follow the "Edge Routing Rules" section below:
|
|
||||||
- SPACING: Minimum 50px gap between all elements
|
|
||||||
- NO OVERLAPS: Elements and edges must never overlap
|
|
||||||
- Follow ALL 7 Edge Routing Rules for arrow positioning
|
|
||||||
- Use waypoints to route edges AROUND obstacles
|
|
||||||
- Use different exitY/entryY values for multiple edges between same nodes
|
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -228,30 +151,34 @@ 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. Generate ONLY mxCell elements - wrapper tags and root cells are added automatically
|
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements
|
||||||
2. All mxCell elements must be siblings - never nested inside other mxCell elements
|
2. Every mxCell needs a unique id attribute
|
||||||
3. Every mxCell needs a unique id attribute (start from "2")
|
3. Every mxCell (except id="0") needs a valid parent attribute referencing an existing cell
|
||||||
4. Every mxCell needs a valid parent attribute (use "1" for top-level, or container-id for grouped)
|
4. Edge source/target attributes must reference existing cell IDs
|
||||||
5. Edge source/target attributes must reference existing cell IDs
|
5. Escape special characters in values: < for <, > for >, & for &, " for "
|
||||||
6. Escape special characters in values: < for <, > for >, & for &, " for "
|
6. Always start with the two root cells: <mxCell id="0"/><mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
**Example with swimlanes and edges** (generate ONLY this - no wrapper tags):
|
**Example with swimlanes and edges** (note: all mxCells are siblings under <root>):
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
|
<root>
|
||||||
|
<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
|
||||||
@@ -259,9 +186,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 include any wrapper tags - just continue the mxCell elements
|
1. Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> - they already exist in the partial
|
||||||
2. Continue from EXACTLY where your previous output stopped
|
2. Continue from EXACTLY where your previous output stopped
|
||||||
3. Complete the remaining mxCell elements
|
3. End with the closing </root> tag to complete the diagram
|
||||||
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.
|
||||||
@@ -335,6 +262,53 @@ If edit_diagram fails with "pattern not found":
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Edge Routing Rules:
|
||||||
|
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
|
||||||
|
|
||||||
|
**Rule 1: NEVER let multiple edges share the same path**
|
||||||
|
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
|
||||||
|
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
|
||||||
|
|
||||||
|
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
|
||||||
|
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
|
||||||
|
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
|
||||||
|
|
||||||
|
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
|
||||||
|
- Every edge MUST have these 4 attributes set in the style
|
||||||
|
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
|
||||||
|
|
||||||
|
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
|
||||||
|
- Before creating an edge, identify ALL shapes positioned between source and target
|
||||||
|
- If any shape is in the direct path, you MUST use waypoints to route around it
|
||||||
|
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
|
||||||
|
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
|
||||||
|
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
|
||||||
|
- NEVER draw a line that visually crosses over another shape's bounding box
|
||||||
|
|
||||||
|
**Rule 5: Plan layout strategically BEFORE generating XML**
|
||||||
|
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
|
||||||
|
- Space shapes 150-200px apart to create clear routing channels for edges
|
||||||
|
- Mentally trace each edge: "What shapes are between source and target?"
|
||||||
|
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
|
||||||
|
|
||||||
|
**Rule 6: Use multiple waypoints for complex routing**
|
||||||
|
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
|
||||||
|
- Each direction change needs a waypoint (corner point)
|
||||||
|
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
|
||||||
|
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
|
||||||
|
|
||||||
|
**Rule 7: Choose NATURAL connection points based on flow direction**
|
||||||
|
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
|
||||||
|
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
|
||||||
|
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
|
||||||
|
- For DIAGONAL connections: use the side closest to the target, not corners
|
||||||
|
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
|
||||||
|
|
||||||
|
**Before generating XML, mentally verify:**
|
||||||
|
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
|
||||||
|
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
|
||||||
|
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
|
||||||
|
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
|
||||||
|
|
||||||
## Edge Examples
|
## Edge Examples
|
||||||
|
|
||||||
@@ -388,16 +362,12 @@ const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the appropriate system prompt based on the model ID and style preference
|
* Get the appropriate system prompt based on the model ID
|
||||||
* Uses extended prompt for Opus 4.5 and Haiku 4.5 which have 4000 token cache minimum
|
* Uses extended prompt for Opus 4.5 and Haiku 4.5 which have 4000 token cache minimum
|
||||||
* @param modelId - The AI model ID from environment
|
* @param modelId - The AI model ID from environment
|
||||||
* @param minimalStyle - If true, removes style instructions to save tokens
|
|
||||||
* @returns The system prompt string
|
* @returns The system prompt string
|
||||||
*/
|
*/
|
||||||
export function getSystemPrompt(
|
export function getSystemPrompt(modelId?: string): string {
|
||||||
modelId?: string,
|
|
||||||
minimalStyle?: boolean,
|
|
||||||
): string {
|
|
||||||
const modelName = modelId || "AI"
|
const modelName = modelId || "AI"
|
||||||
|
|
||||||
let prompt: string
|
let prompt: string
|
||||||
@@ -418,15 +388,5 @@ export function getSystemPrompt(
|
|||||||
prompt = DEFAULT_SYSTEM_PROMPT
|
prompt = DEFAULT_SYSTEM_PROMPT
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add style instructions based on preference
|
|
||||||
// Minimal style: prepend instruction at START (more prominent)
|
|
||||||
// Normal style: append at end
|
|
||||||
if (minimalStyle) {
|
|
||||||
console.log(`[System Prompt] Minimal style mode ENABLED`)
|
|
||||||
prompt = MINIMAL_STYLE_INSTRUCTION + prompt
|
|
||||||
} else {
|
|
||||||
prompt += STYLE_INSTRUCTIONS
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt.replace("{{MODEL_NAME}}", modelName)
|
return prompt.replace("{{MODEL_NAME}}", modelName)
|
||||||
}
|
}
|
||||||
|
|||||||
272
lib/utils.ts
272
lib/utils.ts
@@ -29,38 +29,6 @@ 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.
|
|
||||||
* Also handles function-calling wrapper tags that may be incorrectly included.
|
|
||||||
* @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 {
|
|
||||||
let trimmed = xml?.trim() || ""
|
|
||||||
if (!trimmed) return false
|
|
||||||
|
|
||||||
// Strip Anthropic function-calling wrapper tags if present
|
|
||||||
// These can leak into tool input due to AI SDK parsing issues
|
|
||||||
// Use loop because tags are nested: </mxCell></mxParameter></invoke>
|
|
||||||
let prev = ""
|
|
||||||
while (prev !== trimmed) {
|
|
||||||
prev = trimmed
|
|
||||||
trimmed = trimmed
|
|
||||||
.replace(/<\/mxParameter>\s*$/i, "")
|
|
||||||
.replace(/<\/invoke>\s*$/i, "")
|
|
||||||
.replace(/<\/antml:parameter>\s*$/i, "")
|
|
||||||
.replace(/<\/antml:invoke>\s*$/i, "")
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed.endsWith("/>") || trimmed.endsWith("</mxCell>")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// XML Parsing Helpers
|
// XML Parsing Helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -214,13 +182,6 @@ export function convertToLegalXml(xmlString: string): string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix unescaped & characters in attribute values (but not valid entities)
|
|
||||||
// This prevents DOMParser from failing on content like "semantic & missing-step"
|
|
||||||
cellContent = cellContent.replace(
|
|
||||||
/&(?!(?:lt|gt|amp|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)/g,
|
|
||||||
"&",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Indent each line of the matched block for readability.
|
// Indent each line of the matched block for readability.
|
||||||
const formatted = cellContent
|
const formatted = cellContent
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -236,17 +197,13 @@ 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.
|
||||||
* Always adds root cells (id="0" and id="1") automatically.
|
* Handles cases where XML is just <root>, <mxGraphModel>, or already has <mxfile>.
|
||||||
* If input already contains root cells, they are removed to avoid duplication.
|
* @param xml - The XML string (may be partial or complete)
|
||||||
* LLM should only generate mxCell elements starting from id="2".
|
* @returns Full mxfile-wrapped XML string
|
||||||
* @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 {
|
||||||
const ROOT_CELLS = '<mxCell id="0"/><mxCell id="1" parent="0"/>'
|
if (!xml) {
|
||||||
|
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
|
||||||
@@ -259,20 +216,9 @@ 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>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has <root> wrapper - extract inner content
|
// Just <root> content - extract inner content and wrap fully
|
||||||
let content = xml
|
const rootContent = xml.replace(/<\/?root>/g, "").trim()
|
||||||
if (xml.includes("<root>")) {
|
return `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root>${rootContent}</root></mxGraphModel></diagram></mxfile>`
|
||||||
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>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -823,8 +769,6 @@ function checkNestedMxCells(xml: string): string | null {
|
|||||||
* @returns null if valid, error message string if invalid
|
* @returns null if valid, error message string if invalid
|
||||||
*/
|
*/
|
||||||
export function validateMxCellStructure(xml: string): string | null {
|
export function validateMxCellStructure(xml: string): string | null {
|
||||||
console.time("perf:validateMxCellStructure")
|
|
||||||
console.log(`perf:validateMxCellStructure XML size: ${xml.length} bytes`)
|
|
||||||
// Size check for performance
|
// Size check for performance
|
||||||
if (xml.length > MAX_XML_SIZE) {
|
if (xml.length > MAX_XML_SIZE) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -834,18 +778,10 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
|
|
||||||
// 0. First use DOM parser to catch syntax errors (most accurate)
|
// 0. First use DOM parser to catch syntax errors (most accurate)
|
||||||
try {
|
try {
|
||||||
console.time("perf:validate-DOMParser")
|
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const doc = parser.parseFromString(xml, "text/xml")
|
const doc = parser.parseFromString(xml, "text/xml")
|
||||||
console.timeEnd("perf:validate-DOMParser")
|
|
||||||
const parseError = doc.querySelector("parsererror")
|
const parseError = doc.querySelector("parsererror")
|
||||||
if (parseError) {
|
if (parseError) {
|
||||||
const actualError = parseError.textContent || "Unknown parse error"
|
|
||||||
console.log(
|
|
||||||
"[validateMxCellStructure] DOMParser error:",
|
|
||||||
actualError,
|
|
||||||
)
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,7 +790,6 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
for (const cell of allCells) {
|
for (const cell of allCells) {
|
||||||
if (cell.parentElement?.tagName === "mxCell") {
|
if (cell.parentElement?.tagName === "mxCell") {
|
||||||
const id = cell.getAttribute("id") || "unknown"
|
const id = cell.getAttribute("id") || "unknown"
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
|
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,18 +803,12 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
|
|
||||||
// 1. Check for CDATA wrapper (invalid at document root)
|
// 1. Check for CDATA wrapper (invalid at document root)
|
||||||
if (/^\s*<!\[CDATA\[/.test(xml)) {
|
if (/^\s*<!\[CDATA\[/.test(xml)) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
|
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check for duplicate structural attributes
|
// 2. Check for duplicate structural attributes
|
||||||
console.time("perf:checkDuplicateAttributes")
|
|
||||||
const dupAttrError = checkDuplicateAttributes(xml)
|
const dupAttrError = checkDuplicateAttributes(xml)
|
||||||
console.timeEnd("perf:checkDuplicateAttributes")
|
if (dupAttrError) return dupAttrError
|
||||||
if (dupAttrError) {
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return dupAttrError
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check for unescaped < in attribute values
|
// 3. Check for unescaped < in attribute values
|
||||||
const attrValuePattern = /=\s*"([^"]*)"/g
|
const attrValuePattern = /=\s*"([^"]*)"/g
|
||||||
@@ -887,67 +816,44 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
|
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
|
||||||
const value = attrValMatch[1]
|
const value = attrValMatch[1]
|
||||||
if (/</.test(value) && !/</.test(value)) {
|
if (/</.test(value) && !/</.test(value)) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: Unescaped < character in attribute values. Replace < with <"
|
return "Invalid XML: Unescaped < character in attribute values. Replace < with <"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check for duplicate IDs
|
// 4. Check for duplicate IDs
|
||||||
console.time("perf:checkDuplicateIds")
|
|
||||||
const dupIdError = checkDuplicateIds(xml)
|
const dupIdError = checkDuplicateIds(xml)
|
||||||
console.timeEnd("perf:checkDuplicateIds")
|
if (dupIdError) return dupIdError
|
||||||
if (dupIdError) {
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return dupIdError
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Check for tag mismatches
|
// 5. Check for tag mismatches
|
||||||
console.time("perf:checkTagMismatches")
|
|
||||||
const tagMismatchError = checkTagMismatches(xml)
|
const tagMismatchError = checkTagMismatches(xml)
|
||||||
console.timeEnd("perf:checkTagMismatches")
|
if (tagMismatchError) return tagMismatchError
|
||||||
if (tagMismatchError) {
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return tagMismatchError
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Check invalid character references
|
// 6. Check invalid character references
|
||||||
const charRefError = checkCharacterReferences(xml)
|
const charRefError = checkCharacterReferences(xml)
|
||||||
if (charRefError) {
|
if (charRefError) return charRefError
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return charRefError
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Check for invalid comment syntax (-- inside comments)
|
// 7. Check for invalid comment syntax (-- inside comments)
|
||||||
const commentPattern = /<!--([\s\S]*?)-->/g
|
const commentPattern = /<!--([\s\S]*?)-->/g
|
||||||
let commentMatch
|
let commentMatch
|
||||||
while ((commentMatch = commentPattern.exec(xml)) !== null) {
|
while ((commentMatch = commentPattern.exec(xml)) !== null) {
|
||||||
if (/--/.test(commentMatch[1])) {
|
if (/--/.test(commentMatch[1])) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
|
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Check for unescaped entity references and invalid entity names
|
// 8. Check for unescaped entity references and invalid entity names
|
||||||
const entityError = checkEntityReferences(xml)
|
const entityError = checkEntityReferences(xml)
|
||||||
if (entityError) {
|
if (entityError) return entityError
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return entityError
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Check for empty id attributes on mxCell
|
// 9. Check for empty id attributes on mxCell
|
||||||
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
|
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return "Invalid XML: Found mxCell element(s) with empty id attribute"
|
return "Invalid XML: Found mxCell element(s) with empty id attribute"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Check for nested mxCell tags
|
// 10. Check for nested mxCell tags
|
||||||
const nestedCellError = checkNestedMxCells(xml)
|
const nestedCellError = checkNestedMxCells(xml)
|
||||||
if (nestedCellError) {
|
if (nestedCellError) return nestedCellError
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return nestedCellError
|
|
||||||
}
|
|
||||||
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1151,56 +1057,12 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
|||||||
// This handles both opening and closing tags
|
// This handles both opening and closing tags
|
||||||
const hasCellTags = /<\/?Cell[\s>]/i.test(fixed)
|
const hasCellTags = /<\/?Cell[\s>]/i.test(fixed)
|
||||||
if (hasCellTags) {
|
if (hasCellTags) {
|
||||||
console.log("[autoFixXml] Step 8: Found <Cell> tags to fix")
|
|
||||||
const beforeFix = fixed
|
|
||||||
fixed = fixed.replace(/<Cell(\s)/gi, "<mxCell$1")
|
fixed = fixed.replace(/<Cell(\s)/gi, "<mxCell$1")
|
||||||
fixed = fixed.replace(/<Cell>/gi, "<mxCell>")
|
fixed = fixed.replace(/<Cell>/gi, "<mxCell>")
|
||||||
fixed = fixed.replace(/<\/Cell>/gi, "</mxCell>")
|
fixed = fixed.replace(/<\/Cell>/gi, "</mxCell>")
|
||||||
if (beforeFix !== fixed) {
|
|
||||||
console.log("[autoFixXml] Step 8: Fixed <Cell> tags")
|
|
||||||
}
|
|
||||||
fixes.push("Fixed <Cell> tags to <mxCell>")
|
fixes.push("Fixed <Cell> tags to <mxCell>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8b. Remove non-draw.io tags (LLM sometimes includes Claude's function calling XML)
|
|
||||||
// Valid draw.io tags: mxfile, diagram, mxGraphModel, root, mxCell, mxGeometry, mxPoint, Array, Object
|
|
||||||
const validDrawioTags = new Set([
|
|
||||||
"mxfile",
|
|
||||||
"diagram",
|
|
||||||
"mxGraphModel",
|
|
||||||
"root",
|
|
||||||
"mxCell",
|
|
||||||
"mxGeometry",
|
|
||||||
"mxPoint",
|
|
||||||
"Array",
|
|
||||||
"Object",
|
|
||||||
"mxRectangle",
|
|
||||||
])
|
|
||||||
const foreignTagPattern = /<\/?([a-zA-Z][a-zA-Z0-9_]*)[^>]*>/g
|
|
||||||
let foreignMatch
|
|
||||||
const foreignTags = new Set<string>()
|
|
||||||
while ((foreignMatch = foreignTagPattern.exec(fixed)) !== null) {
|
|
||||||
const tagName = foreignMatch[1]
|
|
||||||
if (!validDrawioTags.has(tagName)) {
|
|
||||||
foreignTags.add(tagName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (foreignTags.size > 0) {
|
|
||||||
console.log(
|
|
||||||
"[autoFixXml] Step 8b: Found foreign tags:",
|
|
||||||
Array.from(foreignTags),
|
|
||||||
)
|
|
||||||
for (const tag of foreignTags) {
|
|
||||||
// Remove opening tags (with or without attributes)
|
|
||||||
fixed = fixed.replace(new RegExp(`<${tag}[^>]*>`, "gi"), "")
|
|
||||||
// Remove closing tags
|
|
||||||
fixed = fixed.replace(new RegExp(`</${tag}>`, "gi"), "")
|
|
||||||
}
|
|
||||||
fixes.push(
|
|
||||||
`Removed foreign tags: ${Array.from(foreignTags).join(", ")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Fix common closing tag typos
|
// 9. Fix common closing tag typos
|
||||||
const tagTypos = [
|
const tagTypos = [
|
||||||
{ wrong: /<\/mxElement>/gi, right: "</mxCell>", name: "</mxElement>" },
|
{ wrong: /<\/mxElement>/gi, right: "</mxCell>", name: "</mxElement>" },
|
||||||
@@ -1266,98 +1128,6 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10b. Remove extra closing tags (more closes than opens)
|
|
||||||
// Need to properly count self-closing tags (they don't need closing tags)
|
|
||||||
const tagCounts = new Map<
|
|
||||||
string,
|
|
||||||
{ opens: number; closes: number; selfClosing: number }
|
|
||||||
>()
|
|
||||||
// Match full tags to detect self-closing by checking if ends with />
|
|
||||||
const fullTagPattern = /<(\/?[a-zA-Z][a-zA-Z0-9]*)[^>]*>/g
|
|
||||||
let tagCountMatch
|
|
||||||
while ((tagCountMatch = fullTagPattern.exec(fixed)) !== null) {
|
|
||||||
const fullMatch = tagCountMatch[0] // e.g., "<mxCell .../>" or "</mxCell>"
|
|
||||||
const tagPart = tagCountMatch[1] // e.g., "mxCell" or "/mxCell"
|
|
||||||
const isClosing = tagPart.startsWith("/")
|
|
||||||
const isSelfClosing = fullMatch.endsWith("/>")
|
|
||||||
const tagName = isClosing ? tagPart.slice(1) : tagPart
|
|
||||||
|
|
||||||
let counts = tagCounts.get(tagName)
|
|
||||||
if (!counts) {
|
|
||||||
counts = { opens: 0, closes: 0, selfClosing: 0 }
|
|
||||||
tagCounts.set(tagName, counts)
|
|
||||||
}
|
|
||||||
if (isClosing) {
|
|
||||||
counts.closes++
|
|
||||||
} else if (isSelfClosing) {
|
|
||||||
counts.selfClosing++
|
|
||||||
} else {
|
|
||||||
counts.opens++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log tag counts for debugging
|
|
||||||
for (const [tagName, counts] of tagCounts) {
|
|
||||||
if (
|
|
||||||
tagName === "mxCell" ||
|
|
||||||
tagName === "mxGeometry" ||
|
|
||||||
counts.opens !== counts.closes
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`[autoFixXml] Step 10b: ${tagName} - opens: ${counts.opens}, closes: ${counts.closes}, selfClosing: ${counts.selfClosing}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find tags with extra closing tags (self-closing tags are balanced, don't need closing)
|
|
||||||
for (const [tagName, counts] of tagCounts) {
|
|
||||||
const extraCloses = counts.closes - counts.opens // Only compare opens vs closes (self-closing are balanced)
|
|
||||||
if (extraCloses > 0) {
|
|
||||||
console.log(
|
|
||||||
`[autoFixXml] Step 10b: ${tagName} has ${counts.opens} opens, ${counts.closes} closes, removing ${extraCloses} extra`,
|
|
||||||
)
|
|
||||||
// Remove extra closing tags from the end
|
|
||||||
let removed = 0
|
|
||||||
const closeTagPattern = new RegExp(`</${tagName}>`, "g")
|
|
||||||
const matches = [...fixed.matchAll(closeTagPattern)]
|
|
||||||
// Remove from the end (last occurrences are likely the extras)
|
|
||||||
for (
|
|
||||||
let i = matches.length - 1;
|
|
||||||
i >= 0 && removed < extraCloses;
|
|
||||||
i--
|
|
||||||
) {
|
|
||||||
const match = matches[i]
|
|
||||||
const idx = match.index ?? 0
|
|
||||||
fixed = fixed.slice(0, idx) + fixed.slice(idx + match[0].length)
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(
|
|
||||||
`[autoFixXml] Step 10b: Removed ${removed} extra </${tagName}>`,
|
|
||||||
)
|
|
||||||
fixes.push(
|
|
||||||
`Removed ${removed} extra </${tagName}> closing tag(s)`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10c. Remove trailing garbage after last XML tag (e.g., stray backslashes, text)
|
|
||||||
// Find the last valid closing tag or self-closing tag
|
|
||||||
const closingTagPattern = /<\/[a-zA-Z][a-zA-Z0-9]*>|\/>/g
|
|
||||||
let lastValidTagEnd = -1
|
|
||||||
let closingMatch
|
|
||||||
while ((closingMatch = closingTagPattern.exec(fixed)) !== null) {
|
|
||||||
lastValidTagEnd = closingMatch.index + closingMatch[0].length
|
|
||||||
}
|
|
||||||
if (lastValidTagEnd > 0 && lastValidTagEnd < fixed.length) {
|
|
||||||
const trailing = fixed.slice(lastValidTagEnd).trim()
|
|
||||||
if (trailing) {
|
|
||||||
fixed = fixed.slice(0, lastValidTagEnd)
|
|
||||||
fixes.push("Removed trailing garbage after last XML tag")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11. Fix nested mxCell by flattening
|
// 11. Fix nested mxCell by flattening
|
||||||
// Pattern A: <mxCell id="X">...<mxCell id="X">...</mxCell></mxCell> (duplicate ID)
|
// Pattern A: <mxCell id="X">...<mxCell id="X">...</mxCell></mxCell> (duplicate ID)
|
||||||
// Pattern B: <mxCell id="X">...<mxCell id="Y">...</mxCell></mxCell> (different ID - true nesting)
|
// Pattern B: <mxCell id="X">...<mxCell id="Y">...</mxCell></mxCell> (different ID - true nesting)
|
||||||
@@ -1567,26 +1337,16 @@ export function validateAndFixXml(xml: string): {
|
|||||||
|
|
||||||
// Try to fix
|
// Try to fix
|
||||||
const { fixed, fixes } = autoFixXml(xml)
|
const { fixed, fixes } = autoFixXml(xml)
|
||||||
console.log("[validateAndFixXml] Fixes applied:", fixes)
|
|
||||||
|
|
||||||
// Validate the fixed version
|
// Validate the fixed version
|
||||||
error = validateMxCellStructure(fixed)
|
error = validateMxCellStructure(fixed)
|
||||||
if (error) {
|
|
||||||
console.log("[validateAndFixXml] Still invalid after fix:", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return { valid: true, error: null, fixed, fixes }
|
return { valid: true, error: null, fixed, fixes }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still invalid after fixes - but return the partially fixed XML
|
// Still invalid after fixes
|
||||||
// so we can see what was fixed and what error remains
|
return { valid: false, error, fixed: null, fixes }
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error,
|
|
||||||
fixed: fixes.length > 0 ? fixed : null,
|
|
||||||
fixes,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractDiagramXML(xml_svg_string: string): string {
|
export function extractDiagramXML(xml_svg_string: string): string {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.1",
|
"version": "0.4.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user