Compare commits

...

6 Commits

Author SHA1 Message Date
dayuan.jiang
0fbd3fe842 docs: fix append_diagram instructions for consistency
- Change 'Do NOT include' to 'Do NOT start with' (clearer intent)
- Add <mxCell id="0"> to prohibited start patterns
- Change 'closing tags </root></mxGraphModel>' to just '</root>' (wrapWithMxFile handles the rest)
2025-12-14 12:33:47 +09:00
dayuan.jiang
9068f608bf refactor: remove debug logs and simplify truncation state
- Remove all debug console.log statements
- Remove isContinuationModeRef, derive from partialXmlRef.current.length > 0
2025-12-14 11:15:26 +09:00
dayuan.jiang
43139a5ef0 fix: show friendly message and yellow badge for truncated output
- Add yellow 'Truncated' badge in UI instead of red 'Error' when XML is incomplete
- Show friendly error message for toolUse.input is invalid errors
- Built on top of append_diagram continuation feature
2025-12-14 10:47:18 +09:00
dayuan.jiang
62e07f5f9c feat: add append_diagram tool for truncation continuation
When LLM output hits maxOutputTokens mid-generation, instead of
failing with an error loop, the system now:

1. Detects truncation (missing </root> in XML)
2. Stores partial XML and tells LLM to use new append_diagram tool
3. LLM continues generating from where it stopped
4. Fragments are accumulated until XML is complete
5. Server limits to 5 steps via stepCountIs(5)

Key changes:
- Add append_diagram tool definition in route.ts
- Add append_diagram handler in chat-panel.tsx
- Track continuation mode separately from error mode
- Continuation mode has unlimited retries (not counted against limit)
- Error mode still limited to MAX_AUTO_RETRY_COUNT (1)
- Update system prompts to document append_diagram tool
2025-12-14 09:38:47 +09:00
Dayuan Jiang
b33e09be05 feat: add XML auto-fix with refined validation logic (#247)
* feat: add XML auto-fix and improve validator accuracy

- Add autoFixXml() to automatically repair common XML issues:
  - CDATA wrapper removal
  - Duplicate attribute removal
  - Unescaped & and < character escaping
  - Invalid entity reference fixing
  - Unclosed tag completion
  - Nested mxCell flattening
  - Duplicate ID renaming

- Improve validateMxCellStructure() with DOM + regex approach:
  - Use DOMParser for syntax error detection (94% recall)
  - Add regex checks for edge cases
  - Stateful parser for handling > in attribute values

- Integrate validateAndFixXml() in chat-message-display and diagram-context
  - Auto-repair invalid XML before loading
  - Log fixes applied for debugging

Metrics: 99.77% accuracy, 94.06% recall, 94.4% auto-fix success rate

* fix: improve XML auto-fix from 58.7% to 99% fix rate

Key improvements:
- Reorder CDATA removal to run before text-before-root check (+35 cases)
- Implement Gemini's backslash-quote fix with regex backreference
  Handles attr="value", value="text\"inner\"more", and mixed patterns
- Add aggressive drop-broken-cells fix for unfixable mxCell elements
  Iteratively removes cells causing DOM parse errors (up to 50)

Results on 9,411 XML dataset:
- 206 invalid XMLs detected
- 204 successfully fixed (99.0% fix rate)
- 2 unfixable (completely broken, need regeneration)

* refactor: extract XML validation/fix helpers and add constants

- Add constants: MAX_XML_SIZE (1MB), MAX_DROP_ITERATIONS (10), STRUCTURAL_ATTRS, VALID_ENTITIES
- Extract parseXmlTags helper for shared tag parsing logic
- Extract validation helpers: checkDuplicateAttributes, checkDuplicateIds, checkTagMismatches, checkCharacterReferences, checkEntityReferences, checkNestedMxCells
- Simplify validateMxCellStructure from ~200 lines to ~55 lines
- Add logging to empty catch block in DOMParser section
- Add size warning for large XML documents
- Remove unused variables (isSelfClose, duplicate idPattern)

* fix: improve XML auto-fix with malformed quote pattern

- Fix =&quot;...&quot; pattern where &quot; was used as delimiter instead of actual quotes
- Common in dashPattern attributes like dashPattern=&quot;1 1;&quot;
2025-12-13 23:31:01 +09:00
Dayuan Jiang
987dc9f026 fix: add configurable MAX_OUTPUT_TOKENS to prevent truncation (#251)
- Add MAX_OUTPUT_TOKENS env var (fixes output truncation with Bedrock)
- Remove redundant fixToolCallInputs function
- Remove jsonrepair dependency
- Consolidate duplicate lastMessage/userInputText variables
2025-12-13 23:28:41 +09:00
7 changed files with 623 additions and 378 deletions

View File

@@ -3,10 +3,12 @@ import {
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
InvalidToolInputError,
LoadAPIKeyError,
stepCountIs,
streamText,
} from "ai"
import { jsonrepair } from "jsonrepair"
import { z } from "zod"
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
import { findCachedResponse } from "@/lib/cached-responses"
@@ -95,35 +97,6 @@ function replaceHistoricalToolInputs(messages: any[]): any[] {
})
}
// Helper function to fix tool call inputs for Bedrock API
// Bedrock requires toolUse.input to be a JSON object, not a string
function fixToolCallInputs(messages: any[]): any[] {
return messages.map((msg) => {
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
return msg
}
const fixedContent = msg.content.map((part: any) => {
if (part.type === "tool-call") {
if (typeof part.input === "string") {
try {
const parsed = JSON.parse(part.input)
return { ...part, input: parsed }
} catch {
// If parsing fails, wrap the string in an object
return { ...part, input: { rawInput: part.input } }
}
}
// Input is already an object, but verify it's not null/undefined
if (part.input === null || part.input === undefined) {
return { ...part, input: {} }
}
}
return part
})
return { ...msg, content: fixedContent }
})
}
// Helper function to create cached stream response
function createCachedStreamResponse(xml: string): Response {
const toolCallId = `cached-${Date.now()}`
@@ -186,9 +159,9 @@ async function handleChatRequest(req: Request): Promise<Response> {
: undefined
// Extract user input text for Langfuse trace
const currentMessage = messages[messages.length - 1]
const lastMessage = messages[messages.length - 1]
const userInputText =
currentMessage?.parts?.find((p: any) => p.type === "text")?.text || ""
lastMessage?.parts?.find((p: any) => p.type === "text")?.text || ""
// Update Langfuse trace with input, session, and user
setTraceInput({
@@ -242,12 +215,6 @@ async function handleChatRequest(req: Request): Promise<Response> {
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
const systemMessage = getSystemPrompt(modelId)
const lastMessage = messages[messages.length - 1]
// Extract text from the last message parts
const lastMessageText =
lastMessage.parts?.find((part: any) => part.type === "text")?.text || ""
// Extract file parts (images) from the last message
const fileParts =
lastMessage.parts?.filter((part: any) => part.type === "file") || []
@@ -255,22 +222,19 @@ async function handleChatRequest(req: Request): Promise<Response> {
// User input only - XML is now in a separate cached system message
const formattedUserInput = `User input:
"""md
${lastMessageText}
${userInputText}
"""`
// Convert UIMessages to ModelMessages and add system message
const modelMessages = convertToModelMessages(messages)
// Fix tool call inputs for Bedrock API (requires JSON objects, not strings)
const fixedMessages = fixToolCallInputs(modelMessages)
// Replace historical tool call XML with placeholders to reduce tokens
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
const enableHistoryReplace =
process.env.ENABLE_HISTORY_XML_REPLACE === "true"
const placeholderMessages = enableHistoryReplace
? replaceHistoricalToolInputs(fixedMessages)
: fixedMessages
? replaceHistoricalToolInputs(modelMessages)
: modelMessages
// Filter out messages with empty content arrays (Bedrock API rejects these)
// This is a safety measure - ideally convertToModelMessages should handle all cases
@@ -354,7 +318,35 @@ ${lastMessageText}
const result = streamText({
model,
...(process.env.MAX_OUTPUT_TOKENS && {
maxOutputTokens: parseInt(process.env.MAX_OUTPUT_TOKENS, 10),
}),
stopWhen: stepCountIs(5),
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
experimental_repairToolCall: async ({ toolCall, error }) => {
// Only attempt repair for invalid tool input (broken JSON from truncation)
if (
error instanceof InvalidToolInputError ||
error.name === "AI_InvalidToolInputError"
) {
try {
// Use jsonrepair to fix truncated JSON
const repairedInput = jsonrepair(toolCall.input)
console.log(
`[repairToolCall] Repaired truncated JSON for tool: ${toolCall.toolName}`,
)
return { ...toolCall, input: repairedInput }
} catch (repairError) {
console.warn(
`[repairToolCall] Failed to repair JSON for tool: ${toolCall.toolName}`,
repairError,
)
return null
}
}
// Don't attempt to repair other errors (like NoSuchToolError)
return null
},
messages: allMessages,
...(providerOptions && { providerOptions }), // This now includes all reasoning configs
...(headers && { headers }),
@@ -365,32 +357,6 @@ ${lastMessageText}
userId,
}),
}),
// Repair malformed tool calls (model sometimes generates invalid JSON with unescaped quotes)
experimental_repairToolCall: async ({ toolCall }) => {
// The toolCall.input contains the raw JSON string that failed to parse
const rawJson =
typeof toolCall.input === "string" ? toolCall.input : null
if (rawJson) {
try {
// Fix unescaped quotes: x="520" should be x=\"520\"
const fixed = rawJson.replace(
/([a-zA-Z])="(\d+)"/g,
'$1=\\"$2\\"',
)
const parsed = JSON.parse(fixed)
return {
type: "tool-call" as const,
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: JSON.stringify(parsed),
}
} catch {
// Repair failed, return null
}
}
return null
},
onFinish: ({ text, usage }) => {
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
setTraceOutput(text, {
@@ -472,6 +438,26 @@ IMPORTANT: Keep edits concise:
),
}),
},
append_diagram: {
description: `Continue generating diagram XML when previous display_diagram output was truncated due to length limits.
WHEN TO USE: Only call this tool after display_diagram was truncated (you'll see an error message about truncation).
CRITICAL INSTRUCTIONS:
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
3. End with the closing </root> tag to complete the diagram
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.`,
inputSchema: z.object({
xml: z
.string()
.describe(
"Continuation XML fragment to append (NO wrapper tags)",
),
}),
},
},
...(process.env.TEMPERATURE !== undefined && {
temperature: parseFloat(process.env.TEMPERATURE),
@@ -496,6 +482,7 @@ IMPORTANT: Keep edits concise:
return {
inputTokens: totalInputTokens,
outputTokens: usage.outputTokens ?? 0,
finishReason: (part as any).finishReason,
}
}
return undefined

View File

@@ -451,11 +451,24 @@ export function ChatMessageDisplay({
Complete
</span>
)}
{state === "output-error" && (
<span className="text-xs font-medium text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
Error
</span>
)}
{state === "output-error" &&
(() => {
// Check if this is a truncation (incomplete XML) vs real error
const isTruncated =
(toolName === "display_diagram" ||
toolName === "append_diagram") &&
(!input?.xml ||
!input.xml.includes("</root>"))
return isTruncated ? (
<span className="text-xs font-medium text-yellow-600 bg-yellow-50 px-2 py-0.5 rounded-full">
Truncated
</span>
) : (
<span className="text-xs font-medium text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
Error
</span>
)
})()}
{input && Object.keys(input).length > 0 && (
<button
type="button"
@@ -488,11 +501,23 @@ export function ChatMessageDisplay({
) : null}
</div>
)}
{output && state === "output-error" && (
<div className="px-4 py-3 border-t border-border/40 text-sm text-red-600">
{output}
</div>
)}
{output &&
state === "output-error" &&
(() => {
const isTruncated =
(toolName === "display_diagram" ||
toolName === "append_diagram") &&
(!input?.xml || !input.xml.includes("</root>"))
return (
<div
className={`px-4 py-3 border-t border-border/40 text-sm ${isTruncated ? "text-yellow-600" : "text-red-600"}`}
>
{isTruncated
? "Output truncated due to length limits. Try a simpler request or increase the maxOutputLength."
: output}
</div>
)
})()}
</div>
)
}

View File

@@ -63,11 +63,11 @@ interface ChatPanelProps {
// Constants for tool states
const TOOL_ERROR_STATE = "output-error" as const
const DEBUG = process.env.NODE_ENV === "development"
const MAX_AUTO_RETRY_COUNT = 3
const MAX_AUTO_RETRY_COUNT = 1
/**
* Check if auto-resubmit should happen based on tool errors.
* Does NOT handle retry count or quota - those are handled by the caller.
* Only checks the LAST tool part (most recent tool call), not all tool parts.
*/
function hasToolErrors(messages: ChatMessage[]): boolean {
const lastMessage = messages[messages.length - 1]
@@ -84,7 +84,8 @@ function hasToolErrors(messages: ChatMessage[]): boolean {
return false
}
return toolParts.some((part) => part.state === TOOL_ERROR_STATE)
const lastToolPart = toolParts[toolParts.length - 1]
return lastToolPart?.state === TOOL_ERROR_STATE
}
export default function ChatPanel({
@@ -192,6 +193,10 @@ export default function ChatPanel({
// Ref to track consecutive auto-retry count (reset on user action)
const autoRetryCountRef = useRef(0)
// Ref to accumulate partial XML when output is truncated due to maxOutputTokens
// When partialXmlRef.current.length > 0, we're in continuation mode
const partialXmlRef = useRef<string>("")
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
const processedToolCallsRef = useRef<Set<string>>(new Set())
@@ -216,14 +221,43 @@ export default function ChatPanel({
if (toolCall.toolName === "display_diagram") {
const { xml } = toolCall.input as { xml: string }
if (DEBUG) {
console.log(
`[display_diagram] Received XML length: ${xml.length}`,
)
// Check if XML is truncated (missing </root> indicates incomplete output)
const isTruncated =
!xml.includes("</root>") && !xml.trim().endsWith("/>")
if (isTruncated) {
// Store the partial XML for continuation via append_diagram
partialXmlRef.current = xml
// Tell LLM to use append_diagram to continue
const partialEnding = partialXmlRef.current.slice(-500)
addToolOutput({
tool: "display_diagram",
toolCallId: toolCall.toolCallId,
state: "output-error",
errorText: `Output was truncated due to length limits. Use the append_diagram tool to continue.
Your output ended with:
\`\`\`
${partialEnding}
\`\`\`
NEXT STEP: Call append_diagram with the continuation XML.
- Do NOT start with <mxGraphModel>, <root>, or <mxCell id="0"> (they already exist)
- Start from EXACTLY where you stopped
- End with the closing </root> tag to complete the diagram`,
})
return
}
// Complete XML received - use it directly
// (continuation is now handled via append_diagram tool)
const finalXml = xml
partialXmlRef.current = "" // Reset any partial from previous truncation
// Wrap raw XML with full mxfile structure for draw.io
const fullXml = wrapWithMxFile(xml)
const fullXml = wrapWithMxFile(finalXml)
// loadDiagram validates and returns error if invalid
const validationError = onDisplayChart(fullXml)
@@ -249,7 +283,7 @@ Please fix the XML issues and call display_diagram again with corrected XML.
Your failed XML:
\`\`\`xml
${xml}
${finalXml}
\`\`\``,
})
} else {
@@ -277,27 +311,13 @@ ${xml}
let currentXml = ""
try {
console.log("[edit_diagram] Starting...")
// Use chartXML from ref directly - more reliable than export
// especially on Vercel where DrawIO iframe may have latency issues
// Using ref to avoid stale closure in callback
const cachedXML = chartXMLRef.current
if (cachedXML) {
currentXml = cachedXML
console.log(
"[edit_diagram] Using cached chartXML, length:",
currentXml.length,
)
} else {
// Fallback to export only if no cached XML
console.log(
"[edit_diagram] No cached XML, fetching from DrawIO...",
)
currentXml = await onFetchChart(false)
console.log(
"[edit_diagram] Got XML from export, length:",
currentXml.length,
)
}
const { replaceXMLParts } = await import("@/lib/utils")
@@ -331,7 +351,6 @@ Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid ref
toolCallId: toolCall.toolCallId,
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
})
console.log("[edit_diagram] Success")
} catch (error) {
console.error("[edit_diagram] Failed:", error)
@@ -353,6 +372,83 @@ ${currentXml || "No XML available"}
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
})
}
} else if (toolCall.toolName === "append_diagram") {
const { xml } = toolCall.input as { xml: string }
// Detect if LLM incorrectly started fresh instead of continuing
const isFreshStart =
xml.trim().startsWith("<mxGraphModel") ||
xml.trim().startsWith("<root") ||
xml.trim().startsWith('<mxCell id="0"')
if (isFreshStart) {
addToolOutput({
tool: "append_diagram",
toolCallId: toolCall.toolCallId,
state: "output-error",
errorText: `ERROR: You started fresh with wrapper tags. Do NOT include <mxGraphModel>, <root>, or <mxCell id="0">.
Continue from EXACTLY where the partial ended:
\`\`\`
${partialXmlRef.current.slice(-500)}
\`\`\`
Start your continuation with the NEXT character after where it stopped.`,
})
return
}
// Append to accumulated XML
partialXmlRef.current += xml
// Check if XML is now complete
const isComplete = partialXmlRef.current.includes("</root>")
if (isComplete) {
// Wrap and display the complete diagram
const finalXml = partialXmlRef.current
partialXmlRef.current = "" // Reset
const fullXml = wrapWithMxFile(finalXml)
const validationError = onDisplayChart(fullXml)
if (validationError) {
addToolOutput({
tool: "append_diagram",
toolCallId: toolCall.toolCallId,
state: "output-error",
errorText: `Validation error after assembly: ${validationError}
Assembled XML:
\`\`\`xml
${finalXml.substring(0, 2000)}...
\`\`\`
Please use display_diagram with corrected XML.`,
})
} else {
addToolOutput({
tool: "append_diagram",
toolCallId: toolCall.toolCallId,
output: "Diagram assembly complete and displayed successfully.",
})
}
} else {
// Still incomplete - signal to continue
addToolOutput({
tool: "append_diagram",
toolCallId: toolCall.toolCallId,
state: "output-error",
errorText: `XML still incomplete (missing </root>). Call append_diagram again to continue.
Current ending:
\`\`\`
${partialXmlRef.current.slice(-500)}
\`\`\`
Continue from EXACTLY where you stopped.`,
})
}
}
},
onError: (error) => {
@@ -371,6 +467,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
friendlyMessage = "Network error. Please check your connection."
}
// Truncated tool input error (model output limit too low)
if (friendlyMessage.includes("toolUse.input is invalid")) {
friendlyMessage =
"Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength."
}
// Translate image not supported error
if (friendlyMessage.includes("image content block")) {
friendlyMessage = "This model doesn't support image input."
@@ -398,6 +500,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
const metadata = message?.metadata as
| Record<string, unknown>
| undefined
if (metadata) {
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
const inputTokens = Number.isFinite(metadata.inputTokens)
@@ -414,65 +517,55 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
}
},
sendAutomaticallyWhen: ({ messages }) => {
const isInContinuationMode = partialXmlRef.current.length > 0
const shouldRetry = hasToolErrors(
messages as unknown as ChatMessage[],
)
if (!shouldRetry) {
// No error, reset retry count
// No error, reset retry count and clear state
autoRetryCountRef.current = 0
if (DEBUG) {
console.log("[sendAutomaticallyWhen] No errors, stopping")
}
partialXmlRef.current = ""
return false
}
// Check retry count limit
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
if (DEBUG) {
console.log(
`[sendAutomaticallyWhen] Max retry count (${MAX_AUTO_RETRY_COUNT}) reached, stopping`,
// Continuation mode: unlimited retries (truncation continuation, not real errors)
// Server limits to 5 steps via stepCountIs(5)
if (isInContinuationMode) {
// Don't count against retry limit for continuation
// Quota checks still apply below
} else {
// Regular error: check retry count limit
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
toast.error(
`Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`,
)
autoRetryCountRef.current = 0
partialXmlRef.current = ""
return false
}
toast.error(
`Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`,
)
autoRetryCountRef.current = 0
return false
// Increment retry count for actual errors
autoRetryCountRef.current++
}
// Check quota limits before auto-retry
const tokenLimitCheck = quotaManager.checkTokenLimit()
if (!tokenLimitCheck.allowed) {
if (DEBUG) {
console.log(
"[sendAutomaticallyWhen] Token limit exceeded, stopping",
)
}
quotaManager.showTokenLimitToast(tokenLimitCheck.used)
autoRetryCountRef.current = 0
partialXmlRef.current = ""
return false
}
const tpmCheck = quotaManager.checkTPMLimit()
if (!tpmCheck.allowed) {
if (DEBUG) {
console.log(
"[sendAutomaticallyWhen] TPM limit exceeded, stopping",
)
}
quotaManager.showTPMLimitToast()
autoRetryCountRef.current = 0
partialXmlRef.current = ""
return false
}
// Increment retry count and allow retry
autoRetryCountRef.current++
if (DEBUG) {
console.log(
`[sendAutomaticallyWhen] Retrying (${autoRetryCountRef.current}/${MAX_AUTO_RETRY_COUNT})`,
)
}
return true
},
})
@@ -817,8 +910,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
previousXml: string,
sessionId: string,
) => {
// Reset auto-retry count on user-initiated message
// Reset all retry/continuation state on user-initiated message
autoRetryCountRef.current = 0
partialXmlRef.current = ""
const config = getAIConfig()

View File

@@ -42,11 +42,18 @@ description: Edit specific parts of the EXISTING diagram. Use this when making s
parameters: {
edits: Array<{search: string, replace: string}>
}
---Tool3---
tool name: append_diagram
description: Continue generating diagram XML when display_diagram was truncated due to output length limits. Only use this after display_diagram truncation.
parameters: {
xml: string // Continuation fragment (NO wrapper tags like <mxGraphModel> or <root>)
}
---End of tools---
IMPORTANT: Choose the right tool:
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
- Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items
- Use append_diagram for: ONLY when display_diagram was truncated due to output length - continue generating from where you stopped
Core capabilities:
- Generate valid, well-formed XML strings for draw.io diagrams
@@ -174,6 +181,18 @@ const EXTENDED_ADDITIONS = `
</root>
\`\`\`
### append_diagram Details
**WHEN TO USE:** Only call this tool when display_diagram output was truncated (you'll see an error message about truncation).
**CRITICAL RULES:**
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
3. End with the closing </root> tag to complete the diagram
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.
### edit_diagram Details
**CRITICAL RULES:**

View File

@@ -6,6 +6,95 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// ============================================================================
// XML Validation/Fix Constants
// ============================================================================
/** Maximum XML size to process (1MB) - larger XMLs may cause performance issues */
const MAX_XML_SIZE = 1_000_000
/** Maximum iterations for aggressive cell dropping to prevent infinite loops */
const MAX_DROP_ITERATIONS = 10
/** Structural attributes that should not be duplicated in draw.io */
const STRUCTURAL_ATTRS = [
"edge",
"parent",
"source",
"target",
"vertex",
"connectable",
]
/** Valid XML entity names */
const VALID_ENTITIES = new Set(["lt", "gt", "amp", "quot", "apos"])
// ============================================================================
// XML Parsing Helpers
// ============================================================================
interface ParsedTag {
tag: string
tagName: string
isClosing: boolean
isSelfClosing: boolean
startIndex: number
endIndex: number
}
/**
* Parse XML tags while properly handling quoted strings
* This is a shared utility used by both validation and fixing logic
*/
function parseXmlTags(xml: string): ParsedTag[] {
const tags: ParsedTag[] = []
let i = 0
while (i < xml.length) {
const tagStart = xml.indexOf("<", i)
if (tagStart === -1) break
// Find matching > by tracking quotes
let tagEnd = tagStart + 1
let inQuote = false
let quoteChar = ""
while (tagEnd < xml.length) {
const c = xml[tagEnd]
if (inQuote) {
if (c === quoteChar) inQuote = false
} else {
if (c === '"' || c === "'") {
inQuote = true
quoteChar = c
} else if (c === ">") {
break
}
}
tagEnd++
}
if (tagEnd >= xml.length) break
const tag = xml.substring(tagStart, tagEnd + 1)
i = tagEnd + 1
const tagMatch = /^<(\/?)([a-zA-Z][a-zA-Z0-9:_-]*)/.exec(tag)
if (!tagMatch) continue
tags.push({
tag,
tagName: tagMatch[2],
isClosing: tagMatch[1] === "/",
isSelfClosing: tag.endsWith("/>"),
startIndex: tagStart,
endIndex: tagEnd,
})
}
return tags
}
/**
* Format XML string with proper indentation and line breaks
* @param xml - The XML string to format
@@ -533,48 +622,13 @@ export function replaceXMLParts(
return result
}
/**
* Validates draw.io XML structure for common issues
* Uses DOM parsing + additional regex checks for high accuracy
* @param xml - The XML string to validate
* @returns null if valid, error message string if invalid
*/
export function validateMxCellStructure(xml: string): string | null {
// 0. First use DOM parser to catch syntax errors (most accurate)
try {
const parser = new DOMParser()
const doc = parser.parseFromString(xml, "text/xml")
const parseError = doc.querySelector("parsererror")
if (parseError) {
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use &lt; for <, &gt; for >, &amp; for &, &quot; for ". Regenerate the diagram with properly escaped values.`
}
// ============================================================================
// Validation Helper Functions
// ============================================================================
// DOM-based checks for nested mxCell
const allCells = doc.querySelectorAll("mxCell")
for (const cell of allCells) {
if (cell.parentElement?.tagName === "mxCell") {
const id = cell.getAttribute("id") || "unknown"
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
}
}
} catch {
// If DOMParser fails, continue with regex checks
}
// 1. Check for CDATA wrapper (invalid at document root)
if (/^\s*<!\[CDATA\[/.test(xml)) {
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
}
// 2. Check for duplicate structural attributes in tags
const structuralAttrs = new Set([
"edge",
"parent",
"source",
"target",
"vertex",
"connectable",
])
/** Check for duplicate structural attributes in a tag */
function checkDuplicateAttributes(xml: string): string | null {
const structuralSet = new Set(STRUCTURAL_ATTRS)
const tagPattern = /<[^>]+>/g
let tagMatch
while ((tagMatch = tagPattern.exec(xml)) !== null) {
@@ -587,24 +641,17 @@ export function validateMxCellStructure(xml: string): string | null {
attributes.set(attrName, (attributes.get(attrName) || 0) + 1)
}
const duplicates = Array.from(attributes.entries())
.filter(([name, count]) => count > 1 && structuralAttrs.has(name))
.filter(([name, count]) => count > 1 && structuralSet.has(name))
.map(([name]) => name)
if (duplicates.length > 0) {
return `Invalid XML: Duplicate structural attribute(s): ${duplicates.join(", ")}. Remove duplicate attributes.`
}
}
return null
}
// 3. Check for unescaped < in attribute values
const attrValuePattern = /=\s*"([^"]*)"/g
let attrValMatch
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
const value = attrValMatch[1]
if (/</.test(value) && !/&lt;/.test(value)) {
return "Invalid XML: Unescaped < character in attribute values. Replace < with &lt;"
}
}
// 4. Check for duplicate IDs
/** Check for duplicate IDs in XML */
function checkDuplicateIds(xml: string): string | null {
const idPattern = /\bid\s*=\s*["']([^"']+)["']/gi
const ids = new Map<string, number>()
let idMatch
@@ -618,50 +665,16 @@ export function validateMxCellStructure(xml: string): string | null {
if (duplicateIds.length > 0) {
return `Invalid XML: Found duplicate ID(s): ${duplicateIds.slice(0, 3).join(", ")}. All id attributes must be unique.`
}
return null
}
// 5. Check for tag mismatches using stateful parser
/** Check for tag mismatches using parsed tags */
function checkTagMismatches(xml: string): string | null {
const xmlWithoutComments = xml.replace(/<!--[\s\S]*?-->/g, "")
const tags = parseXmlTags(xmlWithoutComments)
const tagStack: string[] = []
// Parse tags properly by handling quoted strings
let i = 0
while (i < xmlWithoutComments.length) {
// Find next <
const tagStart = xmlWithoutComments.indexOf("<", i)
if (tagStart === -1) break
// Find matching > by tracking quotes
let tagEnd = tagStart + 1
let inQuote = false
let quoteChar = ""
while (tagEnd < xmlWithoutComments.length) {
const c = xmlWithoutComments[tagEnd]
if (inQuote) {
if (c === quoteChar) inQuote = false
} else {
if (c === '"' || c === "'") {
inQuote = true
quoteChar = c
} else if (c === ">") {
break
}
}
tagEnd++
}
if (tagEnd >= xmlWithoutComments.length) break
const tag = xmlWithoutComments.substring(tagStart, tagEnd + 1)
i = tagEnd + 1
// Parse the tag
const tagMatch = /^<(\/?)([a-zA-Z][a-zA-Z0-9:_-]*)/.exec(tag)
if (!tagMatch) continue
const isClosing = tagMatch[1] === "/"
const tagName = tagMatch[2]
const isSelfClosing = tag.endsWith("/>")
for (const { tagName, isClosing, isSelfClosing } of tags) {
if (isClosing) {
if (tagStack.length === 0) {
return `Invalid XML: Closing tag </${tagName}> without matching opening tag`
@@ -677,8 +690,11 @@ export function validateMxCellStructure(xml: string): string | null {
if (tagStack.length > 0) {
return `Invalid XML: Document has ${tagStack.length} unclosed tag(s): ${tagStack.join(", ")}`
}
return null
}
// 6. Check invalid character references
/** Check for invalid character references */
function checkCharacterReferences(xml: string): string | null {
const charRefPattern = /&#x?[^;]+;?/g
let charMatch
while ((charMatch = charRefPattern.exec(xml)) !== null) {
@@ -701,42 +717,30 @@ export function validateMxCellStructure(xml: string): string | null {
}
}
}
return null
}
// 7. Check for invalid comment syntax (-- inside comments)
const commentPattern = /<!--([\s\S]*?)-->/g
let commentMatch
while ((commentMatch = commentPattern.exec(xml)) !== null) {
if (/--/.test(commentMatch[1])) {
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
}
}
// 8. Check for unescaped entity references and invalid entity names
/** Check for invalid entity references */
function checkEntityReferences(xml: string): string | null {
const xmlWithoutComments = xml.replace(/<!--[\s\S]*?-->/g, "")
const bareAmpPattern = /&(?!(?:lt|gt|amp|quot|apos|#))/g
if (bareAmpPattern.test(xmlWithoutComments)) {
return "Invalid XML: Found unescaped & character(s). Replace & with &amp;"
}
const invalidEntityPattern = /&([a-zA-Z][a-zA-Z0-9]*);/g
const validEntities = new Set(["lt", "gt", "amp", "quot", "apos"])
let entityMatch
while (
(entityMatch = invalidEntityPattern.exec(xmlWithoutComments)) !== null
) {
if (!validEntities.has(entityMatch[1])) {
if (!VALID_ENTITIES.has(entityMatch[1])) {
return `Invalid XML: Invalid entity reference: &${entityMatch[1]}; - use only valid XML entities (lt, gt, amp, quot, apos)`
}
}
return null
}
// 9. Check for empty id attributes on mxCell
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
return "Invalid XML: Found mxCell element(s) with empty id attribute"
}
// 10. Check for mxfile wrapper (warning only - may not work with URL hash loading)
// Disabled: This is just a warning, not an error
// if (xml.trim().startsWith('<mxfile')) { ... }
// 11. Check for nested mxCell tags
/** Check for nested mxCell tags using regex */
function checkNestedMxCells(xml: string): string | null {
const cellTagPattern = /<\/?mxCell[^>]*>/g
const cellStack: number[] = []
let cellMatch
@@ -755,6 +759,100 @@ export function validateMxCellStructure(xml: string): string | null {
}
}
}
return null
}
/**
* Validates draw.io XML structure for common issues
* Uses DOM parsing + additional regex checks for high accuracy
* @param xml - The XML string to validate
* @returns null if valid, error message string if invalid
*/
export function validateMxCellStructure(xml: string): string | null {
// Size check for performance
if (xml.length > MAX_XML_SIZE) {
console.warn(
`[validateMxCellStructure] XML size (${xml.length}) exceeds ${MAX_XML_SIZE} bytes, may cause performance issues`,
)
}
// 0. First use DOM parser to catch syntax errors (most accurate)
try {
const parser = new DOMParser()
const doc = parser.parseFromString(xml, "text/xml")
const parseError = doc.querySelector("parsererror")
if (parseError) {
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use &lt; for <, &gt; for >, &amp; for &, &quot; for ". Regenerate the diagram with properly escaped values.`
}
// DOM-based checks for nested mxCell
const allCells = doc.querySelectorAll("mxCell")
for (const cell of allCells) {
if (cell.parentElement?.tagName === "mxCell") {
const id = cell.getAttribute("id") || "unknown"
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
}
}
} catch (error) {
// Log unexpected DOMParser errors before falling back to regex checks
console.warn(
"[validateMxCellStructure] DOMParser threw unexpected error, falling back to regex validation:",
error,
)
}
// 1. Check for CDATA wrapper (invalid at document root)
if (/^\s*<!\[CDATA\[/.test(xml)) {
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
}
// 2. Check for duplicate structural attributes
const dupAttrError = checkDuplicateAttributes(xml)
if (dupAttrError) return dupAttrError
// 3. Check for unescaped < in attribute values
const attrValuePattern = /=\s*"([^"]*)"/g
let attrValMatch
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
const value = attrValMatch[1]
if (/</.test(value) && !/&lt;/.test(value)) {
return "Invalid XML: Unescaped < character in attribute values. Replace < with &lt;"
}
}
// 4. Check for duplicate IDs
const dupIdError = checkDuplicateIds(xml)
if (dupIdError) return dupIdError
// 5. Check for tag mismatches
const tagMismatchError = checkTagMismatches(xml)
if (tagMismatchError) return tagMismatchError
// 6. Check invalid character references
const charRefError = checkCharacterReferences(xml)
if (charRefError) return charRefError
// 7. Check for invalid comment syntax (-- inside comments)
const commentPattern = /<!--([\s\S]*?)-->/g
let commentMatch
while ((commentMatch = commentPattern.exec(xml)) !== null) {
if (/--/.test(commentMatch[1])) {
return "Invalid XML: Comment contains -- (double hyphen) which is not allowed"
}
}
// 8. Check for unescaped entity references and invalid entity names
const entityError = checkEntityReferences(xml)
if (entityError) return entityError
// 9. Check for empty id attributes on mxCell
if (/<mxCell[^>]*\sid\s*=\s*["']\s*["'][^>]*>/g.test(xml)) {
return "Invalid XML: Found mxCell element(s) with empty id attribute"
}
// 10. Check for nested mxCell tags
const nestedCellError = checkNestedMxCells(xml)
if (nestedCellError) return nestedCellError
return null
}
@@ -768,18 +866,15 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
let fixed = xml
const fixes: string[] = []
// 0. Fix backslash-escaped quotes (common LLM mistakes)
// Handles: attr=\"value\", value="text\"inner\"more", and mixed patterns
// Uses backreference to match opening/closing quote style, then normalizes
if (/\\"/.test(fixed)) {
fixed = fixed.replace(
/(\s[\w:-]+)\s*=\s*(\\"|")([\s\S]*?)\2(?=[\s/>?]|$)/g,
(_match, attrName, _openQuote, content) => {
const cleanContent = content.replace(/\\"/g, "&quot;")
return `${attrName}="${cleanContent}"`
},
)
fixes.push("Fixed backslash-escaped quotes")
// 0. Fix JSON-escaped XML (common when XML is stored in JSON without unescaping)
// Only apply when we see JSON-escaped attribute patterns like =\"value\"
// Don't apply to legitimate \n in value attributes (draw.io uses these for line breaks)
if (/=\\"/.test(fixed)) {
// Replace literal \" with actual quotes
fixed = fixed.replace(/\\"/g, '"')
// Replace literal \n with actual newlines (only after confirming JSON-escaped)
fixed = fixed.replace(/\\n/g, "\n")
fixes.push("Fixed JSON-escaped XML")
}
// 1. Remove CDATA wrapper (MUST be before text-before-root check)
@@ -796,20 +891,11 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
}
// 2. Fix duplicate attributes (keep first occurrence, remove duplicates)
const structuralAttrsToFix = [
"edge",
"parent",
"source",
"target",
"vertex",
"connectable",
]
let dupAttrFixed = false
fixed = fixed.replace(/<[^>]+>/g, (tag) => {
const seenAttrs = new Set<string>()
let newTag = tag
for (const attr of structuralAttrsToFix) {
for (const attr of STRUCTURAL_ATTRS) {
// Find all occurrences of this attribute
const attrRegex = new RegExp(
`\\s${attr}\\s*=\\s*["'][^"']*["']`,
@@ -864,20 +950,44 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
}
}
// 3b. Fix malformed attribute values where &quot; is used as delimiter
// Pattern: attr=&quot;value&quot; should be attr="&quot;value&quot;"
const malformedQuotePattern =
/(\s[a-zA-Z][a-zA-Z0-9_:-]*)=&quot;([^&]*(?:&(?!quot;)[^&]*)*)&quot;/g
// 3b. Fix malformed attribute values where &quot; is used as delimiter instead of actual quotes
// Pattern: attr=&quot;value&quot; should become attr="value" (the &quot; was meant to be the quote delimiter)
// This commonly happens with dashPattern=&quot;1 1;&quot;
const malformedQuotePattern = /(\s[a-zA-Z][a-zA-Z0-9_:-]*)=&quot;/
if (malformedQuotePattern.test(fixed)) {
// Replace =&quot; with =" and trailing &quot; before next attribute or tag end with "
fixed = fixed.replace(
/(\s[a-zA-Z][a-zA-Z0-9_:-]*)=&quot;([^&]*(?:&(?!quot;)[^&]*)*)&quot;/g,
'$1="&quot;$2&quot;"',
/(\s[a-zA-Z][a-zA-Z0-9_:-]*)=&quot;([^&]*?)&quot;/g,
'$1="$2"',
)
fixes.push(
'Fixed malformed attribute quotes (=&quot;...&quot; to ="&quot;...&quot;")',
'Fixed malformed attribute quotes (=&quot;...&quot; to ="...")',
)
}
// 3c. Fix malformed closing tags like </tag/> -> </tag>
const malformedClosingTag = /<\/([a-zA-Z][a-zA-Z0-9]*)\s*\/>/g
if (malformedClosingTag.test(fixed)) {
fixed = fixed.replace(/<\/([a-zA-Z][a-zA-Z0-9]*)\s*\/>/g, "</$1>")
fixes.push("Fixed malformed closing tags (</tag/> to </tag>)")
}
// 3d. Fix missing space between attributes like vertex="1"parent="1"
const missingSpacePattern = /("[^"]*")([a-zA-Z][a-zA-Z0-9_:-]*=)/g
if (missingSpacePattern.test(fixed)) {
fixed = fixed.replace(/("[^"]*")([a-zA-Z][a-zA-Z0-9_:-]*=)/g, "$1 $2")
fixes.push("Added missing space between attributes")
}
// 3e. Fix unescaped quotes in style color values like fillColor="#fff2e6"
// The " after Color= prematurely ends the style attribute. Remove it.
// Pattern: ;fillColor="#fff → ;fillColor=#fff (remove first ", keep second as style closer)
const quotedColorPattern = /;([a-zA-Z]*[Cc]olor)="#/
if (quotedColorPattern.test(fixed)) {
fixed = fixed.replace(/;([a-zA-Z]*[Cc]olor)="#/g, ";$1=#")
fixes.push("Removed quotes around color values in style")
}
// 4. Fix unescaped < in attribute values
// This is tricky - we need to find < inside quoted attribute values
const attrPattern = /(=\s*")([^"]*?)(<)([^"]*?)(")/g
@@ -891,7 +1001,7 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
}
if (hasUnescapedLt) {
// Replace < with &lt; inside attribute values
fixed = fixed.replace(/=\s*"([^"]*)"/g, (match, value) => {
fixed = fixed.replace(/=\s*"([^"]*)"/g, (_match, value) => {
const escaped = value.replace(/</g, "&lt;")
return `="${escaped}"`
})
@@ -977,45 +1087,11 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
}
// 10. Fix unclosed tags by appending missing closing tags
// Track open tags and close any that are left open using stateful parser
// Use parseXmlTags helper to track open tags
const tagStack: string[] = []
const parsedTags = parseXmlTags(fixed)
let idx = 0
while (idx < fixed.length) {
const tagStart = fixed.indexOf("<", idx)
if (tagStart === -1) break
// Find matching > by tracking quotes
let tagEnd = tagStart + 1
let inQuote = false
let quoteChar = ""
while (tagEnd < fixed.length) {
const c = fixed[tagEnd]
if (inQuote) {
if (c === quoteChar) inQuote = false
} else {
if (c === '"' || c === "'") {
inQuote = true
quoteChar = c
} else if (c === ">") {
break
}
}
tagEnd++
}
if (tagEnd >= fixed.length) break
const tag = fixed.substring(tagStart, tagEnd + 1)
idx = tagEnd + 1
const tagMatch2 = /^<(\/?)([a-zA-Z][a-zA-Z0-9:_-]*)/.exec(tag)
if (!tagMatch2) continue
const isClosing = tagMatch2[1] === "/"
const tagName = tagMatch2[2]
const isSelfClosing = tag.endsWith("/>")
for (const { tagName, isClosing, isSelfClosing } of parsedTags) {
if (isClosing) {
// Find matching opening tag (may not be the last one if there's mismatch)
const lastIdx = tagStack.lastIndexOf(tagName)
@@ -1112,7 +1188,6 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
// Track mxCell depth
const isOpenCell = /<mxCell\s/.test(trimmed) && !trimmed.endsWith("/>")
const isCloseCell = trimmed === "</mxCell>"
const isSelfClose = /<mxCell[^>]*\/>/.test(trimmed)
if (isOpenCell) {
if (cellDepth > 0) {
@@ -1143,14 +1218,13 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
}
// 12. Fix duplicate IDs by appending suffix
const idPattern = /\bid\s*=\s*["']([^"']+)["']/gi
const seenIds = new Map<string, number>()
const duplicateIds: string[] = []
// First pass: find duplicates
const idPattern = /\bid\s*=\s*["']([^"']+)["']/gi
let idMatch
const tempPattern = /\bid\s*=\s*["']([^"']+)["']/gi
while ((idMatch = tempPattern.exec(fixed)) !== null) {
while ((idMatch = idPattern.exec(fixed)) !== null) {
const id = idMatch[1]
seenIds.set(id, (seenIds.get(id) || 0) + 1)
}
@@ -1182,7 +1256,7 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
let emptyIdCount = 0
fixed = fixed.replace(
/<mxCell([^>]*)\sid\s*=\s*["']\s*["']([^>]*)>/g,
(match, before, after) => {
(_match, before, after) => {
emptyIdCount++
const newId = `cell_${Date.now()}_${emptyIdCount}`
return `<mxCell${before} id="${newId}"${after}>`
@@ -1196,7 +1270,7 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
// Only do this if DOM parser still finds errors after all other fixes
if (typeof DOMParser !== "undefined") {
let droppedCells = 0
let maxIterations = 50
let maxIterations = MAX_DROP_ITERATIONS
while (maxIterations-- > 0) {
const parser = new DOMParser()
const doc = parser.parseFromString(fixed, "text/xml")

151
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.4.0",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.62",
"@ai-sdk/amazon-bedrock": "^3.0.70",
"@ai-sdk/anthropic": "^2.0.44",
"@ai-sdk/azure": "^2.0.69",
"@ai-sdk/deepseek": "^1.0.30",
@@ -40,6 +40,7 @@
"clsx": "^2.1.1",
"js-tiktoken": "^1.0.21",
"jsdom": "^26.0.0",
"jsonrepair": "^3.13.1",
"lucide-react": "^0.483.0",
"motion": "^12.23.25",
"next": "^16.0.7",
@@ -77,14 +78,14 @@
}
},
"node_modules/@ai-sdk/amazon-bedrock": {
"version": "3.0.62",
"resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-3.0.62.tgz",
"integrity": "sha512-vVtndaj5zfHmgw8NSqN4baFDbFDTBZP6qufhKfqSNLtygEm8+8PL9XQX9urgzSzU3zp+zi3AmNNemvKLkkqblg==",
"version": "3.0.70",
"resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-3.0.70.tgz",
"integrity": "sha512-4NIBlwuS/iLKq2ynOqqyJ9imk/oyHuOzhBx88Bfm5I0ihQPKJ0dMMD1IKKuyDZvLRYKmlOEpa//P+/ZBp10drw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/anthropic": "2.0.50",
"@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.18",
"@ai-sdk/provider-utils": "3.0.19",
"@smithy/eventstream-codec": "^4.0.1",
"@smithy/util-utf8": "^4.0.0",
"aws4fetch": "^1.0.20"
@@ -96,14 +97,48 @@
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/anthropic": {
"version": "2.0.50",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.50.tgz",
"integrity": "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw==",
"node_modules/@ai-sdk/amazon-bedrock/node_modules/@ai-sdk/provider-utils": {
"version": "3.0.19",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz",
"integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.18"
"@standard-schema/spec": "^1.0.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/anthropic": {
"version": "2.0.56",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.56.tgz",
"integrity": "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": {
"version": "3.0.19",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz",
"integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@standard-schema/spec": "^1.0.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
@@ -2488,9 +2523,9 @@
}
},
"node_modules/@next/env": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
"integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2504,9 +2539,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz",
"integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==",
"cpu": [
"arm64"
],
@@ -2520,9 +2555,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz",
"integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==",
"cpu": [
"x64"
],
@@ -2536,9 +2571,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz",
"integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==",
"cpu": [
"arm64"
],
@@ -2552,9 +2587,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz",
"integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==",
"cpu": [
"arm64"
],
@@ -2568,9 +2603,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz",
"integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==",
"cpu": [
"x64"
],
@@ -2584,9 +2619,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz",
"integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==",
"cpu": [
"x64"
],
@@ -2600,9 +2635,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz",
"integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==",
"cpu": [
"arm64"
],
@@ -2616,9 +2651,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
"integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
"cpu": [
"x64"
],
@@ -7978,14 +8013,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -9164,6 +9200,15 @@
"node": ">=6"
}
},
"node_modules/jsonrepair": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz",
"integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==",
"license": "ISC",
"bin": {
"jsonrepair": "bin/cli.js"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -10637,12 +10682,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
"license": "MIT",
"dependencies": {
"@next/env": "16.0.7",
"@next/env": "16.0.10",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -10655,14 +10700,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.0.7",
"@next/swc-darwin-x64": "16.0.7",
"@next/swc-linux-arm64-gnu": "16.0.7",
"@next/swc-linux-arm64-musl": "16.0.7",
"@next/swc-linux-x64-gnu": "16.0.7",
"@next/swc-linux-x64-musl": "16.0.7",
"@next/swc-win32-arm64-msvc": "16.0.7",
"@next/swc-win32-x64-msvc": "16.0.7",
"@next/swc-darwin-arm64": "16.0.10",
"@next/swc-darwin-x64": "16.0.10",
"@next/swc-linux-arm64-gnu": "16.0.10",
"@next/swc-linux-arm64-musl": "16.0.10",
"@next/swc-linux-x64-gnu": "16.0.10",
"@next/swc-linux-x64-musl": "16.0.10",
"@next/swc-win32-arm64-msvc": "16.0.10",
"@next/swc-win32-x64-msvc": "16.0.10",
"sharp": "^0.34.4"
},
"peerDependencies": {

View File

@@ -13,7 +13,7 @@
"prepare": "husky"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.62",
"@ai-sdk/amazon-bedrock": "^3.0.70",
"@ai-sdk/anthropic": "^2.0.44",
"@ai-sdk/azure": "^2.0.69",
"@ai-sdk/deepseek": "^1.0.30",
@@ -44,6 +44,7 @@
"clsx": "^2.1.1",
"js-tiktoken": "^1.0.21",
"jsdom": "^26.0.0",
"jsonrepair": "^3.13.1",
"lucide-react": "^0.483.0",
"motion": "^12.23.25",
"next": "^16.0.7",