mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
19 Commits
v0.4.0
...
fix/trunca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fbd3fe842 | ||
|
|
9068f608bf | ||
|
|
43139a5ef0 | ||
|
|
62e07f5f9c | ||
|
|
b33e09be05 | ||
|
|
987dc9f026 | ||
|
|
6024443816 | ||
|
|
4b838fd6d5 | ||
|
|
e321ba7959 | ||
|
|
aa15519fba | ||
|
|
c2c65973f9 | ||
|
|
b5db980f69 | ||
|
|
c9b60bfdb2 | ||
|
|
f170bb41ae | ||
|
|
a0f163fe9e | ||
|
|
8fd3830b9d | ||
|
|
77a25d2543 | ||
|
|
b9da24dd6d | ||
|
|
97cc0a07dc |
@@ -3,10 +3,12 @@ import {
|
|||||||
convertToModelMessages,
|
convertToModelMessages,
|
||||||
createUIMessageStream,
|
createUIMessageStream,
|
||||||
createUIMessageStreamResponse,
|
createUIMessageStreamResponse,
|
||||||
|
InvalidToolInputError,
|
||||||
LoadAPIKeyError,
|
LoadAPIKeyError,
|
||||||
stepCountIs,
|
stepCountIs,
|
||||||
streamText,
|
streamText,
|
||||||
} from "ai"
|
} from "ai"
|
||||||
|
import { jsonrepair } from "jsonrepair"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
@@ -18,7 +20,7 @@ import {
|
|||||||
} from "@/lib/langfuse"
|
} from "@/lib/langfuse"
|
||||||
import { getSystemPrompt } from "@/lib/system-prompts"
|
import { getSystemPrompt } from "@/lib/system-prompts"
|
||||||
|
|
||||||
export const maxDuration = 300
|
export const maxDuration = 120
|
||||||
|
|
||||||
// File upload limits (must match client-side)
|
// File upload limits (must match client-side)
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||||
@@ -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
|
// Helper function to create cached stream response
|
||||||
function createCachedStreamResponse(xml: string): Response {
|
function createCachedStreamResponse(xml: string): Response {
|
||||||
const toolCallId = `cached-${Date.now()}`
|
const toolCallId = `cached-${Date.now()}`
|
||||||
@@ -186,9 +159,9 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// Extract user input text for Langfuse trace
|
// Extract user input text for Langfuse trace
|
||||||
const currentMessage = messages[messages.length - 1]
|
const lastMessage = messages[messages.length - 1]
|
||||||
const userInputText =
|
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
|
// Update Langfuse trace with input, session, and user
|
||||||
setTraceInput({
|
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)
|
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
||||||
const systemMessage = getSystemPrompt(modelId)
|
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
|
// Extract file parts (images) from the last message
|
||||||
const fileParts =
|
const fileParts =
|
||||||
lastMessage.parts?.filter((part: any) => part.type === "file") || []
|
lastMessage.parts?.filter((part: any) => part.type === "file") || []
|
||||||
@@ -255,17 +222,19 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
|||||||
// User input only - XML is now in a separate cached system message
|
// User input only - XML is now in a separate cached system message
|
||||||
const formattedUserInput = `User input:
|
const formattedUserInput = `User input:
|
||||||
"""md
|
"""md
|
||||||
${lastMessageText}
|
${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)
|
||||||
|
|
||||||
// Fix tool call inputs for Bedrock API (requires JSON objects, not strings)
|
// Replace historical tool call XML with placeholders to reduce tokens
|
||||||
const fixedMessages = fixToolCallInputs(modelMessages)
|
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
|
||||||
|
const enableHistoryReplace =
|
||||||
// Replace historical tool call XML with placeholders to reduce tokens and avoid confusion
|
process.env.ENABLE_HISTORY_XML_REPLACE === "true"
|
||||||
const placeholderMessages = replaceHistoricalToolInputs(fixedMessages)
|
const placeholderMessages = enableHistoryReplace
|
||||||
|
? replaceHistoricalToolInputs(modelMessages)
|
||||||
|
: modelMessages
|
||||||
|
|
||||||
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
||||||
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
||||||
@@ -349,7 +318,35 @@ ${lastMessageText}
|
|||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model,
|
model,
|
||||||
|
...(process.env.MAX_OUTPUT_TOKENS && {
|
||||||
|
maxOutputTokens: parseInt(process.env.MAX_OUTPUT_TOKENS, 10),
|
||||||
|
}),
|
||||||
stopWhen: stepCountIs(5),
|
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,
|
messages: allMessages,
|
||||||
...(providerOptions && { providerOptions }), // This now includes all reasoning configs
|
...(providerOptions && { providerOptions }), // This now includes all reasoning configs
|
||||||
...(headers && { headers }),
|
...(headers && { headers }),
|
||||||
@@ -360,32 +357,6 @@ ${lastMessageText}
|
|||||||
userId,
|
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 }) => {
|
onFinish: ({ text, usage }) => {
|
||||||
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
|
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
|
||||||
setTraceOutput(text, {
|
setTraceOutput(text, {
|
||||||
@@ -467,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 && {
|
...(process.env.TEMPERATURE !== undefined && {
|
||||||
temperature: parseFloat(process.env.TEMPERATURE),
|
temperature: parseFloat(process.env.TEMPERATURE),
|
||||||
@@ -491,6 +482,7 @@ IMPORTANT: Keep edits concise:
|
|||||||
return {
|
return {
|
||||||
inputTokens: totalInputTokens,
|
inputTokens: totalInputTokens,
|
||||||
outputTokens: usage.outputTokens ?? 0,
|
outputTokens: usage.outputTokens ?? 0,
|
||||||
|
finishReason: (part as any).finishReason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { GoogleAnalytics } from "@next/third-parties/google"
|
import { GoogleAnalytics } from "@next/third-parties/google"
|
||||||
import { Analytics } from "@vercel/analytics/react"
|
|
||||||
import type { Metadata, Viewport } from "next"
|
import type { Metadata, Viewport } from "next"
|
||||||
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
||||||
import { DiagramProvider } from "@/contexts/diagram-context"
|
import { DiagramProvider } from "@/contexts/diagram-context"
|
||||||
@@ -117,7 +116,6 @@ export default function RootLayout({
|
|||||||
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<DiagramProvider>{children}</DiagramProvider>
|
<DiagramProvider>{children}</DiagramProvider>
|
||||||
<Analytics />
|
|
||||||
</body>
|
</body>
|
||||||
{process.env.NEXT_PUBLIC_GA_ID && (
|
{process.env.NEXT_PUBLIC_GA_ID && (
|
||||||
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
||||||
|
|||||||
@@ -19,19 +19,17 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import type { MutableRefObject } from "react"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
Reasoning,
|
Reasoning,
|
||||||
ReasoningContent,
|
ReasoningContent,
|
||||||
ReasoningTrigger,
|
ReasoningTrigger,
|
||||||
} from "@/components/ai-elements/reasoning"
|
} from "@/components/ai-elements/reasoning"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import {
|
import { convertToLegalXml, replaceNodes, validateAndFixXml } from "@/lib/utils"
|
||||||
convertToLegalXml,
|
|
||||||
replaceNodes,
|
|
||||||
validateMxCellStructure,
|
|
||||||
} from "@/lib/utils"
|
|
||||||
import ExamplePanel from "./chat-example-panel"
|
import ExamplePanel from "./chat-example-panel"
|
||||||
import { CodeBlock } from "./code-block"
|
import { CodeBlock } from "./code-block"
|
||||||
|
|
||||||
@@ -169,6 +167,7 @@ interface ChatMessageDisplayProps {
|
|||||||
messages: UIMessage[]
|
messages: UIMessage[]
|
||||||
setInput: (input: string) => void
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void
|
setFiles: (files: File[]) => void
|
||||||
|
processedToolCallsRef: MutableRefObject<Set<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
|
||||||
@@ -179,6 +178,7 @@ export function ChatMessageDisplay({
|
|||||||
messages,
|
messages,
|
||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
processedToolCallsRef,
|
||||||
sessionId,
|
sessionId,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onEditMessage,
|
onEditMessage,
|
||||||
@@ -187,7 +187,7 @@ export function ChatMessageDisplay({
|
|||||||
const { chartXML, loadDiagram: onDisplayChart } = useDiagram()
|
const { chartXML, loadDiagram: onDisplayChart } = useDiagram()
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const previousXML = useRef<string>("")
|
const previousXML = useRef<string>("")
|
||||||
const processedToolCalls = useRef<Set<string>>(new Set())
|
const processedToolCalls = processedToolCallsRef
|
||||||
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
@@ -213,9 +213,32 @@ export function ChatMessageDisplay({
|
|||||||
setCopiedMessageId(messageId)
|
setCopiedMessageId(messageId)
|
||||||
setTimeout(() => setCopiedMessageId(null), 2000)
|
setTimeout(() => setCopiedMessageId(null), 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to copy message:", err)
|
// Fallback for non-secure contexts (HTTP) or permission denied
|
||||||
setCopyFailedMessageId(messageId)
|
const textarea = document.createElement("textarea")
|
||||||
setTimeout(() => setCopyFailedMessageId(null), 2000)
|
textarea.value = text
|
||||||
|
textarea.style.position = "fixed"
|
||||||
|
textarea.style.left = "-9999px"
|
||||||
|
textarea.style.opacity = "0"
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
|
||||||
|
try {
|
||||||
|
textarea.select()
|
||||||
|
const success = document.execCommand("copy")
|
||||||
|
if (!success) {
|
||||||
|
throw new Error("Copy command failed")
|
||||||
|
}
|
||||||
|
setCopiedMessageId(messageId)
|
||||||
|
setTimeout(() => setCopiedMessageId(null), 2000)
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
console.error("Failed to copy message:", fallbackErr)
|
||||||
|
toast.error(
|
||||||
|
"Failed to copy message. Please copy manually or check clipboard permissions.",
|
||||||
|
)
|
||||||
|
setCopyFailedMessageId(messageId)
|
||||||
|
setTimeout(() => setCopyFailedMessageId(null), 2000)
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,32 +266,85 @@ export function ChatMessageDisplay({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to log feedback:", error)
|
console.error("Failed to log feedback:", error)
|
||||||
|
toast.error("Failed to record your feedback. Please try again.")
|
||||||
|
// Revert optimistic UI update
|
||||||
|
setFeedback((prev) => {
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[messageId]
|
||||||
|
return next
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
// If chartXML is empty, create a default mxfile structure to use with replaceNodes
|
// Parse and validate XML BEFORE calling replaceNodes
|
||||||
// This ensures the XML is properly wrapped in mxfile/diagram/mxGraphModel format
|
const parser = new DOMParser()
|
||||||
const baseXML =
|
const testDoc = parser.parseFromString(convertedXml, "text/xml")
|
||||||
chartXML ||
|
const parseError = testDoc.querySelector("parsererror")
|
||||||
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
|
||||||
const replacedXML = replaceNodes(baseXML, convertedXml)
|
|
||||||
|
|
||||||
const validationError = validateMxCellStructure(replacedXML)
|
if (parseError) {
|
||||||
if (!validationError) {
|
console.error(
|
||||||
previousXML.current = convertedXml
|
"[ChatMessageDisplay] Malformed XML detected - skipping update",
|
||||||
// Skip validation in loadDiagram since we already validated above
|
|
||||||
onDisplayChart(replacedXML, true)
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"[ChatMessageDisplay] XML validation failed:",
|
|
||||||
validationError,
|
|
||||||
)
|
)
|
||||||
|
// 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
|
||||||
|
// This ensures the XML is properly wrapped in mxfile/diagram/mxGraphModel format
|
||||||
|
const baseXML =
|
||||||
|
chartXML ||
|
||||||
|
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
||||||
|
const replacedXML = replaceNodes(baseXML, convertedXml)
|
||||||
|
|
||||||
|
// Validate and auto-fix the XML
|
||||||
|
const validation = validateAndFixXml(replacedXML)
|
||||||
|
if (validation.valid) {
|
||||||
|
previousXML.current = convertedXml
|
||||||
|
// Use fixed XML if available, otherwise use original
|
||||||
|
const xmlToLoad = validation.fixed || replacedXML
|
||||||
|
if (validation.fixes.length > 0) {
|
||||||
|
console.log(
|
||||||
|
"[ChatMessageDisplay] Auto-fixed XML issues:",
|
||||||
|
validation.fixes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Skip validation in loadDiagram since we already validated above
|
||||||
|
onDisplayChart(xmlToLoad, true)
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"[ChatMessageDisplay] XML validation failed:",
|
||||||
|
validation.error,
|
||||||
|
)
|
||||||
|
// 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.",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -311,12 +387,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,11 +451,24 @@ export function ChatMessageDisplay({
|
|||||||
Complete
|
Complete
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{state === "output-error" && (
|
{state === "output-error" &&
|
||||||
<span className="text-xs font-medium text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
|
(() => {
|
||||||
Error
|
// Check if this is a truncation (incomplete XML) vs real error
|
||||||
</span>
|
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 && (
|
{input && Object.keys(input).length > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -410,11 +501,23 @@ export function ChatMessageDisplay({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{output && state === "output-error" && (
|
{output &&
|
||||||
<div className="px-4 py-3 border-t border-border/40 text-sm text-red-600">
|
state === "output-error" &&
|
||||||
{output}
|
(() => {
|
||||||
</div>
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useChat } from "@ai-sdk/react"
|
|||||||
import { DefaultChatTransport } from "ai"
|
import { DefaultChatTransport } from "ai"
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
MessageSquarePlus,
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -17,6 +18,7 @@ import { FaGithub } from "react-icons/fa"
|
|||||||
import { Toaster, toast } from "sonner"
|
import { Toaster, toast } from "sonner"
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
import { ChatInput } from "@/components/chat-input"
|
import { ChatInput } from "@/components/chat-input"
|
||||||
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { getAIConfig } from "@/lib/ai-config"
|
import { getAIConfig } from "@/lib/ai-config"
|
||||||
@@ -61,31 +63,15 @@ interface ChatPanelProps {
|
|||||||
// Constants for tool states
|
// Constants for tool states
|
||||||
const TOOL_ERROR_STATE = "output-error" as const
|
const TOOL_ERROR_STATE = "output-error" as const
|
||||||
const DEBUG = process.env.NODE_ENV === "development"
|
const DEBUG = process.env.NODE_ENV === "development"
|
||||||
|
const MAX_AUTO_RETRY_COUNT = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom auto-resubmit logic for the AI chat.
|
* Check if auto-resubmit should happen based on tool errors.
|
||||||
*
|
* Only checks the LAST tool part (most recent tool call), not all tool parts.
|
||||||
* Strategy:
|
|
||||||
* - When tools return errors (e.g., invalid XML), automatically resubmit
|
|
||||||
* the conversation to let the AI retry with corrections
|
|
||||||
* - When tools succeed (e.g., diagram displayed), stop without AI acknowledgment
|
|
||||||
* to prevent unnecessary regeneration cycles
|
|
||||||
*
|
|
||||||
* This fixes the issue where successful diagrams were being regenerated
|
|
||||||
* multiple times because the previous logic (lastAssistantMessageIsCompleteWithToolCalls)
|
|
||||||
* auto-resubmitted on BOTH success and error.
|
|
||||||
*
|
|
||||||
* @param messages - Current conversation messages from AI SDK
|
|
||||||
* @returns true to auto-resubmit (for error recovery), false to stop
|
|
||||||
*/
|
*/
|
||||||
function shouldAutoResubmit(messages: ChatMessage[]): boolean {
|
function hasToolErrors(messages: ChatMessage[]): boolean {
|
||||||
const lastMessage = messages[messages.length - 1]
|
const lastMessage = messages[messages.length - 1]
|
||||||
if (!lastMessage || lastMessage.role !== "assistant") {
|
if (!lastMessage || lastMessage.role !== "assistant") {
|
||||||
if (DEBUG) {
|
|
||||||
console.log(
|
|
||||||
"[sendAutomaticallyWhen] No assistant message, returning false",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,31 +81,11 @@ function shouldAutoResubmit(messages: ChatMessage[]): boolean {
|
|||||||
) || []
|
) || []
|
||||||
|
|
||||||
if (toolParts.length === 0) {
|
if (toolParts.length === 0) {
|
||||||
if (DEBUG) {
|
|
||||||
console.log(
|
|
||||||
"[sendAutomaticallyWhen] No tool parts, returning false",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only auto-resubmit if ANY tool has an error
|
const lastToolPart = toolParts[toolParts.length - 1]
|
||||||
const hasError = toolParts.some((part) => part.state === TOOL_ERROR_STATE)
|
return lastToolPart?.state === TOOL_ERROR_STATE
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
if (hasError) {
|
|
||||||
console.log(
|
|
||||||
"[sendAutomaticallyWhen] Retrying due to errors in tools:",
|
|
||||||
toolParts
|
|
||||||
.filter((p) => p.state === TOOL_ERROR_STATE)
|
|
||||||
.map((p) => p.toolName),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
console.log("[sendAutomaticallyWhen] No errors, stopping")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatPanel({
|
export default function ChatPanel({
|
||||||
@@ -178,6 +144,7 @@ export default function ChatPanel({
|
|||||||
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
||||||
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
||||||
const [tpmLimit, setTpmLimit] = useState(0)
|
const [tpmLimit, setTpmLimit] = useState(0)
|
||||||
|
const [showNewChatDialog, setShowNewChatDialog] = useState(false)
|
||||||
|
|
||||||
// Check config on mount
|
// Check config on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -223,6 +190,16 @@ export default function ChatPanel({
|
|||||||
// Ref to hold stop function for use in onToolCall (avoids stale closure)
|
// Ref to hold stop function for use in onToolCall (avoids stale closure)
|
||||||
const stopRef = useRef<(() => void) | null>(null)
|
const stopRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -244,14 +221,43 @@ export default function ChatPanel({
|
|||||||
|
|
||||||
if (toolCall.toolName === "display_diagram") {
|
if (toolCall.toolName === "display_diagram") {
|
||||||
const { xml } = toolCall.input as { xml: string }
|
const { xml } = toolCall.input as { xml: string }
|
||||||
if (DEBUG) {
|
|
||||||
console.log(
|
// Check if XML is truncated (missing </root> indicates incomplete output)
|
||||||
`[display_diagram] Received XML length: ${xml.length}`,
|
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
|
// 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
|
// loadDiagram validates and returns error if invalid
|
||||||
const validationError = onDisplayChart(fullXml)
|
const validationError = onDisplayChart(fullXml)
|
||||||
@@ -277,7 +283,7 @@ Please fix the XML issues and call display_diagram again with corrected XML.
|
|||||||
|
|
||||||
Your failed XML:
|
Your failed XML:
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
${xml}
|
${finalXml}
|
||||||
\`\`\``,
|
\`\`\``,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -305,27 +311,13 @@ ${xml}
|
|||||||
|
|
||||||
let currentXml = ""
|
let currentXml = ""
|
||||||
try {
|
try {
|
||||||
console.log("[edit_diagram] Starting...")
|
|
||||||
// Use chartXML from ref directly - more reliable than export
|
// 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
|
const cachedXML = chartXMLRef.current
|
||||||
if (cachedXML) {
|
if (cachedXML) {
|
||||||
currentXml = cachedXML
|
currentXml = cachedXML
|
||||||
console.log(
|
|
||||||
"[edit_diagram] Using cached chartXML, length:",
|
|
||||||
currentXml.length,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to export only if no cached XML
|
// Fallback to export only if no cached XML
|
||||||
console.log(
|
|
||||||
"[edit_diagram] No cached XML, fetching from DrawIO...",
|
|
||||||
)
|
|
||||||
currentXml = await onFetchChart(false)
|
currentXml = await onFetchChart(false)
|
||||||
console.log(
|
|
||||||
"[edit_diagram] Got XML from export, length:",
|
|
||||||
currentXml.length,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { replaceXMLParts } = await import("@/lib/utils")
|
const { replaceXMLParts } = await import("@/lib/utils")
|
||||||
@@ -359,7 +351,6 @@ Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid ref
|
|||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
||||||
})
|
})
|
||||||
console.log("[edit_diagram] Success")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[edit_diagram] Failed:", error)
|
console.error("[edit_diagram] Failed:", error)
|
||||||
|
|
||||||
@@ -381,6 +372,83 @@ ${currentXml || "No XML available"}
|
|||||||
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
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) => {
|
onError: (error) => {
|
||||||
@@ -399,6 +467,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
friendlyMessage = "Network error. Please check your connection."
|
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
|
// Translate image not supported error
|
||||||
if (friendlyMessage.includes("image content block")) {
|
if (friendlyMessage.includes("image content block")) {
|
||||||
friendlyMessage = "This model doesn't support image input."
|
friendlyMessage = "This model doesn't support image input."
|
||||||
@@ -426,6 +500,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
const metadata = message?.metadata as
|
const metadata = message?.metadata as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
|
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
|
||||||
const inputTokens = Number.isFinite(metadata.inputTokens)
|
const inputTokens = Number.isFinite(metadata.inputTokens)
|
||||||
@@ -441,8 +516,58 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sendAutomaticallyWhen: ({ messages }) =>
|
sendAutomaticallyWhen: ({ messages }) => {
|
||||||
shouldAutoResubmit(messages as unknown as ChatMessage[]),
|
const isInContinuationMode = partialXmlRef.current.length > 0
|
||||||
|
|
||||||
|
const shouldRetry = hasToolErrors(
|
||||||
|
messages as unknown as ChatMessage[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!shouldRetry) {
|
||||||
|
// No error, reset retry count and clear state
|
||||||
|
autoRetryCountRef.current = 0
|
||||||
|
partialXmlRef.current = ""
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// Increment retry count for actual errors
|
||||||
|
autoRetryCountRef.current++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check quota limits before auto-retry
|
||||||
|
const tokenLimitCheck = quotaManager.checkTokenLimit()
|
||||||
|
if (!tokenLimitCheck.allowed) {
|
||||||
|
quotaManager.showTokenLimitToast(tokenLimitCheck.used)
|
||||||
|
autoRetryCountRef.current = 0
|
||||||
|
partialXmlRef.current = ""
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const tpmCheck = quotaManager.checkTPMLimit()
|
||||||
|
if (!tpmCheck.allowed) {
|
||||||
|
quotaManager.showTPMLimitToast()
|
||||||
|
autoRetryCountRef.current = 0
|
||||||
|
partialXmlRef.current = ""
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update stopRef so onToolCall can access it
|
// Update stopRef so onToolCall can access it
|
||||||
@@ -696,6 +821,32 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNewChat = useCallback(() => {
|
||||||
|
setMessages([])
|
||||||
|
clearDiagram()
|
||||||
|
handleFileChange([]) // Use handleFileChange to also clear pdfData
|
||||||
|
const newSessionId = `session-${Date.now()}-${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.slice(2, 9)}`
|
||||||
|
setSessionId(newSessionId)
|
||||||
|
xmlSnapshotsRef.current.clear()
|
||||||
|
// Clear localStorage with error handling
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
||||||
|
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
||||||
|
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
||||||
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId)
|
||||||
|
toast.success("Started a fresh chat")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to clear localStorage:", error)
|
||||||
|
toast.warning(
|
||||||
|
"Chat cleared but browser storage could not be updated",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowNewChatDialog(false)
|
||||||
|
}, [clearDiagram, handleFileChange, setMessages, setSessionId])
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -759,6 +910,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
previousXml: string,
|
previousXml: string,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
) => {
|
) => {
|
||||||
|
// Reset all retry/continuation state on user-initiated message
|
||||||
|
autoRetryCountRef.current = 0
|
||||||
|
partialXmlRef.current = ""
|
||||||
|
|
||||||
const config = getAIConfig()
|
const config = getAIConfig()
|
||||||
|
|
||||||
sendMessage(
|
sendMessage(
|
||||||
@@ -769,12 +924,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
"x-access-code": config.accessCode,
|
"x-access-code": config.accessCode,
|
||||||
...(config.aiProvider && {
|
...(config.aiProvider && {
|
||||||
"x-ai-provider": config.aiProvider,
|
"x-ai-provider": config.aiProvider,
|
||||||
|
...(config.aiBaseUrl && {
|
||||||
|
"x-ai-base-url": config.aiBaseUrl,
|
||||||
|
}),
|
||||||
|
...(config.aiApiKey && {
|
||||||
|
"x-ai-api-key": config.aiApiKey,
|
||||||
|
}),
|
||||||
|
...(config.aiModel && { "x-ai-model": config.aiModel }),
|
||||||
}),
|
}),
|
||||||
...(config.aiBaseUrl && {
|
|
||||||
"x-ai-base-url": config.aiBaseUrl,
|
|
||||||
}),
|
|
||||||
...(config.aiApiKey && { "x-ai-api-key": config.aiApiKey }),
|
|
||||||
...(config.aiModel && { "x-ai-model": config.aiModel }),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1010,6 +1167,18 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
<ButtonWithTooltip
|
||||||
|
tooltipContent="Start fresh chat"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShowNewChatDialog(true)}
|
||||||
|
className="hover:bg-accent"
|
||||||
|
>
|
||||||
|
<MessageSquarePlus
|
||||||
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||||
|
/>
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
<a
|
<a
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -1052,6 +1221,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
messages={messages}
|
messages={messages}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
setFiles={handleFileChange}
|
setFiles={handleFileChange}
|
||||||
|
processedToolCallsRef={processedToolCallsRef}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
onRegenerate={handleRegenerate}
|
onRegenerate={handleRegenerate}
|
||||||
status={status}
|
status={status}
|
||||||
@@ -1068,23 +1238,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
status={status}
|
status={status}
|
||||||
onSubmit={onFormSubmit}
|
onSubmit={onFormSubmit}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onClearChat={() => {
|
onClearChat={handleNewChat}
|
||||||
setMessages([])
|
|
||||||
clearDiagram()
|
|
||||||
const newSessionId = `session-${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.slice(2, 9)}`
|
|
||||||
setSessionId(newSessionId)
|
|
||||||
xmlSnapshotsRef.current.clear()
|
|
||||||
// Clear localStorage
|
|
||||||
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
|
||||||
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
|
||||||
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_SESSION_ID_KEY,
|
|
||||||
newSessionId,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
pdfData={pdfData}
|
pdfData={pdfData}
|
||||||
@@ -1104,6 +1258,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={onToggleDarkMode}
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ResetWarningModal
|
||||||
|
open={showNewChatDialog}
|
||||||
|
onOpenChange={setShowNewChatDialog}
|
||||||
|
onClear={handleNewChat}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createContext, useContext, useRef, useState } from "react"
|
|||||||
import type { DrawIoEmbedRef } from "react-drawio"
|
import type { DrawIoEmbedRef } from "react-drawio"
|
||||||
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
||||||
import type { ExportFormat } from "@/components/save-dialog"
|
import type { ExportFormat } from "@/components/save-dialog"
|
||||||
import { extractDiagramXML, validateMxCellStructure } from "../lib/utils"
|
import { extractDiagramXML, validateAndFixXml } from "../lib/utils"
|
||||||
|
|
||||||
interface DiagramContextType {
|
interface DiagramContextType {
|
||||||
chartXML: string
|
chartXML: string
|
||||||
@@ -86,21 +86,34 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
chart: string,
|
chart: string,
|
||||||
skipValidation?: boolean,
|
skipValidation?: boolean,
|
||||||
): string | null => {
|
): string | null => {
|
||||||
|
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) {
|
||||||
const validationError = validateMxCellStructure(chart)
|
const validation = validateAndFixXml(chart)
|
||||||
if (validationError) {
|
if (!validation.valid) {
|
||||||
console.warn("[loadDiagram] Validation error:", validationError)
|
console.warn(
|
||||||
return validationError
|
"[loadDiagram] Validation error:",
|
||||||
|
validation.error,
|
||||||
|
)
|
||||||
|
return validation.error
|
||||||
|
}
|
||||||
|
// Use fixed XML if auto-fix was applied
|
||||||
|
if (validation.fixed) {
|
||||||
|
console.log(
|
||||||
|
"[loadDiagram] Auto-fixed XML issues:",
|
||||||
|
validation.fixes,
|
||||||
|
)
|
||||||
|
xmlToLoad = validation.fixed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
|
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
|
||||||
setChartXML(chart)
|
setChartXML(xmlToLoad)
|
||||||
|
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: chart,
|
xml: xmlToLoad,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,13 +80,23 @@ SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflo
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
AZURE_API_KEY=your_api_key
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_RESOURCE_NAME=your-resource-name # Required: your Azure resource name
|
||||||
AI_MODEL=your-deployment-name
|
AI_MODEL=your-deployment-name
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional custom endpoint:
|
Or use a custom endpoint instead of resource name:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AZURE_BASE_URL=https://your-resource.openai.azure.com
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_BASE_URL=https://your-resource.openai.azure.com # Alternative to AZURE_RESOURCE_NAME
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional reasoning configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_REASONING_EFFORT=low # Optional: low, medium, high
|
||||||
|
AZURE_REASONING_SUMMARY=detailed # Optional: none, brief, detailed
|
||||||
```
|
```
|
||||||
|
|
||||||
### AWS Bedrock
|
### AWS Bedrock
|
||||||
|
|||||||
@@ -371,7 +371,16 @@ function detectProvider(): ProviderName | null {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (process.env[envVar]) {
|
if (process.env[envVar]) {
|
||||||
configuredProviders.push(provider as ProviderName)
|
// Azure requires additional config (baseURL or resourceName)
|
||||||
|
if (provider === "azure") {
|
||||||
|
const hasBaseUrl = !!process.env.AZURE_BASE_URL
|
||||||
|
const hasResourceName = !!process.env.AZURE_RESOURCE_NAME
|
||||||
|
if (hasBaseUrl || hasResourceName) {
|
||||||
|
configuredProviders.push(provider as ProviderName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
configuredProviders.push(provider as ProviderName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +402,18 @@ function validateProviderCredentials(provider: ProviderName): void {
|
|||||||
`Please set it in your .env.local file.`,
|
`Please set it in your .env.local file.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition to API key
|
||||||
|
if (provider === "azure") {
|
||||||
|
const hasBaseUrl = !!process.env.AZURE_BASE_URL
|
||||||
|
const hasResourceName = !!process.env.AZURE_RESOURCE_NAME
|
||||||
|
if (!hasBaseUrl && !hasResourceName) {
|
||||||
|
throw new Error(
|
||||||
|
`Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME to be set. ` +
|
||||||
|
`Please set one in your .env.local file.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -42,11 +42,18 @@ description: Edit specific parts of the EXISTING diagram. Use this when making s
|
|||||||
parameters: {
|
parameters: {
|
||||||
edits: Array<{search: string, replace: string}>
|
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---
|
---End of tools---
|
||||||
|
|
||||||
IMPORTANT: Choose the right tool:
|
IMPORTANT: Choose the right tool:
|
||||||
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
|
- 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 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:
|
Core capabilities:
|
||||||
- Generate valid, well-formed XML strings for draw.io diagrams
|
- Generate valid, well-formed XML strings for draw.io diagrams
|
||||||
@@ -174,6 +181,18 @@ const EXTENDED_ADDITIONS = `
|
|||||||
</root>
|
</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
|
### edit_diagram Details
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
**CRITICAL RULES:**
|
||||||
|
|||||||
901
lib/utils.ts
901
lib/utils.ts
@@ -6,6 +6,95 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs))
|
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
|
* Format XML string with proper indentation and line breaks
|
||||||
* @param xml - The XML string to format
|
* @param xml - The XML string to format
|
||||||
@@ -533,143 +622,733 @@ export function replaceXMLParts(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Validation Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** 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) {
|
||||||
|
const tag = tagMatch[0]
|
||||||
|
const attrPattern = /\s([a-zA-Z_:][a-zA-Z0-9_:.-]*)\s*=/g
|
||||||
|
const attributes = new Map<string, number>()
|
||||||
|
let attrMatch
|
||||||
|
while ((attrMatch = attrPattern.exec(tag)) !== null) {
|
||||||
|
const attrName = attrMatch[1]
|
||||||
|
attributes.set(attrName, (attributes.get(attrName) || 0) + 1)
|
||||||
|
}
|
||||||
|
const duplicates = Array.from(attributes.entries())
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
while ((idMatch = idPattern.exec(xml)) !== null) {
|
||||||
|
const id = idMatch[1]
|
||||||
|
ids.set(id, (ids.get(id) || 0) + 1)
|
||||||
|
}
|
||||||
|
const duplicateIds = Array.from(ids.entries())
|
||||||
|
.filter(([, count]) => count > 1)
|
||||||
|
.map(([id, count]) => `'${id}' (${count}x)`)
|
||||||
|
if (duplicateIds.length > 0) {
|
||||||
|
return `Invalid XML: Found duplicate ID(s): ${duplicateIds.slice(0, 3).join(", ")}. All id attributes must be unique.`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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[] = []
|
||||||
|
|
||||||
|
for (const { tagName, isClosing, isSelfClosing } of tags) {
|
||||||
|
if (isClosing) {
|
||||||
|
if (tagStack.length === 0) {
|
||||||
|
return `Invalid XML: Closing tag </${tagName}> without matching opening tag`
|
||||||
|
}
|
||||||
|
const expected = tagStack.pop()
|
||||||
|
if (expected?.toLowerCase() !== tagName.toLowerCase()) {
|
||||||
|
return `Invalid XML: Expected closing tag </${expected}> but found </${tagName}>`
|
||||||
|
}
|
||||||
|
} else if (!isSelfClosing) {
|
||||||
|
tagStack.push(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagStack.length > 0) {
|
||||||
|
return `Invalid XML: Document has ${tagStack.length} unclosed tag(s): ${tagStack.join(", ")}`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for invalid character references */
|
||||||
|
function checkCharacterReferences(xml: string): string | null {
|
||||||
|
const charRefPattern = /&#x?[^;]+;?/g
|
||||||
|
let charMatch
|
||||||
|
while ((charMatch = charRefPattern.exec(xml)) !== null) {
|
||||||
|
const ref = charMatch[0]
|
||||||
|
if (ref.startsWith("&#x")) {
|
||||||
|
if (!ref.endsWith(";")) {
|
||||||
|
return `Invalid XML: Missing semicolon after hex reference: ${ref}`
|
||||||
|
}
|
||||||
|
const hexDigits = ref.substring(3, ref.length - 1)
|
||||||
|
if (hexDigits.length === 0 || !/^[0-9a-fA-F]+$/.test(hexDigits)) {
|
||||||
|
return `Invalid XML: Invalid hex character reference: ${ref}`
|
||||||
|
}
|
||||||
|
} else if (ref.startsWith("&#")) {
|
||||||
|
if (!ref.endsWith(";")) {
|
||||||
|
return `Invalid XML: Missing semicolon after decimal reference: ${ref}`
|
||||||
|
}
|
||||||
|
const decDigits = ref.substring(2, ref.length - 1)
|
||||||
|
if (decDigits.length === 0 || !/^[0-9]+$/.test(decDigits)) {
|
||||||
|
return `Invalid XML: Invalid decimal character reference: ${ref}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 &"
|
||||||
|
}
|
||||||
|
const invalidEntityPattern = /&([a-zA-Z][a-zA-Z0-9]*);/g
|
||||||
|
let entityMatch
|
||||||
|
while (
|
||||||
|
(entityMatch = invalidEntityPattern.exec(xmlWithoutComments)) !== null
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for nested mxCell tags using regex */
|
||||||
|
function checkNestedMxCells(xml: string): string | null {
|
||||||
|
const cellTagPattern = /<\/?mxCell[^>]*>/g
|
||||||
|
const cellStack: number[] = []
|
||||||
|
let cellMatch
|
||||||
|
while ((cellMatch = cellTagPattern.exec(xml)) !== null) {
|
||||||
|
const tag = cellMatch[0]
|
||||||
|
if (tag.startsWith("</mxCell>")) {
|
||||||
|
if (cellStack.length > 0) cellStack.pop()
|
||||||
|
} else if (!tag.endsWith("/>")) {
|
||||||
|
const isLabelOrGeometry =
|
||||||
|
/\sas\s*=\s*["'](valueLabel|geometry)["']/.test(tag)
|
||||||
|
if (!isLabelOrGeometry) {
|
||||||
|
cellStack.push(cellMatch.index)
|
||||||
|
if (cellStack.length > 1) {
|
||||||
|
return "Invalid XML: Found nested mxCell tags. Cells should be siblings, not nested inside other mxCell elements."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates draw.io XML structure for common issues
|
* Validates draw.io XML structure for common issues
|
||||||
|
* Uses DOM parsing + additional regex checks for high accuracy
|
||||||
* @param xml - The XML string to validate
|
* @param xml - The XML string to validate
|
||||||
* @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 {
|
||||||
const parser = new DOMParser()
|
// Size check for performance
|
||||||
const doc = parser.parseFromString(xml, "text/xml")
|
if (xml.length > MAX_XML_SIZE) {
|
||||||
|
console.warn(
|
||||||
// Check for XML parsing errors (includes unescaped special characters)
|
`[validateMxCellStructure] XML size (${xml.length}) exceeds ${MAX_XML_SIZE} bytes, may cause performance issues`,
|
||||||
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 < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all mxCell elements once for all validations
|
// 0. First use DOM parser to catch syntax errors (most accurate)
|
||||||
const allCells = doc.querySelectorAll("mxCell")
|
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 < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`
|
||||||
|
}
|
||||||
|
|
||||||
// Single pass: collect IDs, check for duplicates, nesting, orphans, and invalid parents
|
// DOM-based checks for nested mxCell
|
||||||
const cellIds = new Set<string>()
|
const allCells = doc.querySelectorAll("mxCell")
|
||||||
const duplicateIds: string[] = []
|
for (const cell of allCells) {
|
||||||
const nestedCells: string[] = []
|
if (cell.parentElement?.tagName === "mxCell") {
|
||||||
const orphanCells: string[] = []
|
const id = cell.getAttribute("id") || "unknown"
|
||||||
const invalidParents: { id: string; parent: string }[] = []
|
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
|
||||||
const edgesToValidate: {
|
|
||||||
id: string
|
|
||||||
source: string | null
|
|
||||||
target: string | null
|
|
||||||
}[] = []
|
|
||||||
|
|
||||||
allCells.forEach((cell) => {
|
|
||||||
const id = cell.getAttribute("id")
|
|
||||||
const parent = cell.getAttribute("parent")
|
|
||||||
const isEdge = cell.getAttribute("edge") === "1"
|
|
||||||
|
|
||||||
// Check for duplicate IDs
|
|
||||||
if (id) {
|
|
||||||
if (cellIds.has(id)) {
|
|
||||||
duplicateIds.push(id)
|
|
||||||
} else {
|
|
||||||
cellIds.add(id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
// Check for nested mxCell (parent element is also mxCell)
|
// Log unexpected DOMParser errors before falling back to regex checks
|
||||||
if (cell.parentElement?.tagName === "mxCell") {
|
console.warn(
|
||||||
nestedCells.push(id || "unknown")
|
"[validateMxCellStructure] DOMParser threw unexpected error, falling back to regex validation:",
|
||||||
}
|
error,
|
||||||
|
)
|
||||||
// Check parent attribute (skip root cell id="0")
|
|
||||||
if (id !== "0") {
|
|
||||||
if (!parent) {
|
|
||||||
if (id) orphanCells.push(id)
|
|
||||||
} else {
|
|
||||||
// Store for later validation (after all IDs collected)
|
|
||||||
invalidParents.push({ id: id || "unknown", parent })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect edges for connection validation
|
|
||||||
if (isEdge) {
|
|
||||||
edgesToValidate.push({
|
|
||||||
id: id || "unknown",
|
|
||||||
source: cell.getAttribute("source"),
|
|
||||||
target: cell.getAttribute("target"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return errors in priority order
|
|
||||||
if (nestedCells.length > 0) {
|
|
||||||
return `Invalid XML: Found nested mxCell elements (IDs: ${nestedCells.slice(0, 3).join(", ")}). All mxCell elements must be direct children of <root>, never nested inside other mxCell elements. Please regenerate the diagram with correct structure.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duplicateIds.length > 0) {
|
// 1. Check for CDATA wrapper (invalid at document root)
|
||||||
return `Invalid XML: Found duplicate cell IDs (${duplicateIds.slice(0, 3).join(", ")}). Each mxCell must have a unique ID. Please regenerate the diagram with unique IDs for all elements.`
|
if (/^\s*<!\[CDATA\[/.test(xml)) {
|
||||||
|
return "Invalid XML: XML is wrapped in CDATA section - remove <![CDATA[ from start and ]]> from end"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orphanCells.length > 0) {
|
// 2. Check for duplicate structural attributes
|
||||||
return `Invalid XML: Found cells without parent attribute (IDs: ${orphanCells.slice(0, 3).join(", ")}). All mxCell elements (except id="0") must have a parent attribute. Please regenerate the diagram with proper parent references.`
|
const dupAttrError = checkDuplicateAttributes(xml)
|
||||||
}
|
if (dupAttrError) return dupAttrError
|
||||||
|
|
||||||
// Validate parent references (now that all IDs are collected)
|
// 3. Check for unescaped < in attribute values
|
||||||
const badParents = invalidParents.filter((p) => !cellIds.has(p.parent))
|
const attrValuePattern = /=\s*"([^"]*)"/g
|
||||||
if (badParents.length > 0) {
|
let attrValMatch
|
||||||
const details = badParents
|
while ((attrValMatch = attrValuePattern.exec(xml)) !== null) {
|
||||||
.slice(0, 3)
|
const value = attrValMatch[1]
|
||||||
.map((p) => `${p.id} (parent: ${p.parent})`)
|
if (/</.test(value) && !/</.test(value)) {
|
||||||
.join(", ")
|
return "Invalid XML: Unescaped < character in attribute values. Replace < with <"
|
||||||
return `Invalid XML: Found cells with invalid parent references (${details}). Parent IDs must reference existing cells. Please regenerate the diagram with valid parent references.`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate edge connections
|
|
||||||
const invalidConnections: string[] = []
|
|
||||||
edgesToValidate.forEach((edge) => {
|
|
||||||
if (edge.source && !cellIds.has(edge.source)) {
|
|
||||||
invalidConnections.push(`${edge.id} (source: ${edge.source})`)
|
|
||||||
}
|
}
|
||||||
if (edge.target && !cellIds.has(edge.target)) {
|
|
||||||
invalidConnections.push(`${edge.id} (target: ${edge.target})`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (invalidConnections.length > 0) {
|
|
||||||
return `Invalid XML: Found edges with invalid source/target references (${invalidConnections.slice(0, 3).join(", ")}). Edge source and target must reference existing cell IDs. Please regenerate the diagram with valid edge connections.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for orphaned mxPoint elements (not inside <Array as="points"> and without 'as' attribute)
|
// 4. Check for duplicate IDs
|
||||||
// These cause "Could not add object mxPoint" errors in draw.io
|
const dupIdError = checkDuplicateIds(xml)
|
||||||
const allMxPoints = doc.querySelectorAll("mxPoint")
|
if (dupIdError) return dupIdError
|
||||||
const orphanedMxPoints: string[] = []
|
|
||||||
allMxPoints.forEach((point) => {
|
|
||||||
const hasAsAttr = point.hasAttribute("as")
|
|
||||||
const parentIsArray =
|
|
||||||
point.parentElement?.tagName === "Array" &&
|
|
||||||
point.parentElement?.getAttribute("as") === "points"
|
|
||||||
|
|
||||||
if (!hasAsAttr && !parentIsArray) {
|
// 5. Check for tag mismatches
|
||||||
// Find the parent mxCell to report which edge has the problem
|
const tagMismatchError = checkTagMismatches(xml)
|
||||||
let parent = point.parentElement
|
if (tagMismatchError) return tagMismatchError
|
||||||
while (parent && parent.tagName !== "mxCell") {
|
|
||||||
parent = parent.parentElement
|
// 6. Check invalid character references
|
||||||
}
|
const charRefError = checkCharacterReferences(xml)
|
||||||
const cellId = parent?.getAttribute("id") || "unknown"
|
if (charRefError) return charRefError
|
||||||
if (!orphanedMxPoints.includes(cellId)) {
|
|
||||||
orphanedMxPoints.push(cellId)
|
// 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"
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
if (orphanedMxPoints.length > 0) {
|
|
||||||
return `Invalid XML: Found orphaned mxPoint elements in cells (${orphanedMxPoints.slice(0, 3).join(", ")}). mxPoint elements must either have an 'as' attribute (e.g., as="sourcePoint") or be inside <Array as="points">. For edge waypoints, use: <Array as="points"><mxPoint x="..." y="..."/></Array>. Please fix the mxPoint structure.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to auto-fix common XML issues in draw.io diagrams
|
||||||
|
* @param xml - The XML string to fix
|
||||||
|
* @returns Object with fixed XML and list of fixes applied
|
||||||
|
*/
|
||||||
|
export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
|
||||||
|
let fixed = xml
|
||||||
|
const fixes: string[] = []
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if (/^\s*<!\[CDATA\[/.test(fixed)) {
|
||||||
|
fixed = fixed.replace(/^\s*<!\[CDATA\[/, "").replace(/\]\]>\s*$/, "")
|
||||||
|
fixes.push("Removed CDATA wrapper")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remove text before XML declaration or root element (only if it's garbage text, not valid XML)
|
||||||
|
const xmlStart = fixed.search(/<(\?xml|mxGraphModel|mxfile)/i)
|
||||||
|
if (xmlStart > 0 && !/^<[a-zA-Z]/.test(fixed.trim())) {
|
||||||
|
fixed = fixed.substring(xmlStart)
|
||||||
|
fixes.push("Removed text before XML root")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fix duplicate attributes (keep first occurrence, remove duplicates)
|
||||||
|
let dupAttrFixed = false
|
||||||
|
fixed = fixed.replace(/<[^>]+>/g, (tag) => {
|
||||||
|
let newTag = tag
|
||||||
|
|
||||||
|
for (const attr of STRUCTURAL_ATTRS) {
|
||||||
|
// Find all occurrences of this attribute
|
||||||
|
const attrRegex = new RegExp(
|
||||||
|
`\\s${attr}\\s*=\\s*["'][^"']*["']`,
|
||||||
|
"gi",
|
||||||
|
)
|
||||||
|
const matches = tag.match(attrRegex)
|
||||||
|
|
||||||
|
if (matches && matches.length > 1) {
|
||||||
|
// Keep first, remove others
|
||||||
|
let firstKept = false
|
||||||
|
newTag = newTag.replace(attrRegex, (m) => {
|
||||||
|
if (!firstKept) {
|
||||||
|
firstKept = true
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
dupAttrFixed = true
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newTag
|
||||||
|
})
|
||||||
|
if (dupAttrFixed) {
|
||||||
|
fixes.push("Removed duplicate structural attributes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix unescaped & characters (but not valid entities)
|
||||||
|
// Match & not followed by valid entity pattern
|
||||||
|
const ampersandPattern =
|
||||||
|
/&(?!(?:lt|gt|amp|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)/g
|
||||||
|
if (ampersandPattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(
|
||||||
|
/&(?!(?:lt|gt|amp|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)/g,
|
||||||
|
"&",
|
||||||
|
)
|
||||||
|
fixes.push("Escaped unescaped & characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix invalid entity names like &quot; -> "
|
||||||
|
// Common mistake: double-escaping
|
||||||
|
const invalidEntities = [
|
||||||
|
{ pattern: /&quot;/g, replacement: """, name: "&quot;" },
|
||||||
|
{ pattern: /&lt;/g, replacement: "<", name: "&lt;" },
|
||||||
|
{ pattern: /&gt;/g, replacement: ">", name: "&gt;" },
|
||||||
|
{ pattern: /&apos;/g, replacement: "'", name: "&apos;" },
|
||||||
|
{ pattern: /&amp;/g, replacement: "&", name: "&amp;" },
|
||||||
|
]
|
||||||
|
for (const { pattern, replacement, name } of invalidEntities) {
|
||||||
|
if (pattern.test(fixed)) {
|
||||||
|
fixed = fixed.replace(pattern, replacement)
|
||||||
|
fixes.push(`Fixed double-escaped entity ${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3b. Fix malformed attribute values where " is used as delimiter instead of actual quotes
|
||||||
|
// Pattern: attr="value" should become attr="value" (the " was meant to be the quote delimiter)
|
||||||
|
// This commonly happens with dashPattern="1 1;"
|
||||||
|
const malformedQuotePattern = /(\s[a-zA-Z][a-zA-Z0-9_:-]*)="/
|
||||||
|
if (malformedQuotePattern.test(fixed)) {
|
||||||
|
// Replace =" with =" and trailing " before next attribute or tag end with "
|
||||||
|
fixed = fixed.replace(
|
||||||
|
/(\s[a-zA-Z][a-zA-Z0-9_:-]*)="([^&]*?)"/g,
|
||||||
|
'$1="$2"',
|
||||||
|
)
|
||||||
|
fixes.push(
|
||||||
|
'Fixed malformed attribute quotes (="..." 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
|
||||||
|
let attrMatch
|
||||||
|
let hasUnescapedLt = false
|
||||||
|
while ((attrMatch = attrPattern.exec(fixed)) !== null) {
|
||||||
|
if (!attrMatch[3].startsWith("<")) {
|
||||||
|
hasUnescapedLt = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasUnescapedLt) {
|
||||||
|
// Replace < with < inside attribute values
|
||||||
|
fixed = fixed.replace(/=\s*"([^"]*)"/g, (_match, value) => {
|
||||||
|
const escaped = value.replace(/</g, "<")
|
||||||
|
return `="${escaped}"`
|
||||||
|
})
|
||||||
|
fixes.push("Escaped < characters in attribute values")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fix invalid character references (remove malformed ones)
|
||||||
|
// Pattern: &#x followed by non-hex chars before ;
|
||||||
|
const invalidHexRefs: string[] = []
|
||||||
|
fixed = fixed.replace(/&#x([^;]*);/g, (match, hex) => {
|
||||||
|
if (/^[0-9a-fA-F]+$/.test(hex) && hex.length > 0) {
|
||||||
|
return match // Valid hex ref, keep it
|
||||||
|
}
|
||||||
|
invalidHexRefs.push(match)
|
||||||
|
return "" // Remove invalid ref
|
||||||
|
})
|
||||||
|
if (invalidHexRefs.length > 0) {
|
||||||
|
fixes.push(
|
||||||
|
`Removed ${invalidHexRefs.length} invalid hex character reference(s)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Fix invalid decimal character references
|
||||||
|
const invalidDecRefs: string[] = []
|
||||||
|
fixed = fixed.replace(/&#([^x][^;]*);/g, (match, dec) => {
|
||||||
|
if (/^[0-9]+$/.test(dec) && dec.length > 0) {
|
||||||
|
return match // Valid decimal ref, keep it
|
||||||
|
}
|
||||||
|
invalidDecRefs.push(match)
|
||||||
|
return "" // Remove invalid ref
|
||||||
|
})
|
||||||
|
if (invalidDecRefs.length > 0) {
|
||||||
|
fixes.push(
|
||||||
|
`Removed ${invalidDecRefs.length} invalid decimal character reference(s)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Fix invalid comment syntax (replace -- with - repeatedly until none left)
|
||||||
|
fixed = fixed.replace(/<!--([\s\S]*?)-->/g, (match, content) => {
|
||||||
|
if (/--/.test(content)) {
|
||||||
|
// Keep replacing until no double hyphens remain
|
||||||
|
let fixedContent = content
|
||||||
|
while (/--/.test(fixedContent)) {
|
||||||
|
fixedContent = fixedContent.replace(/--/g, "-")
|
||||||
|
}
|
||||||
|
fixes.push("Fixed invalid comment syntax (removed double hyphens)")
|
||||||
|
return `<!--${fixedContent}-->`
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
|
||||||
|
// 8. Fix <Cell> tags that should be <mxCell> (common LLM mistake)
|
||||||
|
// This handles both opening and closing tags
|
||||||
|
const hasCellTags = /<\/?Cell[\s>]/i.test(fixed)
|
||||||
|
if (hasCellTags) {
|
||||||
|
fixed = fixed.replace(/<Cell(\s)/gi, "<mxCell$1")
|
||||||
|
fixed = fixed.replace(/<Cell>/gi, "<mxCell>")
|
||||||
|
fixed = fixed.replace(/<\/Cell>/gi, "</mxCell>")
|
||||||
|
fixes.push("Fixed <Cell> tags to <mxCell>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Fix common closing tag typos
|
||||||
|
const tagTypos = [
|
||||||
|
{ wrong: /<\/mxElement>/gi, right: "</mxCell>", name: "</mxElement>" },
|
||||||
|
{ wrong: /<\/mxcell>/g, right: "</mxCell>", name: "</mxcell>" }, // case sensitivity
|
||||||
|
{
|
||||||
|
wrong: /<\/mxgeometry>/g,
|
||||||
|
right: "</mxGeometry>",
|
||||||
|
name: "</mxgeometry>",
|
||||||
|
},
|
||||||
|
{ wrong: /<\/mxpoint>/g, right: "</mxPoint>", name: "</mxpoint>" },
|
||||||
|
{
|
||||||
|
wrong: /<\/mxgraphmodel>/gi,
|
||||||
|
right: "</mxGraphModel>",
|
||||||
|
name: "</mxgraphmodel>",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for (const { wrong, right, name } of tagTypos) {
|
||||||
|
if (wrong.test(fixed)) {
|
||||||
|
fixed = fixed.replace(wrong, right)
|
||||||
|
fixes.push(`Fixed typo ${name} to ${right}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Fix unclosed tags by appending missing closing tags
|
||||||
|
// Use parseXmlTags helper to track open tags
|
||||||
|
const tagStack: string[] = []
|
||||||
|
const parsedTags = parseXmlTags(fixed)
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (lastIdx !== -1) {
|
||||||
|
tagStack.splice(lastIdx, 1)
|
||||||
|
}
|
||||||
|
} else if (!isSelfClosing) {
|
||||||
|
tagStack.push(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are unclosed tags, append closing tags in reverse order
|
||||||
|
// But first verify with simple count that they're actually unclosed
|
||||||
|
if (tagStack.length > 0) {
|
||||||
|
const tagsToClose: string[] = []
|
||||||
|
for (const tagName of tagStack.reverse()) {
|
||||||
|
// Simple count check: only close if opens > closes
|
||||||
|
const openCount = (
|
||||||
|
fixed.match(new RegExp(`<${tagName}[\\s>]`, "gi")) || []
|
||||||
|
).length
|
||||||
|
const closeCount = (
|
||||||
|
fixed.match(new RegExp(`</${tagName}>`, "gi")) || []
|
||||||
|
).length
|
||||||
|
if (openCount > closeCount) {
|
||||||
|
tagsToClose.push(tagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagsToClose.length > 0) {
|
||||||
|
const closingTags = tagsToClose.map((t) => `</${t}>`).join("\n")
|
||||||
|
fixed = fixed.trimEnd() + "\n" + closingTags
|
||||||
|
fixes.push(
|
||||||
|
`Closed ${tagsToClose.length} unclosed tag(s): ${tagsToClose.join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Fix nested mxCell by flattening
|
||||||
|
// Pattern A: <mxCell id="X">...<mxCell id="X">...</mxCell></mxCell> (duplicate ID)
|
||||||
|
// Pattern B: <mxCell id="X">...<mxCell id="Y">...</mxCell></mxCell> (different ID - true nesting)
|
||||||
|
const lines = fixed.split("\n")
|
||||||
|
let newLines: string[] = []
|
||||||
|
let nestedFixed = 0
|
||||||
|
let extraClosingToRemove = 0
|
||||||
|
|
||||||
|
// First pass: fix duplicate ID nesting (same as before)
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const nextLine = lines[i + 1]
|
||||||
|
|
||||||
|
// Check if current line and next line are both mxCell opening tags with same ID
|
||||||
|
if (
|
||||||
|
nextLine &&
|
||||||
|
/<mxCell\s/.test(line) &&
|
||||||
|
/<mxCell\s/.test(nextLine) &&
|
||||||
|
!line.includes("/>") &&
|
||||||
|
!nextLine.includes("/>")
|
||||||
|
) {
|
||||||
|
const id1 = line.match(/\bid\s*=\s*["']([^"']+)["']/)?.[1]
|
||||||
|
const id2 = nextLine.match(/\bid\s*=\s*["']([^"']+)["']/)?.[1]
|
||||||
|
|
||||||
|
if (id1 && id1 === id2) {
|
||||||
|
nestedFixed++
|
||||||
|
extraClosingToRemove++ // Need to remove one </mxCell> later
|
||||||
|
continue // Skip this duplicate opening line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove extra </mxCell> if we have pending removals
|
||||||
|
if (extraClosingToRemove > 0 && /^\s*<\/mxCell>\s*$/.test(line)) {
|
||||||
|
extraClosingToRemove--
|
||||||
|
continue // Skip this closing tag
|
||||||
|
}
|
||||||
|
|
||||||
|
newLines.push(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nestedFixed > 0) {
|
||||||
|
fixed = newLines.join("\n")
|
||||||
|
fixes.push(`Flattened ${nestedFixed} duplicate-ID nested mxCell(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: fix true nesting (different IDs)
|
||||||
|
// Insert </mxCell> before nested child to close parent
|
||||||
|
const lines2 = fixed.split("\n")
|
||||||
|
newLines = []
|
||||||
|
let trueNestedFixed = 0
|
||||||
|
let cellDepth = 0
|
||||||
|
let pendingCloseRemoval = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < lines2.length; i++) {
|
||||||
|
const line = lines2[i]
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
// Track mxCell depth
|
||||||
|
const isOpenCell = /<mxCell\s/.test(trimmed) && !trimmed.endsWith("/>")
|
||||||
|
const isCloseCell = trimmed === "</mxCell>"
|
||||||
|
|
||||||
|
if (isOpenCell) {
|
||||||
|
if (cellDepth > 0) {
|
||||||
|
// Found nested cell - insert closing tag for parent before this line
|
||||||
|
const indent = line.match(/^(\s*)/)?.[1] || ""
|
||||||
|
newLines.push(indent + "</mxCell>")
|
||||||
|
trueNestedFixed++
|
||||||
|
pendingCloseRemoval++ // Need to remove one </mxCell> later
|
||||||
|
}
|
||||||
|
cellDepth = 1 // Reset to 1 since we just opened a new cell
|
||||||
|
newLines.push(line)
|
||||||
|
} else if (isCloseCell) {
|
||||||
|
if (pendingCloseRemoval > 0) {
|
||||||
|
pendingCloseRemoval--
|
||||||
|
// Skip this extra closing tag
|
||||||
|
} else {
|
||||||
|
cellDepth = Math.max(0, cellDepth - 1)
|
||||||
|
newLines.push(line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newLines.push(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trueNestedFixed > 0) {
|
||||||
|
fixed = newLines.join("\n")
|
||||||
|
fixes.push(`Fixed ${trueNestedFixed} true nested mxCell(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Fix duplicate IDs by appending suffix
|
||||||
|
const seenIds = new Map<string, number>()
|
||||||
|
const duplicateIds: string[] = []
|
||||||
|
|
||||||
|
// First pass: find duplicates
|
||||||
|
const idPattern = /\bid\s*=\s*["']([^"']+)["']/gi
|
||||||
|
let idMatch
|
||||||
|
while ((idMatch = idPattern.exec(fixed)) !== null) {
|
||||||
|
const id = idMatch[1]
|
||||||
|
seenIds.set(id, (seenIds.get(id) || 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find which IDs are duplicated
|
||||||
|
for (const [id, count] of seenIds) {
|
||||||
|
if (count > 1) duplicateIds.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: rename duplicates (keep first occurrence, rename others)
|
||||||
|
if (duplicateIds.length > 0) {
|
||||||
|
const idCounters = new Map<string, number>()
|
||||||
|
fixed = fixed.replace(/\bid\s*=\s*["']([^"']+)["']/gi, (match, id) => {
|
||||||
|
if (!duplicateIds.includes(id)) return match
|
||||||
|
|
||||||
|
const count = idCounters.get(id) || 0
|
||||||
|
idCounters.set(id, count + 1)
|
||||||
|
|
||||||
|
if (count === 0) return match // Keep first occurrence
|
||||||
|
|
||||||
|
// Rename subsequent occurrences
|
||||||
|
const newId = `${id}_dup${count}`
|
||||||
|
return match.replace(id, newId)
|
||||||
|
})
|
||||||
|
fixes.push(`Renamed ${duplicateIds.length} duplicate ID(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Fix empty id attributes by generating unique IDs
|
||||||
|
let emptyIdCount = 0
|
||||||
|
fixed = fixed.replace(
|
||||||
|
/<mxCell([^>]*)\sid\s*=\s*["']\s*["']([^>]*)>/g,
|
||||||
|
(_match, before, after) => {
|
||||||
|
emptyIdCount++
|
||||||
|
const newId = `cell_${Date.now()}_${emptyIdCount}`
|
||||||
|
return `<mxCell${before} id="${newId}"${after}>`
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (emptyIdCount > 0) {
|
||||||
|
fixes.push(`Generated ${emptyIdCount} missing ID(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. Aggressive: drop broken mxCell elements that can't be fixed
|
||||||
|
// Only do this if DOM parser still finds errors after all other fixes
|
||||||
|
if (typeof DOMParser !== "undefined") {
|
||||||
|
let droppedCells = 0
|
||||||
|
let maxIterations = MAX_DROP_ITERATIONS
|
||||||
|
while (maxIterations-- > 0) {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(fixed, "text/xml")
|
||||||
|
const parseError = doc.querySelector("parsererror")
|
||||||
|
if (!parseError) break // Valid now!
|
||||||
|
|
||||||
|
const errText = parseError.textContent || ""
|
||||||
|
const match = errText.match(/(\d+):\d+:/)
|
||||||
|
if (!match) break
|
||||||
|
|
||||||
|
const errLine = parseInt(match[1], 10) - 1
|
||||||
|
const lines = fixed.split("\n")
|
||||||
|
|
||||||
|
// Find the mxCell containing this error line
|
||||||
|
let cellStart = errLine
|
||||||
|
let cellEnd = errLine
|
||||||
|
|
||||||
|
// Go back to find <mxCell
|
||||||
|
while (cellStart > 0 && !lines[cellStart].includes("<mxCell")) {
|
||||||
|
cellStart--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go forward to find </mxCell> or />
|
||||||
|
while (cellEnd < lines.length - 1) {
|
||||||
|
if (
|
||||||
|
lines[cellEnd].includes("</mxCell>") ||
|
||||||
|
lines[cellEnd].trim().endsWith("/>")
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cellEnd++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove these lines
|
||||||
|
lines.splice(cellStart, cellEnd - cellStart + 1)
|
||||||
|
fixed = lines.join("\n")
|
||||||
|
droppedCells++
|
||||||
|
}
|
||||||
|
if (droppedCells > 0) {
|
||||||
|
fixes.push(`Dropped ${droppedCells} unfixable mxCell element(s)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fixed, fixes }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates XML and attempts to fix if invalid
|
||||||
|
* @param xml - The XML string to validate and potentially fix
|
||||||
|
* @returns Object with validation result, fixed XML if applicable, and fixes applied
|
||||||
|
*/
|
||||||
|
export function validateAndFixXml(xml: string): {
|
||||||
|
valid: boolean
|
||||||
|
error: string | null
|
||||||
|
fixed: string | null
|
||||||
|
fixes: string[]
|
||||||
|
} {
|
||||||
|
// First validation attempt
|
||||||
|
let error = validateMxCellStructure(xml)
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return { valid: true, error: null, fixed: null, fixes: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fix
|
||||||
|
const { fixed, fixes } = autoFixXml(xml)
|
||||||
|
|
||||||
|
// Validate the fixed version
|
||||||
|
error = validateMxCellStructure(fixed)
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return { valid: true, error: null, fixed, fixes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still invalid after fixes
|
||||||
|
return { valid: false, error, fixed: null, fixes }
|
||||||
|
}
|
||||||
|
|
||||||
export function extractDiagramXML(xml_svg_string: string): string {
|
export function extractDiagramXML(xml_svg_string: string): string {
|
||||||
try {
|
try {
|
||||||
// 1. Parse the SVG string (using built-in DOMParser in a browser-like environment)
|
// 1. Parse the SVG string (using built-in DOMParser in a browser-like environment)
|
||||||
|
|||||||
194
package-lock.json
generated
194
package-lock.json
generated
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
"@ai-sdk/amazon-bedrock": "^3.0.70",
|
||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/azure": "^2.0.69",
|
"@ai-sdk/azure": "^2.0.69",
|
||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
"@vercel/analytics": "^1.5.0",
|
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
"ai": "^5.0.89",
|
"ai": "^5.0.89",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
@@ -41,6 +40,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
@@ -78,14 +78,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ai-sdk/amazon-bedrock": {
|
"node_modules/@ai-sdk/amazon-bedrock": {
|
||||||
"version": "3.0.62",
|
"version": "3.0.70",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-3.0.62.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-3.0.70.tgz",
|
||||||
"integrity": "sha512-vVtndaj5zfHmgw8NSqN4baFDbFDTBZP6qufhKfqSNLtygEm8+8PL9XQX9urgzSzU3zp+zi3AmNNemvKLkkqblg==",
|
"integrity": "sha512-4NIBlwuS/iLKq2ynOqqyJ9imk/oyHuOzhBx88Bfm5I0ihQPKJ0dMMD1IKKuyDZvLRYKmlOEpa//P+/ZBp10drw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.50",
|
"@ai-sdk/anthropic": "2.0.56",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@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/eventstream-codec": "^4.0.1",
|
||||||
"@smithy/util-utf8": "^4.0.0",
|
"@smithy/util-utf8": "^4.0.0",
|
||||||
"aws4fetch": "^1.0.20"
|
"aws4fetch": "^1.0.20"
|
||||||
@@ -97,14 +97,48 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ai-sdk/anthropic": {
|
"node_modules/@ai-sdk/amazon-bedrock/node_modules/@ai-sdk/provider-utils": {
|
||||||
"version": "2.0.50",
|
"version": "3.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.50.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz",
|
||||||
"integrity": "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw==",
|
"integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@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": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -2489,9 +2523,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
||||||
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
|
"integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -2505,9 +2539,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz",
|
||||||
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
|
"integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2521,9 +2555,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz",
|
||||||
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
|
"integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2537,9 +2571,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz",
|
||||||
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
|
"integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2553,9 +2587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz",
|
||||||
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
|
"integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2569,9 +2603,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz",
|
||||||
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
|
"integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2585,9 +2619,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz",
|
||||||
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
|
"integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2601,9 +2635,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz",
|
||||||
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
|
"integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2617,9 +2651,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
|
||||||
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
|
"integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6028,44 +6062,6 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@vercel/analytics": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==",
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@remix-run/react": "^2",
|
|
||||||
"@sveltejs/kit": "^1 || ^2",
|
|
||||||
"next": ">= 13",
|
|
||||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
|
||||||
"svelte": ">= 4",
|
|
||||||
"vue": "^3",
|
|
||||||
"vue-router": "^4"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@remix-run/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@sveltejs/kit": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"next": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"svelte": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"vue": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"vue-router": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vercel/oidc": {
|
"node_modules/@vercel/oidc": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
|
||||||
@@ -8017,14 +8013,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -9203,6 +9200,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/jsx-ast-utils": {
|
||||||
"version": "3.3.5",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||||
@@ -10676,12 +10682,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
||||||
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
|
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.0.7",
|
"@next/env": "16.0.10",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
@@ -10694,14 +10700,14 @@
|
|||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "16.0.7",
|
"@next/swc-darwin-arm64": "16.0.10",
|
||||||
"@next/swc-darwin-x64": "16.0.7",
|
"@next/swc-darwin-x64": "16.0.10",
|
||||||
"@next/swc-linux-arm64-gnu": "16.0.7",
|
"@next/swc-linux-arm64-gnu": "16.0.10",
|
||||||
"@next/swc-linux-arm64-musl": "16.0.7",
|
"@next/swc-linux-arm64-musl": "16.0.10",
|
||||||
"@next/swc-linux-x64-gnu": "16.0.7",
|
"@next/swc-linux-x64-gnu": "16.0.10",
|
||||||
"@next/swc-linux-x64-musl": "16.0.7",
|
"@next/swc-linux-x64-musl": "16.0.10",
|
||||||
"@next/swc-win32-arm64-msvc": "16.0.7",
|
"@next/swc-win32-arm64-msvc": "16.0.10",
|
||||||
"@next/swc-win32-x64-msvc": "16.0.7",
|
"@next/swc-win32-x64-msvc": "16.0.10",
|
||||||
"sharp": "^0.34.4"
|
"sharp": "^0.34.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
"@ai-sdk/amazon-bedrock": "^3.0.70",
|
||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/azure": "^2.0.69",
|
"@ai-sdk/azure": "^2.0.69",
|
||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
"@vercel/analytics": "^1.5.0",
|
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
"ai": "^5.0.89",
|
"ai": "^5.0.89",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
@@ -45,6 +44,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
|
|||||||
12
vercel.json
Normal file
12
vercel.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"functions": {
|
||||||
|
"app/api/chat/route.ts": {
|
||||||
|
"memory": 512,
|
||||||
|
"maxDuration": 120
|
||||||
|
},
|
||||||
|
"app/api/**/route.ts": {
|
||||||
|
"memory": 256,
|
||||||
|
"maxDuration": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user