Compare commits

..

24 Commits

Author SHA1 Message Date
dayuan.jiang
09c556e4c3 chore: bump version to 0.4.1 2025-12-14 23:11:35 +09:00
Dayuan Jiang
ac1c2ce044 fix: remove overly aggressive message filtering on restore (#263)
The hasValidDiagramXml filter was deleting valid messages that had minor
XML issues. Error handling in handleDisplayChart now catches all errors,
so filtering is no longer needed - invalid XML just won't load the diagram
but the conversation is preserved.
2025-12-14 21:49:08 +09:00
Dayuan Jiang
78a77e102d fix: prevent browser crash during long streaming sessions (#262)
- Debounce streaming diagram updates (150ms) to reduce handleDisplayChart calls by 93%
- Debounce localStorage writes (1s) to prevent blocking main thread
- Limit diagramHistory to 20 entries to prevent unbounded memory growth
- Clean up debounce timeout on component unmount to prevent memory leaks
- Add console timing markers for performance profiling

Fixes #78
2025-12-14 21:23:14 +09:00
Dayuan Jiang
55821301dd fix: recover from invalid XML in localStorage on startup (#261)
When LLM generates invalid XML, the app previously saved corrupted messages
to localStorage, causing an unrecoverable crash loop on restart.

This fix validates messages when restoring from localStorage and filters out
any with invalid diagram XML. Users see a toast notification when corrupted
messages are removed.

Fixes #240
2025-12-14 20:01:24 +09:00
Dayuan Jiang
f743219c03 feat: add minimal style mode toggle for faster diagram generation (#260)
* feat: add minimal style mode toggle for faster diagram generation

- Add Minimal/Styled toggle switch in chat input UI
- When enabled, removes color/style instructions from system prompt
- Faster generation with plain black/white diagrams
- Improves XML auto-fix: handle foreign tags, extra closing tags, trailing garbage
- Fix isMxCellXmlComplete to strip Anthropic function-calling wrappers
- Add debug logging for truncation detection diagnosis

* fix: prevent false XML parse errors during streaming

- Escape unescaped & characters in convertToLegalXml() before DOMParser validation
- Only log console.error for final output, not during streaming updates
- Prevents Next.js dev mode error overlay from showing for expected streaming states
2025-12-14 19:38:40 +09:00
Ikko Eltociear Ashimine
ff34f0baf1 docs: update README.md (#257)
Azue -> Azure
2025-12-14 15:08:07 +09:00
Dayuan Jiang
0851b32b67 refactor: simplify LLM XML format to output bare mxCells only (#254)
* refactor: simplify LLM XML format to output bare mxCells only

- Update wrapWithMxFile() to always add root cells (id=0, id=1) automatically
- LLM now generates only mxCell elements starting from id=2 (no wrapper tags)
- Update system prompts and tool descriptions with new format instructions
- Update cached responses to remove root cells and wrapper tags
- Update truncation detection to check for complete mxCell endings
- Update documentation in xml_guide.md

* fix: address PR review issues for XML format refactor

- Fix critical bug: inconsistent truncation check using old </root> pattern
- Fix stale error message referencing </root> tag
- Add isMxCellXmlComplete() helper for consistent truncation detection
- Improve regex patterns to handle any attribute order in root cells
- Update wrapWithMxFile JSDoc to document root cell removal behavior

* fix: handle non-self-closing root cells in wrapWithMxFile regex
2025-12-14 14:04:44 +09:00
Dayuan Jiang
2e24071539 fix: shorten toast notification duration to 2 seconds (#253) 2025-12-14 13:04:18 +09:00
Dayuan Jiang
66bd0e5493 feat: add append_diagram tool and improve truncation handling (#252)
* feat: add append_diagram tool for truncation continuation

When LLM output hits maxOutputTokens mid-generation, instead of
failing with an error loop, the system now:

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

Key changes:
- Add append_diagram tool definition in route.ts
- Add append_diagram handler in chat-panel.tsx
- Track continuation mode separately from error mode
- Continuation mode has unlimited retries (not counted against limit)
- Error mode still limited to MAX_AUTO_RETRY_COUNT (1)
- Update system prompts to document append_diagram tool

* fix: show friendly message and yellow badge for truncated output

- Add yellow 'Truncated' badge in UI instead of red 'Error' when XML is incomplete
- Show friendly error message for toolUse.input is invalid errors
- Built on top of append_diagram continuation feature

* refactor: remove debug logs and simplify truncation state

- Remove all debug console.log statements
- Remove isContinuationModeRef, derive from partialXmlRef.current.length > 0

* docs: fix append_diagram instructions for consistency

- Change 'Do NOT include' to 'Do NOT start with' (clearer intent)
- Add <mxCell id="0"> to prohibited start patterns
- Change 'closing tags </root></mxGraphModel>' to just '</root>' (wrapWithMxFile handles the rest)
2025-12-14 12:34:34 +09:00
Dayuan Jiang
b33e09be05 feat: add XML auto-fix with refined validation logic (#247)
* feat: add XML auto-fix and improve validator accuracy

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

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

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

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

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

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

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

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

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

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

- Fix =&quot;...&quot; pattern where &quot; was used as delimiter instead of actual quotes
- Common in dashPattern attributes like dashPattern=&quot;1 1;&quot;
2025-12-13 23:31:01 +09:00
Dayuan Jiang
987dc9f026 fix: add configurable MAX_OUTPUT_TOKENS to prevent truncation (#251)
- Add MAX_OUTPUT_TOKENS env var (fixes output truncation with Bedrock)
- Remove redundant fixToolCallInputs function
- Remove jsonrepair dependency
- Consolidate duplicate lastMessage/userInputText variables
2025-12-13 23:28:41 +09:00
dayuan.jiang
6024443816 fix: improve XML auto-fix from 58.7% to 99% fix rate
Key improvements:
- Reorder CDATA removal to run before text-before-root check (+35 cases)
- Implement Gemini's backslash-quote fix with regex backreference
  Handles attr="value", value="text\"inner\"more", and mixed patterns
- Add aggressive drop-broken-cells fix for unfixable mxCell elements
  Iteratively removes cells causing DOM parse errors (up to 50)

Results on 9,411 XML dataset:
- 206 invalid XMLs detected
- 204 successfully fixed (99.0% fix rate)
- 2 unfixable (completely broken, need regeneration)
2025-12-13 16:11:48 +09:00
dayuan.jiang
4b838fd6d5 feat: add XML auto-fix and improve validator accuracy
- Add autoFixXml() to automatically repair common XML issues:
  - CDATA wrapper removal
  - Duplicate attribute removal
  - Unescaped & and < character escaping
  - Invalid entity reference fixing
  - Unclosed tag completion
  - Nested mxCell flattening
  - Duplicate ID renaming

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

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

Metrics: 99.77% accuracy, 94.06% recall, 94.4% auto-fix success rate
2025-12-13 16:11:47 +09:00
Dayuan Jiang
e321ba7959 chore: optimize Vercel costs by removing analytics and configuring functions (#238)
- Create vercel.json with optimized function settings:
  - Chat API: 512MB memory, 120s timeout
  - Other APIs: 256MB memory, 10s timeout
- Remove @vercel/analytics package and imports
- Reduce chat route maxDuration from 300s to 120s

Expected savings: $2-4/month, keeping costs under $20 included credit
2025-12-12 16:13:06 +09:00
Dayuan Jiang
aa15519fba fix: handle malformed XML from DeepSeek gracefully (#235)
* fix: handle malformed XML from DeepSeek gracefully

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

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

Fixes #220 #230 #231

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

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

* refactor: simplify toast deduplication with boolean flag

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

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

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

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

- Remove errorToastShownRef tracking (no longer needed)
- Add showToast parameter to handleDisplayChart (default false)
- Pass false during streaming (XML may be incomplete)
- Pass true at completion (show toast if final XML is malformed)
- Simpler and more explicit error handling
2025-12-12 14:52:25 +09:00
Dayuan Jiang
c2c65973f9 fix: revert UI and notify user when feedback submission fails (#237)
When feedback submission to the API fails, revert the optimistic
UI update and show a toast notification to inform the user.

Changes:
- Add toast import from sonner
- Change console.warn to console.error for proper logging
- Add toast.error() notification when API call fails
- Revert optimistic UI update by removing feedback from state

Previously, feedback submission failures were completely silent.
Users would see the thumbs-up/down visual feedback but their
feedback was never recorded. This creates a false sense that
the feedback was successfully submitted.

Now users are immediately notified when submission fails and
can retry their feedback.
2025-12-12 14:08:20 +09:00
Dayuan Jiang
b5db980f69 fix: add user feedback for clipboard copy failures (#236)
Add toast notification when clipboard copy operation fails,
so users know when their copy attempt was unsuccessful.

Changes:
- Add toast import from sonner
- Add toast.error() notification when clipboard copy fails
- Show clear message: "Failed to copy message. Please copy
  manually or check clipboard permissions."

Previously, clipboard copy failures were only indicated by a
brief visual state change (setCopyFailedMessageId), which users
might miss. Now users receive persistent feedback when copy
operations fail.
2025-12-12 14:06:53 +09:00
Shashi kiran M S
c9b60bfdb2 feat: Add a new chat button with a confirmation modal (#229)
* feat: Add a new chat button with a confirmation modal

* Fix for PR comments

* fix: add error handling and proper cleanup in handleNewChat

- Add try-catch for localStorage operations to handle quota exceeded,
  private browsing, and other storage errors
- Use handleFileChange([]) instead of setFiles([]) to properly clear
  pdfData Map alongside files
- Only show success toast when localStorage operations succeed
- Show warning toast if localStorage fails but chat state is cleared

---------

Co-authored-by: Dayuan Jiang <jdy.toh@gmail.com>
2025-12-12 10:08:18 +09:00
Twelveeee
f170bb41ae fix:custom model setting bug (#227)
* fix:custom model setting bug

* refactor: consolidate aiProvider checks for cleaner code

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-12 09:33:07 +09:00
Dayuan Jiang
a0f163fe9e fix: improve Azure provider auto-detection and validation (#223) (#225)
* fix: improve Azure provider auto-detection and validation (#223)

- Fix detectProvider() to only detect Azure when it has complete config
  (both AZURE_API_KEY and AZURE_RESOURCE_NAME or AZURE_BASE_URL)
- Add validation in validateProviderCredentials() for Azure to provide
  clear error messages when configuration is incomplete
- Update docs/ai-providers.md to clarify Azure requires resource name

* docs: add Azure reasoning options to documentation
2025-12-11 21:49:50 +09:00
try2love
8fd3830b9d Fix/clipboard (#189)
* bugfix: clipboard error bug

* fix: use try-catch fallback for clipboard API instead of feature detection

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-11 21:09:42 +09:00
Biki Kalita
77a25d2543 Persist processed tool calls to prevent replay after chat restore (#224) 2025-12-11 20:48:48 +09:00
Dayuan Jiang
b9da24dd6d fix: limit auto-retry to 3 attempts and enforce quota checks (#219)
- Add MAX_AUTO_RETRY_COUNT (3) to prevent infinite retry loops
- Check token and TPM limits before each auto-retry
- Reset retry counter on user-initiated messages
- Show toast notification when limits are reached

Fixes issue where models returning invalid tool inputs caused 45+ API
requests due to sendAutomaticallyWhen having no retry limit or quota check.
2025-12-11 17:56:40 +09:00
Dayuan Jiang
97cc0a07dc fix: disable history XML replacement by default (#217)
Some models (e.g. minimax) copy placeholder text instead of generating
fresh XML, causing tool call validation failures and infinite loops.

Added ENABLE_HISTORY_XML_REPLACE env var (default: false) to control
this behavior.
2025-12-11 17:36:18 +09:00
16 changed files with 2081 additions and 602 deletions

View File

@@ -207,7 +207,7 @@ All providers except AWS Bedrock and OpenRouter support custom endpoints.
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1. **Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
Note that `claude` series has trained on draw.io diagrams with cloud architecture logos like AWS, Azue, GCP. So if you want to create cloud architecture diagrams, this is the best choice. Note that `claude` series has trained on draw.io diagrams with cloud architecture logos like AWS, Azure, GCP. So if you want to create cloud architecture diagrams, this is the best choice.
## How It Works ## How It Works

View File

@@ -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({
@@ -229,6 +202,9 @@ async function handleChatRequest(req: Request): Promise<Response> {
modelId: req.headers.get("x-ai-model"), modelId: req.headers.get("x-ai-model"),
} }
// Read minimal style preference from header
const minimalStyle = req.headers.get("x-minimal-style") === "true"
// Get AI model with optional client overrides // Get AI model with optional client overrides
const { model, providerOptions, headers, modelId } = const { model, providerOptions, headers, modelId } =
getAIModel(clientOverrides) getAIModel(clientOverrides)
@@ -240,13 +216,7 @@ async function handleChatRequest(req: Request): Promise<Response> {
) )
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5) // Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
const systemMessage = getSystemPrompt(modelId) const systemMessage = getSystemPrompt(modelId, minimalStyle)
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 =
@@ -255,17 +225,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 +321,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 +360,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, {
@@ -396,20 +370,17 @@ ${lastMessageText}
tools: { tools: {
// Client-side tool that will be executed on the client // Client-side tool that will be executed on the client
display_diagram: { display_diagram: {
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags. description: `Display a diagram on draw.io. Pass ONLY the mxCell elements - wrapper tags and root cells are added automatically.
VALIDATION RULES (XML will be rejected if violated): VALIDATION RULES (XML will be rejected if violated):
1. All mxCell elements must be DIRECT children of <root> - never nested 1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
2. Every mxCell needs a unique id 2. Do NOT include root cells (id="0" or id="1") - they are added automatically
3. Every mxCell (except id="0") needs a valid parent attribute 3. All mxCell elements must be siblings - never nested
4. Edge source/target must reference existing cell IDs 4. Every mxCell needs a unique id (start from "2")
5. Escape special chars in values: &lt; &gt; &amp; &quot; 5. Every mxCell needs a valid parent attribute (use "1" for top-level)
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/> 6. Escape special chars in values: &lt; &gt; &amp; &quot;
Example with swimlanes and edges (note: all mxCells are siblings): Example (generate ONLY this - no wrapper tags):
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1"> <mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/> <mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell> </mxCell>
@@ -425,7 +396,6 @@ Example with swimlanes and edges (note: all mxCells are siblings):
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2"> <mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
</root>
Notes: Notes:
- For AWS diagrams, use **AWS 2025 icons**. - For AWS diagrams, use **AWS 2025 icons**.
@@ -467,6 +437,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 include any wrapper tags - just continue the mxCell elements
2. Continue from EXACTLY where your previous output stopped
3. Complete the remaining mxCell elements
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 +481,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

View File

@@ -81,16 +81,15 @@ Contains the actual diagram data.
## Root Cell Container: `<root>` ## Root Cell Container: `<root>`
Contains all the cells in the diagram. Contains all the cells in the diagram. **Note:** When generating diagrams, you only need to provide the mxCell elements - the root container and root cells (id="0", id="1") are added automatically.
**Example:** **Internal structure (auto-generated):**
```xml ```xml
<root> <root>
<mxCell id="0"/> <mxCell id="0"/> <!-- Auto-added -->
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/> <!-- Auto-added -->
<!-- Your mxCell elements go here (start from id="2") -->
<!-- Other cells go here -->
</root> </root>
``` ```
@@ -203,15 +202,15 @@ Draw.io files contain two special cells that are always present:
1. **Root Cell** (id = "0"): The parent of all cells 1. **Root Cell** (id = "0"): The parent of all cells
2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells 2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells
## Tips for Manually Creating Draw.io XML ## Tips for Creating Draw.io XML
1. Start with the basic structure (`mxfile`, `diagram`, `mxGraphModel`, `root`) 1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically
2. Always include the two special cells (id = "0" and id = "1") 2. Start IDs from "2" (id="0" and id="1" are reserved for root cells)
3. Assign unique and sequential IDs to all cells 3. Assign unique and sequential IDs to all cells
4. Define parent relationships correctly 4. Define parent relationships correctly (use parent="1" for top-level shapes)
5. Use `mxGeometry` elements to position shapes 5. Use `mxGeometry` elements to position shapes
6. For connectors, specify `source` and `target` attributes 6. For connectors, specify `source` and `target` attributes
7. **CRITICAL: All mxCell elements must be DIRECT children of `<root>`. NEVER nest mxCell inside another mxCell.** 7. **CRITICAL: All mxCell elements must be siblings. NEVER nest mxCell inside another mxCell.**
## Common Patterns ## Common Patterns

View File

@@ -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} />

View File

@@ -17,7 +17,13 @@ import { HistoryDialog } from "@/components/history-dialog"
import { ResetWarningModal } from "@/components/reset-warning-modal" import { ResetWarningModal } from "@/components/reset-warning-modal"
import { SaveDialog } from "@/components/save-dialog" import { SaveDialog } from "@/components/save-dialog"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { FilePreviewList } from "./file-preview-list" import { FilePreviewList } from "./file-preview-list"
@@ -129,6 +135,8 @@ interface ChatInputProps {
onToggleHistory?: (show: boolean) => void onToggleHistory?: (show: boolean) => void
sessionId?: string sessionId?: string
error?: Error | null error?: Error | null
minimalStyle?: boolean
onMinimalStyleChange?: (value: boolean) => void
} }
export function ChatInput({ export function ChatInput({
@@ -144,6 +152,8 @@ export function ChatInput({
onToggleHistory = () => {}, onToggleHistory = () => {},
sessionId, sessionId,
error = null, error = null,
minimalStyle = false,
onMinimalStyleChange = () => {},
}: ChatInputProps) { }: ChatInputProps) {
const { diagramHistory, saveDiagramToFile } = useDiagram() const { diagramHistory, saveDiagramToFile } = useDiagram()
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
@@ -343,6 +353,32 @@ export function ChatInput({
showHistory={showHistory} showHistory={showHistory}
onToggleHistory={onToggleHistory} onToggleHistory={onToggleHistory}
/> />
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5">
<Switch
id="minimal-style"
checked={minimalStyle}
onCheckedChange={onMinimalStyleChange}
className="scale-75"
/>
<label
htmlFor="minimal-style"
className={`text-xs cursor-pointer select-none ${
minimalStyle
? "text-primary font-medium"
: "text-muted-foreground"
}`}
>
{minimalStyle ? "Minimal" : "Styled"}
</label>
</div>
</TooltipTrigger>
<TooltipContent side="top">
Use minimal for faster generation (no colors)
</TooltipContent>
</Tooltip>
</div> </div>
{/* Right actions */} {/* Right actions */}

View File

@@ -19,8 +19,10 @@ 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,
@@ -29,8 +31,9 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { import {
convertToLegalXml, convertToLegalXml,
isMxCellXmlComplete,
replaceNodes, replaceNodes,
validateMxCellStructure, validateAndFixXml,
} from "@/lib/utils" } 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 +172,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 +183,7 @@ export function ChatMessageDisplay({
messages, messages,
setInput, setInput,
setFiles, setFiles,
processedToolCallsRef,
sessionId, sessionId,
onRegenerate, onRegenerate,
onEditMessage, onEditMessage,
@@ -187,7 +192,15 @@ 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
// Track the last processed XML per toolCallId to skip redundant processing during streaming
const lastProcessedXmlRef = useRef<Map<string, string>>(new Map())
// Debounce streaming diagram updates - store pending XML and timeout
const pendingXmlRef = useRef<string | null>(null)
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
)
const STREAMING_DEBOUNCE_MS = 150 // Only update diagram every 150ms during streaming
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>( const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
{}, {},
) )
@@ -213,9 +226,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
const textarea = document.createElement("textarea")
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) setCopyFailedMessageId(messageId)
setTimeout(() => setCopyFailedMessageId(null), 2000) setTimeout(() => setCopyFailedMessageId(null), 2000)
} finally {
document.body.removeChild(textarea)
}
} }
} }
@@ -243,33 +279,100 @@ 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) => {
console.time("perf:handleDisplayChart")
const currentXml = xml || "" const currentXml = xml || ""
const convertedXml = convertToLegalXml(currentXml) const convertedXml = convertToLegalXml(currentXml)
if (convertedXml !== previousXML.current) { if (convertedXml !== previousXML.current) {
// Parse and validate XML BEFORE calling replaceNodes
console.time("perf:DOMParser")
const parser = new DOMParser()
const testDoc = parser.parseFromString(convertedXml, "text/xml")
console.timeEnd("perf:DOMParser")
const parseError = testDoc.querySelector("parsererror")
if (parseError) {
// Use console.warn instead of console.error to avoid triggering
// Next.js dev mode error overlay for expected streaming states
// (partial XML during streaming is normal and will be fixed by subsequent updates)
if (showToast) {
// Only log as error and show toast if this is the final XML
console.error(
"[ChatMessageDisplay] Malformed XML detected in final output",
)
toast.error(
"AI generated invalid diagram XML. Please try regenerating.",
)
}
console.timeEnd("perf:handleDisplayChart")
return // Skip this update
}
try {
// If chartXML is empty, create a default mxfile structure to use with replaceNodes // If chartXML is empty, create a default mxfile structure to use with replaceNodes
// This ensures the XML is properly wrapped in mxfile/diagram/mxGraphModel format // This ensures the XML is properly wrapped in mxfile/diagram/mxGraphModel format
const baseXML = const baseXML =
chartXML || chartXML ||
`<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>` `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
console.time("perf:replaceNodes")
const replacedXML = replaceNodes(baseXML, convertedXml) const replacedXML = replaceNodes(baseXML, convertedXml)
console.timeEnd("perf:replaceNodes")
const validationError = validateMxCellStructure(replacedXML) // Validate and auto-fix the XML
if (!validationError) { console.time("perf:validateAndFixXml")
const validation = validateAndFixXml(replacedXML)
console.timeEnd("perf:validateAndFixXml")
if (validation.valid) {
previousXML.current = convertedXml previousXML.current = convertedXml
// Skip validation in loadDiagram since we already validated above // Use fixed XML if available, otherwise use original
onDisplayChart(replacedXML, true) const xmlToLoad = validation.fixed || replacedXML
} else { if (validation.fixes.length > 0) {
console.log( console.log(
"[ChatMessageDisplay] XML validation failed:", "[ChatMessageDisplay] Auto-fixed XML issues:",
validationError, 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.",
)
}
}
console.timeEnd("perf:handleDisplayChart")
} else {
console.timeEnd("perf:handleDisplayChart")
} }
}, },
[chartXML, onDisplayChart], [chartXML, onDisplayChart],
@@ -288,7 +391,17 @@ export function ChatMessageDisplay({
}, [editingMessageId]) }, [editingMessageId])
useEffect(() => { useEffect(() => {
messages.forEach((message) => { console.time("perf:message-display-useEffect")
let processedCount = 0
let skippedCount = 0
let debouncedCount = 0
// Only process the last message for streaming performance
// Previous messages are already processed and won't change
const messagesToProcess =
messages.length > 0 ? [messages[messages.length - 1]] : []
messagesToProcess.forEach((message) => {
if (message.parts) { if (message.parts) {
message.parts.forEach((part) => { message.parts.forEach((part) => {
if (part.type?.startsWith("tool-")) { if (part.type?.startsWith("tool-")) {
@@ -307,23 +420,82 @@ export function ChatMessageDisplay({
input?.xml input?.xml
) { ) {
const xml = input.xml as string const xml = input.xml as string
// Skip if XML hasn't changed since last processing
const lastXml =
lastProcessedXmlRef.current.get(toolCallId)
if (lastXml === xml) {
skippedCount++
return // Skip redundant processing
}
if ( if (
state === "input-streaming" || state === "input-streaming" ||
state === "input-available" state === "input-available"
) { ) {
handleDisplayChart(xml) // Debounce streaming updates - queue the XML and process after delay
pendingXmlRef.current = xml
if (!debounceTimeoutRef.current) {
// No pending timeout - set one up
debounceTimeoutRef.current = setTimeout(
() => {
const pendingXml =
pendingXmlRef.current
debounceTimeoutRef.current = null
pendingXmlRef.current = null
if (pendingXml) {
console.log(
"perf:debounced-handleDisplayChart executing",
)
handleDisplayChart(
pendingXml,
false,
)
lastProcessedXmlRef.current.set(
toolCallId,
pendingXml,
)
}
},
STREAMING_DEBOUNCE_MS,
)
}
debouncedCount++
} else if ( } else if (
state === "output-available" && state === "output-available" &&
!processedToolCalls.current.has(toolCallId) !processedToolCalls.current.has(toolCallId)
) { ) {
handleDisplayChart(xml) // Final output - process immediately (clear any pending debounce)
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
debounceTimeoutRef.current = null
pendingXmlRef.current = null
}
// Show toast only if final XML is malformed
handleDisplayChart(xml, true)
processedToolCalls.current.add(toolCallId) processedToolCalls.current.add(toolCallId)
// Clean up the ref entry - tool is complete, no longer needed
lastProcessedXmlRef.current.delete(toolCallId)
processedCount++
} }
} }
} }
}) })
} }
}) })
console.log(
`perf:message-display-useEffect processed=${processedCount} skipped=${skippedCount} debounced=${debouncedCount}`,
)
console.timeEnd("perf:message-display-useEffect")
// Cleanup: clear any pending debounce timeout on unmount
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
debounceTimeoutRef.current = null
}
}
}, [messages, handleDisplayChart]) }, [messages, handleDisplayChart])
const renderToolPart = (part: ToolPartLike) => { const renderToolPart = (part: ToolPartLike) => {
@@ -373,11 +545,23 @@ export function ChatMessageDisplay({
Complete Complete
</span> </span>
)} )}
{state === "output-error" && ( {state === "output-error" &&
(() => {
// Check if this is a truncation (incomplete XML) vs real error
const isTruncated =
(toolName === "display_diagram" ||
toolName === "append_diagram") &&
!isMxCellXmlComplete(input?.xml)
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"> <span className="text-xs font-medium text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
Error Error
</span> </span>
)} )
})()}
{input && Object.keys(input).length > 0 && ( {input && Object.keys(input).length > 0 && (
<button <button
type="button" type="button"
@@ -410,11 +594,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} (() => {
const isTruncated =
(toolName === "display_diagram" ||
toolName === "append_diagram") &&
!isMxCellXmlComplete(input?.xml)
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> </div>
) )
} }

View File

@@ -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"
@@ -24,7 +26,7 @@ import { findCachedResponse } from "@/lib/cached-responses"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { type FileData, useFileProcessor } from "@/lib/use-file-processor" import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
import { useQuotaManager } from "@/lib/use-quota-manager" import { useQuotaManager } from "@/lib/use-quota-manager"
import { formatXML, wrapWithMxFile } from "@/lib/utils" import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
import { ChatMessageDisplay } from "./chat-message-display" import { ChatMessageDisplay } from "./chat-message-display"
// localStorage keys for persistence // localStorage keys for persistence
@@ -38,6 +40,7 @@ interface MessagePart {
type: string type: string
state?: string state?: string
toolName?: string toolName?: string
input?: { xml?: string; [key: string]: unknown }
[key: string]: unknown [key: string]: unknown
} }
@@ -61,31 +64,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 +82,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 +145,8 @@ 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)
const [minimalStyle, setMinimalStyle] = useState(false)
// Check config on mount // Check config on mount
useEffect(() => { useEffect(() => {
@@ -223,6 +192,25 @@ 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())
// Debounce timeout for localStorage writes (prevents blocking during streaming)
const localStorageDebounceRef = useRef<ReturnType<
typeof setTimeout
> | null>(null)
const xmlStorageDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
)
const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second
const { const {
messages, messages,
sendMessage, sendMessage,
@@ -244,14 +232,50 @@ 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) {
// DEBUG: Log raw input to diagnose false truncation detection
console.log( console.log(
`[display_diagram] Received XML length: ${xml.length}`, "[display_diagram] XML ending (last 100 chars):",
xml.slice(-100),
) )
console.log("[display_diagram] XML length:", xml.length)
// Check if XML is truncated (incomplete mxCell indicates truncated output)
const isTruncated = !isMxCellXmlComplete(xml)
console.log("[display_diagram] isTruncated:", isTruncated)
if (isTruncated) {
// 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 include wrapper tags or root cells (id="0", id="1")
- Start from EXACTLY where you stopped
- Complete all remaining mxCell elements`,
})
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 +301,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 +329,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 +369,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 +390,87 @@ ${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
// LLM should only output bare mxCells now, so wrapper tags indicate error
const trimmed = xml.trim()
const isFreshStart =
trimmed.startsWith("<mxGraphModel") ||
trimmed.startsWith("<root") ||
trimmed.startsWith("<mxfile") ||
trimmed.startsWith('<mxCell id="0"') ||
trimmed.startsWith('<mxCell id="1"')
if (isFreshStart) {
addToolOutput({
tool: "append_diagram",
toolCallId: toolCall.toolCallId,
state: "output-error",
errorText: `ERROR: You started fresh with wrapper tags. Do NOT include wrapper tags or root cells (id="0", id="1").
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 (last mxCell is complete)
const isComplete = isMxCellXmlComplete(partialXmlRef.current)
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 (mxCell not closed). Call append_diagram again to continue.
Current ending:
\`\`\`
${partialXmlRef.current.slice(-500)}
\`\`\`
Continue from EXACTLY where you stopped.`,
})
}
} }
}, },
onError: (error) => { onError: (error) => {
@@ -399,6 +489,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 +522,11 @@ 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
// DEBUG: Log finish reason to diagnose truncation
console.log("[onFinish] finishReason:", metadata?.finishReason)
console.log("[onFinish] metadata:", metadata)
if (metadata) { if (metadata) {
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true) // Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
const inputTokens = Number.isFinite(metadata.inputTokens) const inputTokens = Number.isFinite(metadata.inputTokens)
@@ -441,8 +542,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
@@ -481,6 +632,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
} }
} catch (error) { } catch (error) {
console.error("Failed to restore from localStorage:", error) console.error("Failed to restore from localStorage:", error)
// On complete failure, clear storage to allow recovery
localStorage.removeItem(STORAGE_MESSAGES_KEY)
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
toast.error("Session data was corrupted. Starting fresh.")
} }
}, [setMessages]) }, [setMessages])
@@ -525,32 +680,71 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
}, 500) }, 500)
}, [isDrawioReady, onDisplayChart]) }, [isDrawioReady, onDisplayChart])
// Save messages to localStorage whenever they change // Save messages to localStorage whenever they change (debounced to prevent blocking during streaming)
useEffect(() => { useEffect(() => {
if (!hasRestoredRef.current) return if (!hasRestoredRef.current) return
// Clear any pending save
if (localStorageDebounceRef.current) {
clearTimeout(localStorageDebounceRef.current)
}
// Debounce: save after 1 second of no changes
localStorageDebounceRef.current = setTimeout(() => {
try { try {
localStorage.setItem(STORAGE_MESSAGES_KEY, JSON.stringify(messages)) console.time("perf:localStorage-messages")
localStorage.setItem(
STORAGE_MESSAGES_KEY,
JSON.stringify(messages),
)
console.timeEnd("perf:localStorage-messages")
} catch (error) { } catch (error) {
console.error("Failed to save messages to localStorage:", error) console.error("Failed to save messages to localStorage:", error)
} }
}, LOCAL_STORAGE_DEBOUNCE_MS)
// Cleanup on unmount
return () => {
if (localStorageDebounceRef.current) {
clearTimeout(localStorageDebounceRef.current)
}
}
}, [messages]) }, [messages])
// Save diagram XML to localStorage whenever it changes // Save diagram XML to localStorage whenever it changes (debounced)
useEffect(() => { useEffect(() => {
if (!canSaveDiagram) return if (!canSaveDiagram) return
if (chartXML && chartXML.length > 300) { if (!chartXML || chartXML.length <= 300) return
// Clear any pending save
if (xmlStorageDebounceRef.current) {
clearTimeout(xmlStorageDebounceRef.current)
}
// Debounce: save after 1 second of no changes
xmlStorageDebounceRef.current = setTimeout(() => {
console.time("perf:localStorage-xml")
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML) localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
console.timeEnd("perf:localStorage-xml")
}, LOCAL_STORAGE_DEBOUNCE_MS)
return () => {
if (xmlStorageDebounceRef.current) {
clearTimeout(xmlStorageDebounceRef.current)
}
} }
}, [chartXML, canSaveDiagram]) }, [chartXML, canSaveDiagram])
// Save XML snapshots to localStorage whenever they change // Save XML snapshots to localStorage whenever they change
const saveXmlSnapshots = useCallback(() => { const saveXmlSnapshots = useCallback(() => {
try { try {
console.time("perf:localStorage-snapshots")
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries()) const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
localStorage.setItem( localStorage.setItem(
STORAGE_XML_SNAPSHOTS_KEY, STORAGE_XML_SNAPSHOTS_KEY,
JSON.stringify(snapshotsArray), JSON.stringify(snapshotsArray),
) )
console.timeEnd("perf:localStorage-snapshots")
} catch (error) { } catch (error) {
console.error( console.error(
"Failed to save XML snapshots to localStorage:", "Failed to save XML snapshots to localStorage:",
@@ -696,6 +890,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 +979,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 +993,17 @@ 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 && { ...(config.aiBaseUrl && {
"x-ai-base-url": config.aiBaseUrl, "x-ai-base-url": config.aiBaseUrl,
}), }),
...(config.aiApiKey && { "x-ai-api-key": config.aiApiKey }), ...(config.aiApiKey && {
"x-ai-api-key": config.aiApiKey,
}),
...(config.aiModel && { "x-ai-model": config.aiModel }), ...(config.aiModel && { "x-ai-model": config.aiModel }),
}),
...(minimalStyle && {
"x-minimal-style": "true",
}),
}, },
}, },
) )
@@ -960,6 +1189,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
style: { style: {
maxWidth: "480px", maxWidth: "480px",
}, },
duration: 2000,
}} }}
/> />
{/* Header */} {/* Header */}
@@ -1010,6 +1240,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 +1294,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 +1311,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}
@@ -1092,6 +1319,8 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
onToggleHistory={setShowHistory} onToggleHistory={setShowHistory}
sessionId={sessionId} sessionId={sessionId}
error={error} error={error}
minimalStyle={minimalStyle}
onMinimalStyleChange={setMinimalStyle}
/> />
</footer> </footer>
@@ -1104,6 +1333,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>
) )
} }

View File

@@ -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,24 +86,44 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
chart: string, chart: string,
skipValidation?: boolean, skipValidation?: boolean,
): string | null => { ): string | null => {
console.time("perf:loadDiagram")
let xmlToLoad = chart
// 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) console.time("perf:loadDiagram-validation")
if (validationError) { const validation = validateAndFixXml(chart)
console.warn("[loadDiagram] Validation error:", validationError) console.timeEnd("perf:loadDiagram-validation")
return validationError if (!validation.valid) {
console.warn(
"[loadDiagram] Validation error:",
validation.error,
)
console.timeEnd("perf:loadDiagram")
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) {
console.time("perf:drawio-iframe-load")
drawioRef.current.load({ drawioRef.current.load({
xml: chart, xml: xmlToLoad,
}) })
console.timeEnd("perf:drawio-iframe-load")
} }
console.timeEnd("perf:loadDiagram")
return null return null
} }
@@ -125,14 +145,20 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
setLatestSvg(data.data) setLatestSvg(data.data)
// Only add to history if this was a user-initiated export // Only add to history if this was a user-initiated export
// Limit to 20 entries to prevent memory leaks during long sessions
const MAX_HISTORY_SIZE = 20
if (expectHistoryExportRef.current) { if (expectHistoryExportRef.current) {
setDiagramHistory((prev) => [ setDiagramHistory((prev) => {
const newHistory = [
...prev, ...prev,
{ {
svg: data.data, svg: data.data,
xml: extractedXML, xml: extractedXML,
}, },
]) ]
// Keep only the last MAX_HISTORY_SIZE entries (circular buffer)
return newHistory.slice(-MAX_HISTORY_SIZE)
})
expectHistoryExportRef.current = false expectHistoryExportRef.current = false
} }

View File

@@ -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

View File

@@ -371,8 +371,17 @@ function detectProvider(): ProviderName | null {
continue continue
} }
if (process.env[envVar]) { if (process.env[envVar]) {
// 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) configuredProviders.push(provider as ProviderName)
} }
} else {
configuredProviders.push(provider as ProviderName)
}
}
} }
if (configuredProviders.length === 1) { if (configuredProviders.length === 1) {
@@ -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.`,
)
}
}
} }
/** /**

View File

@@ -9,12 +9,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
promptText: promptText:
"Give me a **animated connector** diagram of transformer's architecture", "Give me a **animated connector** diagram of transformer's architecture",
hasImage: false, hasImage: false,
xml: `<root> xml: `<mxCell id="title" value="Transformer Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="title" value="Transformer Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="300" y="20" width="250" height="30" as="geometry"/> <mxGeometry x="300" y="20" width="250" height="30" as="geometry"/>
</mxCell> </mxCell>
@@ -254,18 +249,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="output_label" value="Outputs&#xa;(shifted right)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;" vertex="1" parent="1"> <mxCell id="output_label" value="Outputs&#xa;(shifted right)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="660" y="530" width="100" height="30" as="geometry"/> <mxGeometry x="660" y="530" width="100" height="30" as="geometry"/>
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Replicate this in aws style", promptText: "Replicate this in aws style",
hasImage: true, hasImage: true,
xml: `<root> xml: `<mxCell id="2" value="AWS" style="sketch=0;outlineConnect=0;gradientColor=none;html=1;whiteSpace=wrap;fontSize=12;fontStyle=0;container=1;pointerEvents=0;collapsible=0;recursiveResize=0;shape=mxgraph.aws4.group;grIcon=mxgraph.aws4.group_aws_cloud;strokeColor=#232F3E;fillColor=none;verticalAlign=top;align=left;spacingLeft=30;fontColor=#232F3E;dashed=0;rounded=1;arcSize=5;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="AWS" style="sketch=0;outlineConnect=0;gradientColor=none;html=1;whiteSpace=wrap;fontSize=12;fontStyle=0;container=1;pointerEvents=0;collapsible=0;recursiveResize=0;shape=mxgraph.aws4.group;grIcon=mxgraph.aws4.group_aws_cloud;strokeColor=#232F3E;fillColor=none;verticalAlign=top;align=left;spacingLeft=30;fontColor=#232F3E;dashed=0;rounded=1;arcSize=5;" vertex="1" parent="1">
<mxGeometry x="340" y="40" width="880" height="520" as="geometry"/> <mxGeometry x="340" y="40" width="880" height="520" as="geometry"/>
</mxCell> </mxCell>
@@ -324,18 +313,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxPoint x="700" y="350" as="sourcePoint"/> <mxPoint x="700" y="350" as="sourcePoint"/>
<mxPoint x="750" y="300" as="targetPoint"/> <mxPoint x="750" y="300" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Replicate this flowchart.", promptText: "Replicate this flowchart.",
hasImage: true, hasImage: true,
xml: `<root> xml: `<mxCell id="2" value="Lamp doesn't work" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffcccc;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Lamp doesn't work" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffcccc;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="140" y="40" width="180" height="60" as="geometry"/> <mxGeometry x="140" y="40" width="180" height="60" as="geometry"/>
</mxCell> </mxCell>
@@ -391,16 +374,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="12" value="Repair lamp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#99ff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1"> <mxCell id="12" value="Repair lamp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#99ff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="130" y="650" width="200" height="60" as="geometry"/> <mxGeometry x="130" y="650" width="200" height="60" as="geometry"/>
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Summarize this paper as a diagram", promptText: "Summarize this paper as a diagram",
hasImage: true, hasImage: true,
xml: ` <root> xml: `<mxCell id="title_bg" parent="1"
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="title_bg" parent="1"
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1a237e;strokeColor=none;arcSize=8;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1a237e;strokeColor=none;arcSize=8;"
value="" vertex="1"> value="" vertex="1">
<mxGeometry height="80" width="720" x="40" y="20" as="geometry" /> <mxGeometry height="80" width="720" x="40" y="20" as="geometry" />
@@ -751,18 +730,12 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
value="Foundational technique for modern LLM reasoning - inspired many follow-up works including Self-Consistency, Tree-of-Thought, etc." value="Foundational technique for modern LLM reasoning - inspired many follow-up works including Self-Consistency, Tree-of-Thought, etc."
vertex="1"> vertex="1">
<mxGeometry height="55" width="230" x="530" y="600" as="geometry" /> <mxGeometry height="55" width="230" x="530" y="600" as="geometry" />
</mxCell> </mxCell>`,
</root>`,
}, },
{ {
promptText: "Draw a cat for me", promptText: "Draw a cat for me",
hasImage: false, hasImage: false,
xml: `<root> xml: `<mxCell id="2" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="300" y="150" width="120" height="120" as="geometry"/> <mxGeometry x="300" y="150" width="120" height="120" as="geometry"/>
</mxCell> </mxCell>
@@ -902,9 +875,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxPoint x="235" y="290"/> <mxPoint x="235" y="290"/>
</Array> </Array>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>`,
</root>`,
}, },
] ]

View 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
@@ -97,22 +104,21 @@ When using edit_diagram tool:
## Draw.io XML Structure Reference ## Draw.io XML Structure Reference
Basic structure: **IMPORTANT:** You only generate the mxCell elements. The wrapper structure and root cells (id="0", id="1") are added automatically.
Example - generate ONLY this:
\`\`\`xml \`\`\`xml
<mxGraphModel> <mxCell id="2" value="Label" style="rounded=1;" vertex="1" parent="1">
<root> <mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
<mxCell id="0"/> </mxCell>
<mxCell id="1" parent="0"/>
</root>
</mxGraphModel>
\`\`\` \`\`\`
Note: All other mxCell elements go as siblings after id="1".
CRITICAL RULES: CRITICAL RULES:
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/> 1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell 2. Do NOT include root cells (id="0" or id="1") - they are added automatically
3. Use unique sequential IDs for all cells (start from "2" for user content) 3. ALL mxCell elements must be siblings - NEVER nest mxCell inside another mxCell
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements 4. Use unique sequential IDs starting from "2"
5. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
Shape (vertex) example: Shape (vertex) example:
\`\`\`xml \`\`\`xml
@@ -126,12 +132,90 @@ Connector (edge) example:
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4"> <mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
### Edge Routing Rules:
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
**Rule 1: NEVER let multiple edges share the same path**
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
- Every edge MUST have these 4 attributes set in the style
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
- Before creating an edge, identify ALL shapes positioned between source and target
- If any shape is in the direct path, you MUST use waypoints to route around it
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
- NEVER draw a line that visually crosses over another shape's bounding box
**Rule 5: Plan layout strategically BEFORE generating XML**
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
- Space shapes 150-200px apart to create clear routing channels for edges
- Mentally trace each edge: "What shapes are between source and target?"
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
**Rule 6: Use multiple waypoints for complex routing**
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
- Each direction change needs a waypoint (corner point)
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
**Rule 7: Choose NATURAL connection points based on flow direction**
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
- For DIAGONAL connections: use the side closest to the target, not corners
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
**Before generating XML, mentally verify:**
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
\`\`\` \`\`\`
`
// Style instructions - only included when minimalStyle is false
const STYLE_INSTRUCTIONS = `
Common styles: Common styles:
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex - Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle - Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right - Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
`
// Minimal style instruction - skip styling and focus on layout (prepended to prompt for emphasis)
const MINIMAL_STYLE_INSTRUCTION = `
## ⚠️ MINIMAL STYLE MODE ACTIVE ⚠️
### No Styling - Plain Black/White Only
- NO fillColor, NO strokeColor, NO rounded, NO fontSize, NO fontStyle
- NO color attributes (no hex colors like #ff69b4)
- Style: "whiteSpace=wrap;html=1;" for shapes, "html=1;endArrow=classic;" for edges
- IGNORE all color/style examples below
### Container/Group Shapes - MUST be Transparent
- For container shapes (boxes that contain other shapes): use "fillColor=none;" to make background transparent
- This prevents containers from covering child elements
- Example: style="whiteSpace=wrap;html=1;fillColor=none;" for container rectangles
### Focus on Layout Quality
Since we skip styling, STRICTLY follow the "Edge Routing Rules" section below:
- SPACING: Minimum 50px gap between all elements
- NO OVERLAPS: Elements and edges must never overlap
- Follow ALL 7 Edge Routing Rules for arrow positioning
- Use waypoints to route edges AROUND obstacles
- Use different exitY/entryY values for multiple edges between same nodes
` `
@@ -144,18 +228,15 @@ const EXTENDED_ADDITIONS = `
### display_diagram Details ### display_diagram Details
**VALIDATION RULES** (XML will be rejected if violated): **VALIDATION RULES** (XML will be rejected if violated):
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements 1. Generate ONLY mxCell elements - wrapper tags and root cells are added automatically
2. Every mxCell needs a unique id attribute 2. All mxCell elements must be siblings - never nested inside other mxCell elements
3. Every mxCell (except id="0") needs a valid parent attribute referencing an existing cell 3. Every mxCell needs a unique id attribute (start from "2")
4. Edge source/target attributes must reference existing cell IDs 4. Every mxCell needs a valid parent attribute (use "1" for top-level, or container-id for grouped)
5. Escape special characters in values: &lt; for <, &gt; for >, &amp; for &, &quot; for " 5. Edge source/target attributes must reference existing cell IDs
6. Always start with the two root cells: <mxCell id="0"/><mxCell id="1" parent="0"/> 6. Escape special characters in values: &lt; for <, &gt; for >, &amp; for &, &quot; for "
**Example with swimlanes and edges** (note: all mxCells are siblings under <root>): **Example with swimlanes and edges** (generate ONLY this - no wrapper tags):
\`\`\`xml \`\`\`xml
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1"> <mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/> <mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell> </mxCell>
@@ -171,9 +252,20 @@ const EXTENDED_ADDITIONS = `
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2"> <mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
</root>
\`\`\` \`\`\`
### append_diagram Details
**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 include any wrapper tags - just continue the mxCell elements
2. Continue from EXACTLY where your previous output stopped
3. Complete the remaining mxCell elements
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:**
@@ -243,53 +335,6 @@ If edit_diagram fails with "pattern not found":
### Edge Routing Rules:
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
**Rule 1: NEVER let multiple edges share the same path**
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
- Every edge MUST have these 4 attributes set in the style
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
- Before creating an edge, identify ALL shapes positioned between source and target
- If any shape is in the direct path, you MUST use waypoints to route around it
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
- NEVER draw a line that visually crosses over another shape's bounding box
**Rule 5: Plan layout strategically BEFORE generating XML**
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
- Space shapes 150-200px apart to create clear routing channels for edges
- Mentally trace each edge: "What shapes are between source and target?"
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
**Rule 6: Use multiple waypoints for complex routing**
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
- Each direction change needs a waypoint (corner point)
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
**Rule 7: Choose NATURAL connection points based on flow direction**
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
- For DIAGONAL connections: use the side closest to the target, not corners
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
**Before generating XML, mentally verify:**
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
## Edge Examples ## Edge Examples
@@ -343,12 +388,16 @@ const EXTENDED_PROMPT_MODEL_PATTERNS = [
] ]
/** /**
* Get the appropriate system prompt based on the model ID * Get the appropriate system prompt based on the model ID and style preference
* Uses extended prompt for Opus 4.5 and Haiku 4.5 which have 4000 token cache minimum * Uses extended prompt for Opus 4.5 and Haiku 4.5 which have 4000 token cache minimum
* @param modelId - The AI model ID from environment * @param modelId - The AI model ID from environment
* @param minimalStyle - If true, removes style instructions to save tokens
* @returns The system prompt string * @returns The system prompt string
*/ */
export function getSystemPrompt(modelId?: string): string { export function getSystemPrompt(
modelId?: string,
minimalStyle?: boolean,
): string {
const modelName = modelId || "AI" const modelName = modelId || "AI"
let prompt: string let prompt: string
@@ -369,5 +418,15 @@ export function getSystemPrompt(modelId?: string): string {
prompt = DEFAULT_SYSTEM_PROMPT prompt = DEFAULT_SYSTEM_PROMPT
} }
// Add style instructions based on preference
// Minimal style: prepend instruction at START (more prominent)
// Normal style: append at end
if (minimalStyle) {
console.log(`[System Prompt] Minimal style mode ENABLED`)
prompt = MINIMAL_STYLE_INSTRUCTION + prompt
} else {
prompt += STYLE_INSTRUCTIONS
}
return prompt.replace("{{MODEL_NAME}}", modelName) return prompt.replace("{{MODEL_NAME}}", modelName)
} }

File diff suppressed because it is too large Load Diff

194
package-lock.json generated
View File

@@ -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": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.0", "version": "0.4.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -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
View File

@@ -0,0 +1,12 @@
{
"functions": {
"app/api/chat/route.ts": {
"memory": 512,
"maxDuration": 120
},
"app/api/**/route.ts": {
"memory": 256,
"maxDuration": 10
}
}
}