mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
fix: edit_diagram streaming and JSON repair improvements (#271)
- Add shared editDiagramOriginalXmlRef between streaming preview and tool handler to avoid conflicts when applying operations (fixes "cell already exists" errors) - Add JSON repair preprocessing to fix LLM-generated malformed JSON like `:=` - Filter out tool calls with invalid/undefined inputs from interrupted streaming - Remove perf console logs
This commit is contained in:
@@ -70,14 +70,25 @@ function isMinimalDiagram(xml: string): boolean {
|
|||||||
|
|
||||||
// Helper function to replace historical tool call XML with placeholders
|
// Helper function to replace historical tool call XML with placeholders
|
||||||
// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
|
// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
|
||||||
|
// Also fixes invalid/undefined inputs from interrupted streaming
|
||||||
function replaceHistoricalToolInputs(messages: any[]): any[] {
|
function replaceHistoricalToolInputs(messages: any[]): any[] {
|
||||||
return messages.map((msg) => {
|
return messages.map((msg) => {
|
||||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
const replacedContent = msg.content.map((part: any) => {
|
const replacedContent = msg.content
|
||||||
|
.map((part: any) => {
|
||||||
if (part.type === "tool-call") {
|
if (part.type === "tool-call") {
|
||||||
const toolName = part.toolName
|
const toolName = part.toolName
|
||||||
|
// Fix invalid/undefined inputs from interrupted streaming
|
||||||
|
if (
|
||||||
|
!part.input ||
|
||||||
|
typeof part.input !== "object" ||
|
||||||
|
Object.keys(part.input).length === 0
|
||||||
|
) {
|
||||||
|
// Skip tool calls with invalid inputs entirely
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
toolName === "display_diagram" ||
|
toolName === "display_diagram" ||
|
||||||
toolName === "edit_diagram"
|
toolName === "edit_diagram"
|
||||||
@@ -93,6 +104,7 @@ function replaceHistoricalToolInputs(messages: any[]): any[] {
|
|||||||
}
|
}
|
||||||
return part
|
return part
|
||||||
})
|
})
|
||||||
|
.filter(Boolean) // Remove null entries (invalid tool calls)
|
||||||
return { ...msg, content: replacedContent }
|
return { ...msg, content: replacedContent }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -231,6 +243,36 @@ ${userInputText}
|
|||||||
// Convert UIMessages to ModelMessages and add system message
|
// Convert UIMessages to ModelMessages and add system message
|
||||||
const modelMessages = convertToModelMessages(messages)
|
const modelMessages = convertToModelMessages(messages)
|
||||||
|
|
||||||
|
// DEBUG: Log incoming messages structure
|
||||||
|
console.log("[route.ts] Incoming messages count:", messages.length)
|
||||||
|
messages.forEach((msg: any, idx: number) => {
|
||||||
|
console.log(
|
||||||
|
`[route.ts] Message ${idx} role:`,
|
||||||
|
msg.role,
|
||||||
|
"parts count:",
|
||||||
|
msg.parts?.length,
|
||||||
|
)
|
||||||
|
if (msg.parts) {
|
||||||
|
msg.parts.forEach((part: any, partIdx: number) => {
|
||||||
|
if (
|
||||||
|
part.type === "tool-invocation" ||
|
||||||
|
part.type === "tool-result"
|
||||||
|
) {
|
||||||
|
console.log(`[route.ts] Part ${partIdx}:`, {
|
||||||
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
|
hasInput: !!part.input,
|
||||||
|
inputType: typeof part.input,
|
||||||
|
inputKeys:
|
||||||
|
part.input && typeof part.input === "object"
|
||||||
|
? Object.keys(part.input)
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Replace historical tool call XML with placeholders to reduce tokens
|
// Replace historical tool call XML with placeholders to reduce tokens
|
||||||
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
|
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
|
||||||
const enableHistoryReplace =
|
const enableHistoryReplace =
|
||||||
@@ -246,6 +288,63 @@ ${userInputText}
|
|||||||
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Filter out tool-calls with invalid inputs (from failed repair or interrupted streaming)
|
||||||
|
// Bedrock API rejects messages where toolUse.input is not a valid JSON object
|
||||||
|
enhancedMessages = enhancedMessages
|
||||||
|
.map((msg: any) => {
|
||||||
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
const filteredContent = msg.content.filter((part: any) => {
|
||||||
|
if (part.type === "tool-call") {
|
||||||
|
// Check if input is a valid object (not null, undefined, or empty)
|
||||||
|
if (
|
||||||
|
!part.input ||
|
||||||
|
typeof part.input !== "object" ||
|
||||||
|
Object.keys(part.input).length === 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`[route.ts] Filtering out tool-call with invalid input:`,
|
||||||
|
{ toolName: part.toolName, input: part.input },
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return { ...msg, content: filteredContent }
|
||||||
|
})
|
||||||
|
.filter((msg: any) => msg.content && msg.content.length > 0)
|
||||||
|
|
||||||
|
// DEBUG: Log modelMessages structure (what's being sent to AI)
|
||||||
|
console.log("[route.ts] Model messages count:", enhancedMessages.length)
|
||||||
|
enhancedMessages.forEach((msg: any, idx: number) => {
|
||||||
|
console.log(
|
||||||
|
`[route.ts] ModelMsg ${idx} role:`,
|
||||||
|
msg.role,
|
||||||
|
"content count:",
|
||||||
|
msg.content?.length,
|
||||||
|
)
|
||||||
|
if (msg.content) {
|
||||||
|
msg.content.forEach((part: any, partIdx: number) => {
|
||||||
|
if (part.type === "tool-call" || part.type === "tool-result") {
|
||||||
|
console.log(`[route.ts] Content ${partIdx}:`, {
|
||||||
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
|
hasInput: !!part.input,
|
||||||
|
inputType: typeof part.input,
|
||||||
|
inputValue:
|
||||||
|
part.input === undefined
|
||||||
|
? "undefined"
|
||||||
|
: part.input === null
|
||||||
|
? "null"
|
||||||
|
: "object",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Update the last message with user input only (XML moved to separate cached system message)
|
// Update the last message with user input only (XML moved to separate cached system message)
|
||||||
if (enhancedMessages.length >= 1) {
|
if (enhancedMessages.length >= 1) {
|
||||||
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
|
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
|
||||||
@@ -327,14 +426,30 @@ ${userInputText}
|
|||||||
stopWhen: stepCountIs(5),
|
stopWhen: stepCountIs(5),
|
||||||
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
|
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
|
||||||
experimental_repairToolCall: async ({ toolCall, error }) => {
|
experimental_repairToolCall: async ({ toolCall, error }) => {
|
||||||
|
// DEBUG: Log what we're trying to repair
|
||||||
|
console.log(`[repairToolCall] Tool: ${toolCall.toolName}`)
|
||||||
|
console.log(
|
||||||
|
`[repairToolCall] Error: ${error.name} - ${error.message}`,
|
||||||
|
)
|
||||||
|
console.log(`[repairToolCall] Input type: ${typeof toolCall.input}`)
|
||||||
|
console.log(`[repairToolCall] Input value:`, toolCall.input)
|
||||||
|
|
||||||
// Only attempt repair for invalid tool input (broken JSON from truncation)
|
// Only attempt repair for invalid tool input (broken JSON from truncation)
|
||||||
if (
|
if (
|
||||||
error instanceof InvalidToolInputError ||
|
error instanceof InvalidToolInputError ||
|
||||||
error.name === "AI_InvalidToolInputError"
|
error.name === "AI_InvalidToolInputError"
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
// Pre-process to fix common LLM JSON errors that jsonrepair can't handle
|
||||||
|
let inputToRepair = toolCall.input
|
||||||
|
if (typeof inputToRepair === "string") {
|
||||||
|
// Fix `:=` instead of `: ` (LLM sometimes generates this)
|
||||||
|
inputToRepair = inputToRepair.replace(/:=/g, ": ")
|
||||||
|
// Fix `= "` instead of `: "`
|
||||||
|
inputToRepair = inputToRepair.replace(/=\s*"/g, ': "')
|
||||||
|
}
|
||||||
// Use jsonrepair to fix truncated JSON
|
// Use jsonrepair to fix truncated JSON
|
||||||
const repairedInput = jsonrepair(toolCall.input)
|
const repairedInput = jsonrepair(inputToRepair)
|
||||||
console.log(
|
console.log(
|
||||||
`[repairToolCall] Repaired truncated JSON for tool: ${toolCall.toolName}`,
|
`[repairToolCall] Repaired truncated JSON for tool: ${toolCall.toolName}`,
|
||||||
)
|
)
|
||||||
@@ -344,6 +459,26 @@ ${userInputText}
|
|||||||
`[repairToolCall] Failed to repair JSON for tool: ${toolCall.toolName}`,
|
`[repairToolCall] Failed to repair JSON for tool: ${toolCall.toolName}`,
|
||||||
repairError,
|
repairError,
|
||||||
)
|
)
|
||||||
|
// Return a placeholder input to avoid API errors in multi-step
|
||||||
|
// The tool will fail gracefully on client side
|
||||||
|
if (toolCall.toolName === "edit_diagram") {
|
||||||
|
return {
|
||||||
|
...toolCall,
|
||||||
|
input: {
|
||||||
|
operations: [],
|
||||||
|
_error: "JSON repair failed - no operations to apply",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toolCall.toolName === "display_diagram") {
|
||||||
|
return {
|
||||||
|
...toolCall,
|
||||||
|
input: {
|
||||||
|
xml: "",
|
||||||
|
_error: "JSON repair failed - empty diagram",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} 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 {
|
||||||
|
applyDiagramOperations,
|
||||||
convertToLegalXml,
|
convertToLegalXml,
|
||||||
isMxCellXmlComplete,
|
isMxCellXmlComplete,
|
||||||
replaceNodes,
|
replaceNodes,
|
||||||
@@ -42,6 +43,23 @@ interface DiagramOperation {
|
|||||||
new_xml?: string
|
new_xml?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to extract complete operations from streaming input
|
||||||
|
function getCompleteOperations(
|
||||||
|
operations: DiagramOperation[] | undefined,
|
||||||
|
): DiagramOperation[] {
|
||||||
|
if (!operations || !Array.isArray(operations)) return []
|
||||||
|
return operations.filter(
|
||||||
|
(op) =>
|
||||||
|
op &&
|
||||||
|
typeof op.type === "string" &&
|
||||||
|
["update", "add", "delete"].includes(op.type) &&
|
||||||
|
typeof op.cell_id === "string" &&
|
||||||
|
op.cell_id.length > 0 &&
|
||||||
|
// delete doesn't need new_xml, update/add do
|
||||||
|
(op.type === "delete" || typeof op.new_xml === "string"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Tool part interface for type safety
|
// Tool part interface for type safety
|
||||||
interface ToolPartLike {
|
interface ToolPartLike {
|
||||||
type: string
|
type: string
|
||||||
@@ -167,6 +185,7 @@ interface ChatMessageDisplayProps {
|
|||||||
setInput: (input: string) => void
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void
|
setFiles: (files: File[]) => void
|
||||||
processedToolCallsRef: MutableRefObject<Set<string>>
|
processedToolCallsRef: MutableRefObject<Set<string>>
|
||||||
|
editDiagramOriginalXmlRef: MutableRefObject<Map<string, string>>
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
onRegenerate?: (messageIndex: number) => void
|
onRegenerate?: (messageIndex: number) => void
|
||||||
onEditMessage?: (messageIndex: number, newText: string) => void
|
onEditMessage?: (messageIndex: number, newText: string) => void
|
||||||
@@ -178,6 +197,7 @@ export function ChatMessageDisplay({
|
|||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
processedToolCallsRef,
|
processedToolCallsRef,
|
||||||
|
editDiagramOriginalXmlRef,
|
||||||
sessionId,
|
sessionId,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onEditMessage,
|
onEditMessage,
|
||||||
@@ -195,6 +215,14 @@ export function ChatMessageDisplay({
|
|||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
const STREAMING_DEBOUNCE_MS = 150 // Only update diagram every 150ms during streaming
|
const STREAMING_DEBOUNCE_MS = 150 // Only update diagram every 150ms during streaming
|
||||||
|
// Refs for edit_diagram streaming
|
||||||
|
const pendingEditRef = useRef<{
|
||||||
|
operations: DiagramOperation[]
|
||||||
|
toolCallId: string
|
||||||
|
} | null>(null)
|
||||||
|
const editDebounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
@@ -286,15 +314,12 @@ 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) {
|
||||||
@@ -310,7 +335,6 @@ export function ChatMessageDisplay({
|
|||||||
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,14 +344,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
|
||||||
@@ -364,9 +384,6 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
} else {
|
|
||||||
console.timeEnd("perf:handleDisplayChart")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[chartXML, onDisplayChart],
|
[chartXML, onDisplayChart],
|
||||||
@@ -385,11 +402,6 @@ export function ChatMessageDisplay({
|
|||||||
}, [editingMessageId])
|
}, [editingMessageId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.time("perf:message-display-useEffect")
|
|
||||||
let processedCount = 0
|
|
||||||
let skippedCount = 0
|
|
||||||
let debouncedCount = 0
|
|
||||||
|
|
||||||
// Only process the last message for streaming performance
|
// Only process the last message for streaming performance
|
||||||
// Previous messages are already processed and won't change
|
// Previous messages are already processed and won't change
|
||||||
const messagesToProcess =
|
const messagesToProcess =
|
||||||
@@ -419,7 +431,6 @@ export function ChatMessageDisplay({
|
|||||||
const lastXml =
|
const lastXml =
|
||||||
lastProcessedXmlRef.current.get(toolCallId)
|
lastProcessedXmlRef.current.get(toolCallId)
|
||||||
if (lastXml === xml) {
|
if (lastXml === xml) {
|
||||||
skippedCount++
|
|
||||||
return // Skip redundant processing
|
return // Skip redundant processing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,9 +450,6 @@ export function ChatMessageDisplay({
|
|||||||
debounceTimeoutRef.current = null
|
debounceTimeoutRef.current = null
|
||||||
pendingXmlRef.current = null
|
pendingXmlRef.current = null
|
||||||
if (pendingXml) {
|
if (pendingXml) {
|
||||||
console.log(
|
|
||||||
"perf:debounced-handleDisplayChart executing",
|
|
||||||
)
|
|
||||||
handleDisplayChart(
|
handleDisplayChart(
|
||||||
pendingXml,
|
pendingXml,
|
||||||
false,
|
false,
|
||||||
@@ -455,7 +463,6 @@ export function ChatMessageDisplay({
|
|||||||
STREAMING_DEBOUNCE_MS,
|
STREAMING_DEBOUNCE_MS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
debouncedCount++
|
|
||||||
} else if (
|
} else if (
|
||||||
state === "output-available" &&
|
state === "output-available" &&
|
||||||
!processedToolCalls.current.has(toolCallId)
|
!processedToolCalls.current.has(toolCallId)
|
||||||
@@ -471,17 +478,129 @@ export function ChatMessageDisplay({
|
|||||||
processedToolCalls.current.add(toolCallId)
|
processedToolCalls.current.add(toolCallId)
|
||||||
// Clean up the ref entry - tool is complete, no longer needed
|
// Clean up the ref entry - tool is complete, no longer needed
|
||||||
lastProcessedXmlRef.current.delete(toolCallId)
|
lastProcessedXmlRef.current.delete(toolCallId)
|
||||||
processedCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
// Handle edit_diagram streaming - apply operations incrementally for preview
|
||||||
}
|
// Uses shared editDiagramOriginalXmlRef to coordinate with tool handler
|
||||||
})
|
if (
|
||||||
console.log(
|
part.type === "tool-edit_diagram" &&
|
||||||
`perf:message-display-useEffect processed=${processedCount} skipped=${skippedCount} debounced=${debouncedCount}`,
|
input?.operations
|
||||||
|
) {
|
||||||
|
const completeOps = getCompleteOperations(
|
||||||
|
input.operations as DiagramOperation[],
|
||||||
)
|
)
|
||||||
console.timeEnd("perf:message-display-useEffect")
|
|
||||||
|
if (completeOps.length === 0) return
|
||||||
|
|
||||||
|
// Capture original XML when streaming starts (store in shared ref)
|
||||||
|
if (
|
||||||
|
!editDiagramOriginalXmlRef.current.has(
|
||||||
|
toolCallId,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!chartXML) {
|
||||||
|
console.warn(
|
||||||
|
"[edit_diagram streaming] No chart XML available",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
editDiagramOriginalXmlRef.current.set(
|
||||||
|
toolCallId,
|
||||||
|
chartXML,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalXml =
|
||||||
|
editDiagramOriginalXmlRef.current.get(
|
||||||
|
toolCallId,
|
||||||
|
)
|
||||||
|
if (!originalXml) return
|
||||||
|
|
||||||
|
// Skip if no change from last processed state
|
||||||
|
const lastCount = lastProcessedXmlRef.current.get(
|
||||||
|
toolCallId + "-opCount",
|
||||||
|
)
|
||||||
|
if (lastCount === String(completeOps.length)) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
state === "input-streaming" ||
|
||||||
|
state === "input-available"
|
||||||
|
) {
|
||||||
|
// Queue the operations for debounced processing
|
||||||
|
pendingEditRef.current = {
|
||||||
|
operations: completeOps,
|
||||||
|
toolCallId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editDebounceTimeoutRef.current) {
|
||||||
|
editDebounceTimeoutRef.current = setTimeout(
|
||||||
|
() => {
|
||||||
|
const pending =
|
||||||
|
pendingEditRef.current
|
||||||
|
editDebounceTimeoutRef.current =
|
||||||
|
null
|
||||||
|
pendingEditRef.current = null
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
const origXml =
|
||||||
|
editDiagramOriginalXmlRef.current.get(
|
||||||
|
pending.toolCallId,
|
||||||
|
)
|
||||||
|
if (!origXml) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
result: editedXml,
|
||||||
|
} = applyDiagramOperations(
|
||||||
|
origXml,
|
||||||
|
pending.operations,
|
||||||
|
)
|
||||||
|
handleDisplayChart(
|
||||||
|
editedXml,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
lastProcessedXmlRef.current.set(
|
||||||
|
pending.toolCallId +
|
||||||
|
"-opCount",
|
||||||
|
String(
|
||||||
|
pending.operations
|
||||||
|
.length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`[edit_diagram streaming] Operation failed:`,
|
||||||
|
e instanceof Error
|
||||||
|
? e.message
|
||||||
|
: e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
STREAMING_DEBOUNCE_MS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
state === "output-available" &&
|
||||||
|
!processedToolCalls.current.has(toolCallId)
|
||||||
|
) {
|
||||||
|
// Final state - cleanup streaming refs (tool handler does final application)
|
||||||
|
if (editDebounceTimeoutRef.current) {
|
||||||
|
clearTimeout(editDebounceTimeoutRef.current)
|
||||||
|
editDebounceTimeoutRef.current = null
|
||||||
|
}
|
||||||
|
lastProcessedXmlRef.current.delete(
|
||||||
|
toolCallId + "-opCount",
|
||||||
|
)
|
||||||
|
processedToolCalls.current.add(toolCallId)
|
||||||
|
// Note: Don't delete editDiagramOriginalXmlRef here - tool handler needs it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Cleanup: clear any pending debounce timeout on unmount
|
// Cleanup: clear any pending debounce timeout on unmount
|
||||||
return () => {
|
return () => {
|
||||||
@@ -489,8 +608,12 @@ export function ChatMessageDisplay({
|
|||||||
clearTimeout(debounceTimeoutRef.current)
|
clearTimeout(debounceTimeoutRef.current)
|
||||||
debounceTimeoutRef.current = null
|
debounceTimeoutRef.current = null
|
||||||
}
|
}
|
||||||
|
if (editDebounceTimeoutRef.current) {
|
||||||
|
clearTimeout(editDebounceTimeoutRef.current)
|
||||||
|
editDebounceTimeoutRef.current = null
|
||||||
}
|
}
|
||||||
}, [messages, handleDisplayChart])
|
}
|
||||||
|
}, [messages, handleDisplayChart, chartXML])
|
||||||
|
|
||||||
const renderToolPart = (part: ToolPartLike) => {
|
const renderToolPart = (part: ToolPartLike) => {
|
||||||
const callId = part.toolCallId
|
const callId = part.toolCallId
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ 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())
|
||||||
|
|
||||||
|
// Store original XML for edit_diagram streaming - shared between streaming preview and tool handler
|
||||||
|
// Key: toolCallId, Value: original XML before any operations applied
|
||||||
|
const editDiagramOriginalXmlRef = useRef<Map<string, string>>(new Map())
|
||||||
|
|
||||||
// Debounce timeout for localStorage writes (prevents blocking during streaming)
|
// Debounce timeout for localStorage writes (prevents blocking during streaming)
|
||||||
const localStorageDebounceRef = useRef<ReturnType<
|
const localStorageDebounceRef = useRef<ReturnType<
|
||||||
typeof setTimeout
|
typeof setTimeout
|
||||||
@@ -333,14 +337,23 @@ ${finalXml}
|
|||||||
|
|
||||||
let currentXml = ""
|
let currentXml = ""
|
||||||
try {
|
try {
|
||||||
// Use chartXML from ref directly - more reliable than export
|
// Use the original XML captured during streaming (shared with chat-message-display)
|
||||||
|
// This ensures we apply operations to the same base XML that streaming used
|
||||||
|
const originalXml = editDiagramOriginalXmlRef.current.get(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
|
if (originalXml) {
|
||||||
|
currentXml = originalXml
|
||||||
|
} else {
|
||||||
|
// Fallback: use chartXML from ref if streaming didn't capture original
|
||||||
const cachedXML = chartXMLRef.current
|
const cachedXML = chartXMLRef.current
|
||||||
if (cachedXML) {
|
if (cachedXML) {
|
||||||
currentXml = cachedXML
|
currentXml = cachedXML
|
||||||
} else {
|
} else {
|
||||||
// Fallback to export only if no cached XML
|
// Last resort: export from iframe
|
||||||
currentXml = await onFetchChart(false)
|
currentXml = await onFetchChart(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { applyDiagramOperations } = await import(
|
const { applyDiagramOperations } = await import(
|
||||||
"@/lib/utils"
|
"@/lib/utils"
|
||||||
@@ -370,6 +383,10 @@ ${currentXml}
|
|||||||
|
|
||||||
Please check the cell IDs and retry.`,
|
Please check the cell IDs and retry.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +410,10 @@ ${currentXml}
|
|||||||
|
|
||||||
Please fix the operations to avoid structural issues.`,
|
Please fix the operations to avoid structural issues.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onExport()
|
onExport()
|
||||||
@@ -401,6 +422,10 @@ Please fix the operations to avoid structural issues.`,
|
|||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: `Successfully applied ${operations.length} operation(s) to the diagram.`,
|
output: `Successfully applied ${operations.length} operation(s) to the diagram.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[edit_diagram] Failed:", error)
|
console.error("[edit_diagram] Failed:", error)
|
||||||
|
|
||||||
@@ -420,6 +445,10 @@ ${currentXml || "No XML available"}
|
|||||||
|
|
||||||
Please check cell IDs and retry, or use display_diagram to regenerate.`,
|
Please check cell IDs and retry, or use display_diagram to regenerate.`,
|
||||||
})
|
})
|
||||||
|
// Clean up the shared original XML ref even on error
|
||||||
|
editDiagramOriginalXmlRef.current.delete(
|
||||||
|
toolCall.toolCallId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (toolCall.toolName === "append_diagram") {
|
} else if (toolCall.toolName === "append_diagram") {
|
||||||
const { xml } = toolCall.input as { xml: string }
|
const { xml } = toolCall.input as { xml: string }
|
||||||
@@ -508,6 +537,32 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
// Silence access code error in console since it's handled by UI
|
// Silence access code error in console since it's handled by UI
|
||||||
if (!error.message.includes("Invalid or missing access code")) {
|
if (!error.message.includes("Invalid or missing access code")) {
|
||||||
console.error("Chat error:", error)
|
console.error("Chat error:", error)
|
||||||
|
// Debug: Log messages structure when error occurs
|
||||||
|
console.log("[onError] messages count:", messages.length)
|
||||||
|
messages.forEach((msg, idx) => {
|
||||||
|
console.log(`[onError] Message ${idx}:`, {
|
||||||
|
role: msg.role,
|
||||||
|
partsCount: msg.parts?.length,
|
||||||
|
})
|
||||||
|
if (msg.parts) {
|
||||||
|
msg.parts.forEach((part: any, partIdx: number) => {
|
||||||
|
console.log(
|
||||||
|
`[onError] Part ${partIdx}:`,
|
||||||
|
JSON.stringify({
|
||||||
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
|
hasInput: !!part.input,
|
||||||
|
inputType: typeof part.input,
|
||||||
|
inputKeys:
|
||||||
|
part.input &&
|
||||||
|
typeof part.input === "object"
|
||||||
|
? Object.keys(part.input)
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate technical errors into user-friendly messages
|
// Translate technical errors into user-friendly messages
|
||||||
@@ -723,12 +778,10 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
// Debounce: save after 1 second of no changes
|
// Debounce: save after 1 second of no changes
|
||||||
localStorageDebounceRef.current = setTimeout(() => {
|
localStorageDebounceRef.current = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
console.time("perf:localStorage-messages")
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_MESSAGES_KEY,
|
STORAGE_MESSAGES_KEY,
|
||||||
JSON.stringify(messages),
|
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)
|
||||||
}
|
}
|
||||||
@@ -754,9 +807,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
|
|
||||||
// Debounce: save after 1 second of no changes
|
// Debounce: save after 1 second of no changes
|
||||||
xmlStorageDebounceRef.current = setTimeout(() => {
|
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)
|
}, LOCAL_STORAGE_DEBOUNCE_MS)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -769,13 +820,11 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
// 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:",
|
||||||
@@ -1326,6 +1375,7 @@ Continue from EXACTLY where you stopped.`,
|
|||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
setFiles={handleFileChange}
|
setFiles={handleFileChange}
|
||||||
processedToolCallsRef={processedToolCallsRef}
|
processedToolCallsRef={processedToolCallsRef}
|
||||||
|
editDiagramOriginalXmlRef={editDiagramOriginalXmlRef}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
onRegenerate={handleRegenerate}
|
onRegenerate={handleRegenerate}
|
||||||
status={status}
|
status={status}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
lib/utils.ts
28
lib/utils.ts
@@ -743,8 +743,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(
|
||||||
@@ -754,18 +752,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.`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,7 +764,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.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -788,16 +777,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) {
|
if (dupAttrError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return dupAttrError
|
return dupAttrError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,33 +792,25 @@ 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) {
|
if (dupIdError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return dupIdError
|
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) {
|
if (tagMismatchError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return tagMismatchError
|
return tagMismatchError
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Check invalid character references
|
// 6. Check invalid character references
|
||||||
const charRefError = checkCharacterReferences(xml)
|
const charRefError = checkCharacterReferences(xml)
|
||||||
if (charRefError) {
|
if (charRefError) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return charRefError
|
return charRefError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,7 +819,6 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -850,24 +826,20 @@ export function validateMxCellStructure(xml: string): string | null {
|
|||||||
// 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) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return entityError
|
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) {
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return nestedCellError
|
return nestedCellError
|
||||||
}
|
}
|
||||||
|
|
||||||
console.timeEnd("perf:validateMxCellStructure")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user