fix: handle malformed XML from DeepSeek gracefully (#235)

* fix: handle malformed XML from DeepSeek gracefully

Add early XML validation with parsererror check before calling
replaceNodes to prevent application crashes when AI models
generate malformed XML with unescaped special characters.

Changes:
- Add toast import from sonner
- Parse and validate XML before processing
- Add parsererror detection to catch malformed XML early
- Wrap replaceNodes in try-catch for additional safety
- Add user-friendly toast notifications for all error cases
- Change console.log to console.error for validation failures

Fixes #220 #230 #231

* fix: prevent toast spam during streaming and merge silent failure fixes

- Only show error toasts after streaming completes (not during partial updates)
- Track which tool calls have shown errors to prevent duplicate toasts
- Merge clipboard copy error handling from PR #236
- Merge feedback submission error handling from PR #237
- Add comments explaining streaming vs completion behavior

* refactor: simplify toast deduplication with boolean flag

Based on code review feedback, simplified the approach from tracking
per-tool-call IDs in a Set to using a single boolean flag.

Changes:
- Replaced Set<string> with boolean ref for toast tracking
- Removed toolCallId and showToast parameters from handleDisplayChart
- Reset flag when streaming starts (simpler mental model)
- Same behavior: one toast per streaming session, no spam

Benefits:
- Fewer concepts (1 boolean vs Set + 2 parameters)
- No manual coordination between call sites
- Easier to understand and maintain
- ~15 fewer lines of tracking logic

* fix: only show toast for final malformed XML, not during streaming

- Remove errorToastShownRef tracking (no longer needed)
- Add showToast parameter to handleDisplayChart (default false)
- Pass false during streaming (XML may be incomplete)
- Pass true at completion (show toast if final XML is malformed)
- Simpler and more explicit error handling
This commit is contained in:
Dayuan Jiang
2025-12-12 14:52:25 +09:00
committed by GitHub
parent c2c65973f9
commit aa15519fba

View File

@@ -282,10 +282,29 @@ export function ChatMessageDisplay({
} }
const handleDisplayChart = useCallback( const handleDisplayChart = useCallback(
(xml: string) => { (xml: string, showToast = false) => {
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
const parser = new DOMParser()
const testDoc = parser.parseFromString(convertedXml, "text/xml")
const parseError = testDoc.querySelector("parsererror")
if (parseError) {
console.error(
"[ChatMessageDisplay] Malformed XML detected - skipping update",
)
// Only show toast if this is the final XML (not during streaming)
if (showToast) {
toast.error(
"AI generated invalid diagram XML. Please try regenerating.",
)
}
return // Skip this update
}
try {
// If chartXML is empty, create a default mxfile structure to use with replaceNodes // If chartXML is empty, create a default mxfile structure to use with replaceNodes
// This ensures the XML is properly wrapped in mxfile/diagram/mxGraphModel format // This ensures the XML is properly wrapped in mxfile/diagram/mxGraphModel format
const baseXML = const baseXML =
@@ -299,10 +318,28 @@ export function ChatMessageDisplay({
// Skip validation in loadDiagram since we already validated above // Skip validation in loadDiagram since we already validated above
onDisplayChart(replacedXML, true) onDisplayChart(replacedXML, true)
} else { } else {
console.log( console.error(
"[ChatMessageDisplay] XML validation failed:", "[ChatMessageDisplay] XML validation failed:",
validationError, validationError,
) )
// Only show toast if this is the final XML (not during streaming)
if (showToast) {
toast.error(
"Diagram validation failed. Please try regenerating.",
)
}
}
} catch (error) {
console.error(
"[ChatMessageDisplay] Error processing XML:",
error,
)
// Only show toast if this is the final XML (not during streaming)
if (showToast) {
toast.error(
"Failed to process diagram. Please try regenerating.",
)
}
} }
} }
}, },
@@ -345,12 +382,14 @@ export function ChatMessageDisplay({
state === "input-streaming" || state === "input-streaming" ||
state === "input-available" state === "input-available"
) { ) {
handleDisplayChart(xml) // During streaming, don't show toast (XML may be incomplete)
handleDisplayChart(xml, false)
} else if ( } else if (
state === "output-available" && state === "output-available" &&
!processedToolCalls.current.has(toolCallId) !processedToolCalls.current.has(toolCallId)
) { ) {
handleDisplayChart(xml) // Show toast only if final XML is malformed
handleDisplayChart(xml, true)
processedToolCalls.current.add(toolCallId) processedToolCalls.current.add(toolCallId)
} }
} }