Compare commits

..

3 Commits

Author SHA1 Message Date
dayuan.jiang
d36bf127d9 fix: remove deprecated instrumentationHook (enabled by default in Next.js 15) 2025-12-04 00:15:56 +09:00
dayuan.jiang
30b598d960 feat: add optional Langfuse observability integration
- Add session tracking with unique sessionId per conversation
- Add user tracking via IP address (x-forwarded-for header)
- Make telemetry conditional - only enabled if LANGFUSE_PUBLIC_KEY is set
- Add environment variable validation in instrumentation.ts
- Add sessionId validation (type check + 200 char limit)
- Update env.example with Langfuse configuration docs
- Remove unused langfuse-vercel and @vercel/otel packages
2025-12-04 00:13:42 +09:00
dayuan.jiang
d84edb529c feat: integrate Langfuse for LLM observability
- Add instrumentation.ts with Langfuse OpenTelemetry exporter
- Enable experimental telemetry on streamText calls
- Add instrumentationHook to Next.js config
- Install required dependencies (@vercel/otel, langfuse-vercel, etc.)
2025-12-03 23:41:20 +09:00
23 changed files with 1131 additions and 4046 deletions

1
.gitignore vendored
View File

@@ -41,4 +41,3 @@ yarn-error.log*
next-env.d.ts
push-via-ec2.sh
.claude/settings.local.json
.playwright-mcp/

190
LICENSE
View File

@@ -1,190 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024 Dayuan Jiang
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,22 +0,0 @@
version: 1
frontend:
phases:
preBuild:
commands:
- npm ci --cache .npm --prefer-offline
build:
commands:
# Write env vars to .env.production for Next.js SSR runtime
- env | grep -e AI_MODEL >> .env.production
- env | grep -e AI_PROVIDER >> .env.production
- env | grep -e OPENAI_API_KEY >> .env.production
- env | grep -e NEXT_PUBLIC_ >> .env.production
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- .next/cache/**/*
- .npm/**/*

View File

@@ -1,7 +1,6 @@
import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai';
import { getAIModel } from '@/lib/ai-providers';
import { findCachedResponse } from '@/lib/cached-responses';
import { getSystemPrompt } from '@/lib/system-prompts';
import { z } from "zod";
export const maxDuration = 300;
@@ -29,158 +28,266 @@ function createCachedStreamResponse(xml: string): Response {
return createUIMessageStreamResponse({ stream });
}
// Inner handler function
async function handleChatRequest(req: Request): Promise<Response> {
const { messages, xml } = await req.json();
export async function POST(req: Request) {
try {
const { messages, xml, sessionId } = await req.json();
// === CACHE CHECK START ===
const isFirstMessage = messages.length === 1;
const isEmptyDiagram = !xml || xml.trim() === '' || isMinimalDiagram(xml);
// Get user IP for Langfuse tracking
const forwardedFor = req.headers.get('x-forwarded-for');
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
if (isFirstMessage && isEmptyDiagram) {
const lastMessage = messages[0];
const textPart = lastMessage.parts?.find((p: any) => p.type === 'text');
const filePart = lastMessage.parts?.find((p: any) => p.type === 'file');
// Validate sessionId for Langfuse (must be string, max 200 chars)
const validSessionId = sessionId && typeof sessionId === 'string' && sessionId.length <= 200
? sessionId
: undefined;
const cached = findCachedResponse(textPart?.text || '', !!filePart);
// === CACHE CHECK START ===
const isFirstMessage = messages.length === 1;
const isEmptyDiagram = !xml || xml.trim() === '' || isMinimalDiagram(xml);
if (cached) {
console.log('[Cache] Returning cached response for:', textPart?.text);
return createCachedStreamResponse(cached.xml);
if (isFirstMessage && isEmptyDiagram) {
const lastMessage = messages[0];
const textPart = lastMessage.parts?.find((p: any) => p.type === 'text');
const filePart = lastMessage.parts?.find((p: any) => p.type === 'file');
const cached = findCachedResponse(textPart?.text || '', !!filePart);
if (cached) {
console.log('[Cache] Returning cached response for:', textPart?.text);
return createCachedStreamResponse(cached.xml);
}
}
}
// === CACHE CHECK END ===
// === CACHE CHECK END ===
// Get AI model from environment configuration
const { model, providerOptions, headers, modelId } = getAIModel();
const systemMessage = `
You are an expert diagram creation assistant specializing in draw.io XML generation.
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
You can see the image that user uploaded.
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
const systemMessage = getSystemPrompt(modelId);
You utilize the following tools:
---Tool1---
tool name: display_diagram
description: Display a NEW diagram on draw.io. Use this when creating a diagram from scratch or when major structural changes are needed.
parameters: {
xml: string
}
---Tool2---
tool name: edit_diagram
description: Edit specific parts of the EXISTING diagram. Use this when making small targeted changes like adding/removing elements, changing labels, or adjusting properties. This is more efficient than regenerating the entire diagram.
parameters: {
edits: Array<{search: string, replace: string}>
}
---End of tools---
const lastMessage = messages[messages.length - 1];
IMPORTANT: Choose the right tool:
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
- Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items
// Extract text from the last message parts
const lastMessageText = lastMessage.parts?.find((part: any) => part.type === 'text')?.text || '';
Core capabilities:
- Generate valid, well-formed XML strings for draw.io diagrams
- Create professional flowcharts, mind maps, entity diagrams, and technical illustrations
- Convert user descriptions into visually appealing diagrams using basic shapes and connectors
- Apply proper spacing, alignment and visual hierarchy in diagram layouts
- Adapt artistic concepts into abstract diagram representations using available shapes
- Optimize element positioning to prevent overlapping and maintain readability
- Structure complex systems into clear, organized visual components
// Extract file parts (images) from the last message
const fileParts = lastMessage.parts?.filter((part: any) => part.type === 'file') || [];
Layout constraints:
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
- Maximum width for containers (like AWS cloud boxes): 700 pixels
- Maximum height for containers: 550 pixels
- Use compact, efficient layouts that fit the entire diagram in one view
- Start positioning from reasonable margins (e.g., x=40, y=40) and keep elements grouped closely
- For large diagrams with many elements, use vertical stacking or grid layouts that stay within bounds
- Avoid spreading elements too far apart horizontally - users should see the complete diagram without a page break line
// User input only - XML is now in a separate cached system message
const formattedUserInput = `User input:
Note that:
- Use proper tool calls to generate or edit diagrams;
- never return raw XML in text responses,
- never use display_diagram to generate messages that you want to send user directly. e.g. to generate a "hello" text box when you want to greet user.
- Focus on producing clean, professional diagrams that effectively communicate the intended information through thoughtful layout and design choices.
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
- Return XML only via tool calls, never in text responses.
- If user asks you to replicate a diagram based on an image, remember to match the diagram style and layout as closely as possible. Especially, pay attention to the lines and shapes, for example, if the lines are straight or curved, and if the shapes are rounded or square.
- Note that when you need to generate diagram about aws architecture, use **AWS 2025 icons**.
When using edit_diagram tool:
- Keep edits minimal - only include the specific line being changed plus 1-2 context lines
- Example GOOD edit: {"search": " <mxCell id=\"2\" value=\"Old Text\">", "replace": " <mxCell id=\"2\" value=\"New Text\">"}
- Example BAD edit: Including 10+ unchanged lines just to change one attribute
- For multiple changes, use separate edits: [{"search": "line1", "replace": "new1"}, {"search": "line2", "replace": "new2"}]
- RETRY POLICY: If edit_diagram fails because the search pattern cannot be found:
* You may retry edit_diagram up to 3 times with adjusted search patterns
* After 3 failed attempts, you MUST fall back to using display_diagram to regenerate the entire diagram
* The error message will indicate how many retries remain
## Draw.io XML Structure Reference
Basic structure:
\`\`\`xml
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- All other cells go here as siblings -->
</root>
</mxGraphModel>
\`\`\`
CRITICAL RULES:
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/>
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell
3. Use unique sequential IDs for all cells (start from "2" for user content)
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
Shape (vertex) example:
\`\`\`xml
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
</mxCell>
\`\`\`
Connector (edge) example:
\`\`\`xml
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
\`\`\`
Common styles:
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
`;
const lastMessage = messages[messages.length - 1];
// Extract text from the last message parts
const lastMessageText = lastMessage.parts?.find((part: any) => part.type === 'text')?.text || '';
// Extract file parts (images) from the last message
const fileParts = lastMessage.parts?.filter((part: any) => part.type === 'file') || [];
const formattedTextContent = `
Current diagram XML:
"""xml
${xml || ''}
"""
User input:
"""md
${lastMessageText}
"""`;
// Convert UIMessages to ModelMessages and add system message
const modelMessages = convertToModelMessages(messages);
// Convert UIMessages to ModelMessages and add system message
const modelMessages = convertToModelMessages(messages);
// Filter out messages with empty content arrays (Bedrock API rejects these)
// This is a safety measure - ideally convertToModelMessages should handle all cases
let enhancedMessages = modelMessages.filter((msg: any) =>
msg.content && Array.isArray(msg.content) && msg.content.length > 0
);
// Update the last message with user input only (XML moved to separate cached system message)
if (enhancedMessages.length >= 1) {
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1];
if (lastModelMessage.role === 'user') {
// Build content array with user input text and file parts
const contentParts: any[] = [
{ type: 'text', text: formattedUserInput }
];
// Add image parts back
for (const filePart of fileParts) {
contentParts.push({
type: 'image',
image: filePart.url,
mimeType: filePart.mediaType
});
}
enhancedMessages = [
...enhancedMessages.slice(0, -1),
{ ...lastModelMessage, content: contentParts }
];
// Log messages with empty content for debugging (helps identify root cause)
const emptyMessages = modelMessages.filter((msg: any) =>
!msg.content || !Array.isArray(msg.content) || msg.content.length === 0
);
if (emptyMessages.length > 0) {
console.warn('[Chat API] Messages with empty content detected:',
JSON.stringify(emptyMessages.map((m: any) => ({ role: m.role, contentLength: m.content?.length })))
);
console.warn('[Chat API] Original UI messages structure:',
JSON.stringify(messages.map((m: any) => ({
id: m.id,
role: m.role,
partsCount: m.parts?.length,
partTypes: m.parts?.map((p: any) => p.type)
})))
);
}
}
// Add cache point to the last assistant message in conversation history
// This caches the entire conversation prefix for subsequent requests
// Strategy: system (cached) + history with last assistant (cached) + new user message
if (enhancedMessages.length >= 2) {
// Find the last assistant message (should be second-to-last, before current user message)
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
if (enhancedMessages[i].role === 'assistant') {
enhancedMessages[i] = {
...enhancedMessages[i],
providerOptions: {
bedrock: { cachePoint: { type: 'default' } },
},
};
break; // Only cache the last assistant message
// Filter out messages with empty content arrays (Bedrock API rejects these)
// This is a safety measure - ideally convertToModelMessages should handle all cases
let enhancedMessages = modelMessages.filter((msg: any) =>
msg.content && Array.isArray(msg.content) && msg.content.length > 0
);
// Update the last message with formatted content if it's a user message
if (enhancedMessages.length >= 1) {
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1];
if (lastModelMessage.role === 'user') {
// Build content array with text and file parts
const contentParts: any[] = [
{ type: 'text', text: formattedTextContent }
];
// Add image parts back
for (const filePart of fileParts) {
contentParts.push({
type: 'image',
image: filePart.url,
mimeType: filePart.mediaType
});
}
enhancedMessages = [
...enhancedMessages.slice(0, -1),
{ ...lastModelMessage, content: contentParts }
];
}
}
}
// System messages with multiple cache breakpoints for optimal caching:
// - Breakpoint 1: Static instructions (~1500 tokens) - rarely changes
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
// This allows: if only user message changes, both system caches are reused
// if XML changes, instruction cache is still reused
const systemMessages = [
// Cache breakpoint 1: Instructions (rarely change)
{
// Add cache point to the last assistant message in conversation history
// This caches the entire conversation prefix for subsequent requests
// Strategy: system (cached) + history with last assistant (cached) + new user message
if (enhancedMessages.length >= 2) {
// Find the last assistant message (should be second-to-last, before current user message)
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
if (enhancedMessages[i].role === 'assistant') {
enhancedMessages[i] = {
...enhancedMessages[i],
providerOptions: {
bedrock: { cachePoint: { type: 'default' } },
},
};
break; // Only cache the last assistant message
}
}
}
// Get AI model from environment configuration
const { model, providerOptions, headers } = getAIModel();
// System message with cache point for Bedrock (requires 1024+ tokens)
const systemMessageWithCache = {
role: 'system' as const,
content: systemMessage,
providerOptions: {
bedrock: { cachePoint: { type: 'default' } },
},
},
// Cache breakpoint 2: Current diagram XML context
{
role: 'system' as const,
content: `Current diagram XML:\n"""xml\n${xml || ''}\n"""\nWhen using edit_diagram, COPY search patterns exactly from this XML - attribute order matters!`,
providerOptions: {
bedrock: { cachePoint: { type: 'default' } },
};
const result = streamText({
model,
messages: [systemMessageWithCache, ...enhancedMessages],
...(providerOptions && { providerOptions }),
...(headers && { headers }),
// Only enable telemetry if Langfuse is configured
...(process.env.LANGFUSE_PUBLIC_KEY && {
experimental_telemetry: {
isEnabled: true,
metadata: {
sessionId: validSessionId,
userId: userId,
},
},
}),
onFinish: ({ usage, providerMetadata }) => {
console.log('[Cache] Usage:', JSON.stringify({
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
cachedInputTokens: usage?.cachedInputTokens,
}, null, 2));
console.log('[Cache] Provider metadata:', JSON.stringify(providerMetadata, null, 2));
},
},
];
const allMessages = [...systemMessages, ...enhancedMessages];
const result = streamText({
model,
messages: allMessages,
...(providerOptions && { providerOptions }),
...(headers && { headers }),
onFinish: ({ usage, providerMetadata, finishReason, text, toolCalls }) => {
// Detect potential mid-stream failures (e.g., Bedrock 503 ServiceUnavailableException)
// When this happens, usage is empty and providerMetadata is undefined
const hasUsage = usage && Object.keys(usage).length > 0;
if (!hasUsage) {
console.error('[Stream Error] Empty usage detected - possible Bedrock 503 or mid-stream failure');
console.error('[Stream Error] finishReason:', finishReason);
console.error('[Stream Error] text received:', text?.substring(0, 200) || '(none)');
console.error('[Stream Error] toolCalls:', toolCalls?.length || 0);
// Log the user's last message for debugging
const lastUserMsg = enhancedMessages.filter(m => m.role === 'user').pop();
if (lastUserMsg) {
const content = lastUserMsg.content;
const preview = Array.isArray(content)
? (content.find((c) => c.type === 'text') as { type: 'text'; text: string } | undefined)?.text?.substring(0, 100)
: String(content).substring(0, 100);
console.error('[Stream Error] Last user message preview:', preview);
}
} else {
console.log('[Cache] Full providerMetadata:', JSON.stringify(providerMetadata, null, 2));
console.log('[Cache] Usage:', JSON.stringify(usage, null, 2));
}
},
tools: {
// Client-side tool that will be executed on the client
display_diagram: {
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
tools: {
// Client-side tool that will be executed on the client
display_diagram: {
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
VALIDATION RULES (XML will be rejected if violated):
1. All mxCell elements must be DIRECT children of <root> - never nested
@@ -215,79 +322,58 @@ Notes:
- For AWS diagrams, use **AWS 2025 icons**.
- For animated connectors, add "flowAnimation=1" to edge style.
`,
inputSchema: z.object({
xml: z.string().describe("XML string to be displayed on draw.io")
})
},
edit_diagram: {
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
CRITICAL: Copy-paste the EXACT search pattern from the "Current diagram XML" in system context. Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly.
inputSchema: z.object({
xml: z.string().describe("XML string to be displayed on draw.io")
})
},
edit_diagram: {
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
IMPORTANT: Keep edits concise:
- COPY the exact mxCell line from the current XML (attribute order matters!)
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
- Break large changes into multiple smaller edits
- Each search must contain complete lines (never truncate mid-line)
- First match only - be specific enough to target the right element`,
inputSchema: z.object({
edits: z.array(z.object({
search: z.string().describe("EXACT lines copied from current XML (preserve attribute order!)"),
replace: z.string().describe("Replacement lines")
})).describe("Array of search/replace pairs to apply sequentially")
})
inputSchema: z.object({
edits: z.array(z.object({
search: z.string().describe("Exact lines to search for (including whitespace and indentation)"),
replace: z.string().describe("Replacement lines")
})).describe("Array of search/replace pairs to apply sequentially")
})
},
},
},
temperature: 0,
});
temperature: 0,
});
// Error handler function to provide detailed error messages
function errorHandler(error: unknown) {
if (error == null) {
return 'unknown error';
// Error handler function to provide detailed error messages
function errorHandler(error: unknown) {
if (error == null) {
return 'unknown error';
}
const errorString = typeof error === 'string'
? error
: error instanceof Error
? error.message
: JSON.stringify(error);
// Check for image not supported error (e.g., DeepSeek models)
if (errorString.includes('image_url') ||
errorString.includes('unknown variant') ||
(errorString.includes('image') && errorString.includes('not supported'))) {
return 'This model does not support image inputs. Please remove the image and try again, or switch to a vision-capable model.';
}
return errorString;
}
const errorString = typeof error === 'string'
? error
: error instanceof Error
? error.message
: JSON.stringify(error);
// Check for Bedrock service errors (503, throttling, etc.)
if (errorString.includes('ServiceUnavailable') ||
errorString.includes('503') ||
errorString.includes('temporarily unavailable')) {
console.error('[Bedrock Error] ServiceUnavailableException:', errorString);
return 'The AI service is temporarily unavailable. Please try again in a few seconds.';
}
// Check for throttling errors
if (errorString.includes('ThrottlingException') ||
errorString.includes('rate limit') ||
errorString.includes('too many requests') ||
errorString.includes('429')) {
console.error('[Bedrock Error] ThrottlingException:', errorString);
return 'Too many requests. Please wait a moment and try again.';
}
// Check for image not supported error (e.g., DeepSeek models)
if (errorString.includes('image_url') ||
errorString.includes('unknown variant') ||
(errorString.includes('image') && errorString.includes('not supported'))) {
return 'This model does not support image inputs. Please remove the image and try again, or switch to a vision-capable model.';
}
return errorString;
}
return result.toUIMessageStreamResponse({
onError: errorHandler,
});
}
export async function POST(req: Request) {
try {
return await handleChatRequest(req);
return result.toUIMessageStreamResponse({
onError: errorHandler,
});
} catch (error) {
console.error('Error in chat route:', error);
return Response.json({ error: 'Internal server error' }, { status: 500 });
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -32,18 +32,6 @@ export default function Home() {
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Show confirmation dialog when user tries to leave the page
// This helps prevent accidental navigation from browser back gestures
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
return '';
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, []);
return (
<div className="flex h-screen bg-background relative overflow-hidden">
{/* Mobile warning overlay */}

View File

@@ -27,7 +27,7 @@ export function ButtonWithTooltip({
<TooltipTrigger asChild>
<Button {...buttonProps}>{children}</Button>
</TooltipTrigger>
<TooltipContent className="max-w-xs text-wrap">{tooltipContent}</TooltipContent>
<TooltipContent>{tooltipContent}</TooltipContent>
</Tooltip>
</TooltipProvider>
);

View File

@@ -90,10 +90,7 @@ export default function ExamplePanel({
icon={<Zap className="w-4 h-4 text-primary" />}
title="Animated Diagram"
description="Draw a transformer architecture with animated connectors"
onClick={() => {
setInput("Give me a **animated connector** diagram of transformer's architecture")
setFiles([])
}}
onClick={() => setInput("Give me a **animated connector** diagram of transformer's architecture")}
/>
<ExampleCard
@@ -114,10 +111,7 @@ export default function ExamplePanel({
icon={<Palette className="w-4 h-4 text-primary" />}
title="Creative Drawing"
description="Draw something fun and creative"
onClick={() => {
setInput("Draw a cat for me")
setFiles([])
}}
onClick={() => setInput("Draw a cat for me")}
/>
</div>

View File

@@ -29,7 +29,6 @@ interface ChatInputProps {
onFileChange?: (files: File[]) => void;
showHistory?: boolean;
onToggleHistory?: (show: boolean) => void;
error?: Error | null;
}
export function ChatInput({
@@ -42,7 +41,6 @@ export function ChatInput({
onFileChange = () => {},
showHistory = false,
onToggleHistory = () => {},
error = null,
}: ChatInputProps) {
const { diagramHistory, saveDiagramToFile } = useDiagram();
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -51,8 +49,7 @@ export function ChatInput({
const [showClearDialog, setShowClearDialog] = useState(false);
const [showSaveDialog, setShowSaveDialog] = useState(false);
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
const isDisabled = (status === "streaming" || status === "submitted") && !error;
const isDisabled = status === "streaming" || status === "submitted";
useEffect(() => {
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled);
@@ -252,7 +249,7 @@ export function ChatInput({
<SaveDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={(filename, format) => saveDiagramToFile(filename, format)}
onSave={saveDiagramToFile}
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
/>

View File

@@ -6,7 +6,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import ExamplePanel from "./chat-example-panel";
import { UIMessage } from "ai";
import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils";
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, RotateCcw, Pencil } from "lucide-react";
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus } from "lucide-react";
import { CodeBlock } from "./code-block";
interface EditPair {
@@ -67,8 +67,6 @@ interface ChatMessageDisplayProps {
error?: Error | null;
setInput: (input: string) => void;
setFiles: (files: File[]) => void;
onRegenerate?: (messageIndex: number) => void;
onEditMessage?: (messageIndex: number, newText: string) => void;
}
export function ChatMessageDisplay({
@@ -76,8 +74,6 @@ export function ChatMessageDisplay({
error,
setInput,
setFiles,
onRegenerate,
onEditMessage,
}: ChatMessageDisplayProps) {
const { chartXML, loadDiagram: onDisplayChart } = useDiagram();
const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -88,8 +84,6 @@ export function ChatMessageDisplay({
);
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
const [copyFailedMessageId, setCopyFailedMessageId] = useState<string | null>(null);
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
const [editText, setEditText] = useState<string>("");
const copyMessageToClipboard = async (messageId: string, text: string) => {
try {
@@ -115,7 +109,7 @@ export function ChatMessageDisplay({
previousXML.current = convertedXml;
onDisplayChart(replacedXML);
} else {
console.log("[ChatMessageDisplay] XML validation failed:", validationError);
console.error("[ChatMessageDisplay] XML validation failed:", validationError);
}
}
},
@@ -259,146 +253,65 @@ export function ChatMessageDisplay({
<div className="py-4 space-y-4">
{messages.map((message, messageIndex) => {
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
const isLastAssistantMessage = message.role === "assistant" && (
messageIndex === messages.length - 1 ||
messages.slice(messageIndex + 1).every(m => m.role !== "assistant")
);
const isLastUserMessage = message.role === "user" && (
messageIndex === messages.length - 1 ||
messages.slice(messageIndex + 1).every(m => m.role !== "user")
);
const isEditing = editingMessageId === message.id;
return (
<div
key={message.id}
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"} animate-message-in`}
style={{ animationDelay: `${messageIndex * 50}ms` }}
>
{message.role === "user" && userMessageText && !isEditing && (
<div className="flex items-center gap-1 self-center mr-2">
{/* Edit button - only on last user message */}
{onEditMessage && isLastUserMessage && (
<button
onClick={() => {
setEditingMessageId(message.id);
setEditText(userMessageText);
}}
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
title="Edit message"
>
<Pencil className="h-3.5 w-3.5" />
</button>
{message.role === "user" && userMessageText && (
<button
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors self-center mr-2"
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
>
{copiedMessageId === message.id ? (
<Check className="h-3.5 w-3.5 text-green-500" />
) : copyFailedMessageId === message.id ? (
<X className="h-3.5 w-3.5 text-red-500" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
<button
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
>
{copiedMessageId === message.id ? (
<Check className="h-3.5 w-3.5 text-green-500" />
) : copyFailedMessageId === message.id ? (
<X className="h-3.5 w-3.5 text-red-500" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</button>
</div>
</button>
)}
<div className="max-w-[85%]">
{/* Edit mode for user messages */}
{isEditing && message.role === "user" ? (
<div className="flex flex-col gap-2">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
className="w-full min-w-[300px] px-4 py-3 text-sm rounded-2xl border border-primary bg-background text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-primary"
rows={Math.min(editText.split('\n').length + 1, 6)}
autoFocus
onKeyDown={(e) => {
if (e.key === "Escape") {
setEditingMessageId(null);
setEditText("");
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
if (editText.trim() && onEditMessage) {
onEditMessage(messageIndex, editText.trim());
setEditingMessageId(null);
setEditText("");
}
}
}}
/>
<div className="flex justify-end gap-2">
<button
onClick={() => {
setEditingMessageId(null);
setEditText("");
}}
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/80 transition-colors"
>
Cancel
</button>
<button
onClick={() => {
if (editText.trim() && onEditMessage) {
onEditMessage(messageIndex, editText.trim());
setEditingMessageId(null);
setEditText("");
}
}}
disabled={!editText.trim()}
className="px-3 py-1.5 text-xs rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
>
Save & Submit
</button>
</div>
{/* Text content in bubble */}
{message.parts?.some((part: any) => part.type === "text" || part.type === "file") && (
<div
className={`px-4 py-3 text-sm leading-relaxed ${
message.role === "user"
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
}`}
>
{message.parts?.map((part: any, index: number) => {
switch (part.type) {
case "text":
return (
<div key={index} className="whitespace-pre-wrap break-words">
{part.text}
</div>
);
case "file":
return (
<div key={index} className="mt-2">
<Image
src={part.url}
width={200}
height={200}
alt={`Uploaded diagram or image for AI analysis`}
className="rounded-lg border border-white/20"
style={{
objectFit: "contain",
}}
/>
</div>
);
default:
return null;
}
})}
</div>
) : (
/* Text content in bubble */
message.parts?.some((part: any) => part.type === "text" || part.type === "file") && (
<div
className={`px-4 py-3 text-sm leading-relaxed ${
message.role === "user"
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
onClick={() => {
if (message.role === "user" && isLastUserMessage && onEditMessage) {
setEditingMessageId(message.id);
setEditText(userMessageText);
}
}}
title={message.role === "user" && isLastUserMessage && onEditMessage ? "Click to edit" : undefined}
>
{message.parts?.map((part: any, index: number) => {
switch (part.type) {
case "text":
return (
<div key={index} className="whitespace-pre-wrap break-words">
{part.text}
</div>
);
case "file":
return (
<div key={index} className="mt-2">
<Image
src={part.url}
width={200}
height={200}
alt={`Uploaded diagram or image for AI analysis`}
className="rounded-lg border border-white/20"
style={{
objectFit: "contain",
}}
/>
</div>
);
default:
return null;
}
})}
</div>
)
)}
{/* Tool calls outside bubble */}
{message.parts?.map((part: any) => {
@@ -407,37 +320,6 @@ export function ChatMessageDisplay({
}
return null;
})}
{/* Action buttons for assistant messages */}
{message.role === "assistant" && (
<div className="flex items-center gap-1 mt-2">
{/* Copy button */}
<button
onClick={() => copyMessageToClipboard(message.id, getMessageTextContent(message))}
className={`p-1.5 rounded-lg transition-colors ${
copiedMessageId === message.id
? "text-green-600 bg-green-100"
: "text-muted-foreground/60 hover:text-foreground hover:bg-muted"
}`}
title={copiedMessageId === message.id ? "Copied!" : "Copy response"}
>
{copiedMessageId === message.id ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</button>
{/* Regenerate button - only on last assistant message */}
{onRegenerate && isLastAssistantMessage && (
<button
onClick={() => onRegenerate(messageIndex)}
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
title="Regenerate response"
>
<RotateCcw className="h-3.5 w-3.5" />
</button>
)}
</div>
)}
</div>
</div>
);

View File

@@ -2,9 +2,8 @@
import type React from "react";
import { useRef, useEffect, useState } from "react";
import { flushSync } from "react-dom";
import { FaGithub } from "react-icons/fa";
import { PanelRightClose, PanelRightOpen, CheckCircle } from "lucide-react";
import { PanelRightClose, PanelRightOpen } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
@@ -61,173 +60,81 @@ export default function ChatPanel({
const [files, setFiles] = useState<File[]>([]);
const [showHistory, setShowHistory] = useState(false);
const [input, setInput] = useState("");
const [streamingError, setStreamingError] = useState<Error | null>(null);
// Store XML snapshots for each user message (keyed by message index)
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map());
// Generate a unique session ID for Langfuse tracing
const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
const chartXMLRef = useRef(chartXML);
useEffect(() => {
chartXMLRef.current = chartXML;
}, [chartXML]);
const { messages, sendMessage, addToolResult, status, error, setMessages } =
useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
}),
async onToolCall({ toolCall }) {
if (toolCall.toolName === "display_diagram") {
const { xml } = toolCall.input as { xml: string };
const {
messages,
sendMessage,
addToolResult,
status,
error,
setMessages,
stop,
} = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
}),
async onToolCall({ toolCall }) {
if (toolCall.toolName === "display_diagram") {
const { xml } = toolCall.input as { xml: string };
const validationError = validateMxCellStructure(xml);
const validationError = validateMxCellStructure(xml);
if (validationError) {
addToolResult({
tool: "display_diagram",
toolCallId: toolCall.toolCallId,
output: validationError,
});
} else {
addToolResult({
tool: "display_diagram",
toolCallId: toolCall.toolCallId,
output: "Successfully displayed the diagram.",
});
}
} else if (toolCall.toolName === "edit_diagram") {
const { edits } = toolCall.input as {
edits: Array<{ search: string; replace: string }>;
};
let currentXml = "";
try {
console.log("[edit_diagram] Starting...");
// Use chartXML from ref directly - more reliable than export
// especially on Vercel where DrawIO iframe may have latency issues
// Using ref to avoid stale closure in callback
const cachedXML = chartXMLRef.current;
if (cachedXML) {
currentXml = cachedXML;
console.log(
"[edit_diagram] Using cached chartXML, length:",
currentXml.length
);
if (validationError) {
addToolResult({
tool: "display_diagram",
toolCallId: toolCall.toolCallId,
output: validationError,
});
} else {
// Fallback to export only if no cached XML
console.log(
"[edit_diagram] No cached XML, fetching from DrawIO..."
);
currentXml = await onFetchChart(false);
console.log(
"[edit_diagram] Got XML from export, length:",
currentXml.length
);
addToolResult({
tool: "display_diagram",
toolCallId: toolCall.toolCallId,
output: "Successfully displayed the diagram.",
});
}
} else if (toolCall.toolName === "edit_diagram") {
const { edits } = toolCall.input as {
edits: Array<{ search: string; replace: string }>;
};
const { replaceXMLParts } = await import("@/lib/utils");
const editedXml = replaceXMLParts(currentXml, edits);
let currentXml = "";
try {
// Fetch without saving to history - edit_diagram shouldn't create history entry
currentXml = await onFetchChart(false);
onDisplayChart(editedXml);
const { replaceXMLParts } = await import("@/lib/utils");
const editedXml = replaceXMLParts(currentXml, edits);
addToolResult({
tool: "edit_diagram",
toolCallId: toolCall.toolCallId,
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
});
console.log("[edit_diagram] Success");
} catch (error) {
console.error("[edit_diagram] Failed:", error);
onDisplayChart(editedXml);
const errorMessage =
error instanceof Error ? error.message : String(error);
addToolResult({
tool: "edit_diagram",
toolCallId: toolCall.toolCallId,
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
});
} catch (error) {
console.error("Edit diagram failed:", error);
addToolResult({
tool: "edit_diagram",
toolCallId: toolCall.toolCallId,
output: `Edit failed: ${errorMessage}
const errorMessage =
error instanceof Error
? error.message
: String(error);
addToolResult({
tool: "edit_diagram",
toolCallId: toolCall.toolCallId,
output: `Edit failed: ${errorMessage}
Current diagram XML:
\`\`\`xml
${currentXml || "No XML available"}
${currentXml}
\`\`\`
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
});
});
}
}
}
},
onError: (error) => {
console.error("Chat error:", error);
setStreamingError(error);
},
});
// Streaming timeout detection - detects when stream stalls mid-response (e.g., Bedrock 503)
// This catches cases where onError doesn't fire because headers were already sent
const lastMessageCountRef = useRef(0);
const lastMessagePartsRef = useRef(0);
useEffect(() => {
// Clear streaming error when status changes to ready
if (status === "ready") {
setStreamingError(null);
lastMessageCountRef.current = 0;
lastMessagePartsRef.current = 0;
return;
}
if (status !== "streaming") return;
const STALL_TIMEOUT_MS = 15000; // 15 seconds without any update
// Capture current state BEFORE setting timeout
// This way we compare against values at the time timeout was set
const currentPartsCount = messages.reduce(
(acc, msg) => acc + (msg.parts?.length || 0),
0
);
const capturedMessageCount = messages.length;
const capturedPartsCount = currentPartsCount;
// Update refs immediately so next effect run has fresh values
lastMessageCountRef.current = messages.length;
lastMessagePartsRef.current = currentPartsCount;
const timeoutId = setTimeout(() => {
// Re-count parts at timeout time
const newPartsCount = messages.reduce(
(acc, msg) => acc + (msg.parts?.length || 0),
0
);
// If no change since timeout was set, stream has stalled
if (
messages.length === capturedMessageCount &&
newPartsCount === capturedPartsCount
) {
console.error(
"[Streaming Timeout] No activity for 15s - forcing error state"
);
setStreamingError(
new Error(
"Connection lost. The AI service may be temporarily unavailable. Please try again."
)
);
stop(); // Allow user to retry by transitioning status to "ready"
}
}, STALL_TIMEOUT_MS);
return () => clearTimeout(timeoutId);
}, [status, messages, stop]);
},
onError: (error) => {
console.error("Chat error:", error);
},
});
const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -237,23 +144,18 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
}
}, [messages]);
useEffect(() => {
console.log("[ChatPanel] Status changed to:", status);
}, [status]);
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Allow retry if there's a streaming error (workaround for stop() not transitioning status)
const isProcessing =
(status === "streaming" || status === "submitted") &&
!streamingError;
const isProcessing = status === "streaming" || status === "submitted";
if (input.trim() && !isProcessing) {
// Clear any previous streaming error before starting new request
setStreamingError(null);
try {
let chartXml = await onFetchChart();
chartXml = formatXML(chartXml);
// Update ref directly to avoid race condition with React's async state update
// This ensures edit_diagram has the correct XML before AI responds
chartXMLRef.current = chartXml;
const parts: any[] = [{ type: "text", text: input }];
if (files.length > 0) {
@@ -273,15 +175,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
}
}
// Save XML snapshot for this message (will be at index = current messages.length)
const messageIndex = messages.length;
xmlSnapshotsRef.current.set(messageIndex, chartXml);
sendMessage(
{ parts },
{
body: {
xml: chartXml,
sessionId,
},
}
);
@@ -304,125 +203,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
setFiles(newFiles);
};
const handleRegenerate = async (messageIndex: number) => {
const isProcessing = status === "streaming" || status === "submitted";
if (isProcessing) return;
// Find the user message before this assistant message
let userMessageIndex = messageIndex - 1;
while (
userMessageIndex >= 0 &&
messages[userMessageIndex].role !== "user"
) {
userMessageIndex--;
}
if (userMessageIndex < 0) return;
const userMessage = messages[userMessageIndex];
const userParts = userMessage.parts;
// Get the text from the user message
const textPart = userParts?.find((p: any) => p.type === "text");
if (!textPart) return;
// Get the saved XML snapshot for this user message
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex);
if (!savedXml) {
console.error(
"No saved XML snapshot for message index:",
userMessageIndex
);
return;
}
// Restore the diagram to the saved state
onDisplayChart(savedXml);
// Update ref directly to ensure edit_diagram has the correct XML
chartXMLRef.current = savedXml;
// Clean up snapshots for messages after the user message (they will be removed)
for (const key of xmlSnapshotsRef.current.keys()) {
if (key > userMessageIndex) {
xmlSnapshotsRef.current.delete(key);
}
}
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
// Use flushSync to ensure state update is processed synchronously before sending
const newMessages = messages.slice(0, userMessageIndex);
flushSync(() => {
setMessages(newMessages);
});
// Now send the message after state is guaranteed to be updated
sendMessage(
{ parts: userParts },
{
body: {
xml: savedXml,
},
}
);
};
const handleEditMessage = async (messageIndex: number, newText: string) => {
const isProcessing = status === "streaming" || status === "submitted";
if (isProcessing) return;
const message = messages[messageIndex];
if (!message || message.role !== "user") return;
// Get the saved XML snapshot for this user message
const savedXml = xmlSnapshotsRef.current.get(messageIndex);
if (!savedXml) {
console.error(
"No saved XML snapshot for message index:",
messageIndex
);
return;
}
// Restore the diagram to the saved state
onDisplayChart(savedXml);
// Update ref directly to ensure edit_diagram has the correct XML
chartXMLRef.current = savedXml;
// Clean up snapshots for messages after the user message (they will be removed)
for (const key of xmlSnapshotsRef.current.keys()) {
if (key > messageIndex) {
xmlSnapshotsRef.current.delete(key);
}
}
// Create new parts with updated text
const newParts = message.parts?.map((part: any) => {
if (part.type === "text") {
return { ...part, text: newText };
}
return part;
}) || [{ type: "text", text: newText }];
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
// Use flushSync to ensure state update is processed synchronously before sending
const newMessages = messages.slice(0, messageIndex);
flushSync(() => {
setMessages(newMessages);
});
// Now send the edited message after state is guaranteed to be updated
sendMessage(
{ parts: newParts },
{
body: {
xml: savedXml,
},
}
);
};
// Collapsed view
if (!isVisible) {
return (
@@ -474,14 +254,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
>
About
</Link>
<ButtonWithTooltip
tooltipContent="Recent generation failures were caused by our AI provider's infrastructure issue, not the app code. After extensive debugging, I've switched providers and observed 30+ minutes of stability. If issues persist, please report on GitHub."
variant="ghost"
size="icon"
className="h-6 w-6 text-green-500 hover:text-green-600"
>
<CheckCircle className="h-4 w-4" />
</ButtonWithTooltip>
</div>
<div className="flex items-center gap-1">
<a
@@ -509,11 +281,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
<main className="flex-1 overflow-hidden">
<ChatMessageDisplay
messages={messages}
error={error || streamingError}
error={error}
setInput={setInput}
setFiles={handleFileChange}
onRegenerate={handleRegenerate}
onEditMessage={handleEditMessage}
/>
</main>
@@ -527,13 +297,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
onClearChat={() => {
setMessages([]);
clearDiagram();
xmlSnapshotsRef.current.clear();
setSessionId(`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
}}
files={files}
onFileChange={handleFileChange}
showHistory={showHistory}
onToggleHistory={setShowHistory}
error={error || streamingError}
/>
</footer>
</div>

View File

@@ -10,26 +10,11 @@ import {
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export type ExportFormat = "drawio" | "png" | "svg";
const FORMAT_OPTIONS: { value: ExportFormat; label: string; extension: string }[] = [
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
{ value: "png", label: "PNG Image", extension: ".png" },
{ value: "svg", label: "SVG Image", extension: ".svg" },
];
interface SaveDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave: (filename: string, format: ExportFormat) => void;
onSave: (filename: string) => void;
defaultFilename: string;
}
@@ -40,7 +25,6 @@ export function SaveDialog({
defaultFilename,
}: SaveDialogProps) {
const [filename, setFilename] = useState(defaultFilename);
const [format, setFormat] = useState<ExportFormat>("drawio");
useEffect(() => {
if (open) {
@@ -50,7 +34,7 @@ export function SaveDialog({
const handleSave = () => {
const finalFilename = filename.trim() || defaultFilename;
onSave(finalFilename, format);
onSave(finalFilename);
onOpenChange(false);
};
@@ -61,46 +45,27 @@ export function SaveDialog({
}
};
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Save Diagram</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Format</label>
<Select value={format} onValueChange={(v) => setFormat(v as ExportFormat)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FORMAT_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Filename</label>
<div className="flex items-stretch">
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter filename"
autoFocus
onFocus={(e) => e.target.select()}
className="rounded-r-none border-r-0 focus-visible:z-10"
/>
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
{currentFormat?.extension || ".drawio"}
</span>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Filename</label>
<div className="flex items-stretch">
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter filename"
autoFocus
onFocus={(e) => e.target.select()}
className="rounded-r-none border-r-0 focus-visible:z-10"
/>
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
.drawio
</span>
</div>
</div>
<DialogFooter>

View File

@@ -1,187 +0,0 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -3,7 +3,6 @@
import React, { createContext, useContext, useRef, useState } from "react";
import type { DrawIoEmbedRef } from "react-drawio";
import { extractDiagramXML } from "../lib/utils";
import type { ExportFormat } from "@/components/save-dialog";
interface DiagramContextType {
chartXML: string;
@@ -16,7 +15,7 @@ interface DiagramContextType {
drawioRef: React.Ref<DrawIoEmbedRef | null>;
handleDiagramExport: (data: any) => void;
clearDiagram: () => void;
saveDiagramToFile: (filename: string, format: ExportFormat) => void;
saveDiagramToFile: (filename: string) => void;
}
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
@@ -31,11 +30,8 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
const resolverRef = useRef<((value: string) => void) | null>(null);
// Track if we're expecting an export for history (user-initiated)
const expectHistoryExportRef = useRef<boolean>(false);
// Track if we're expecting an export for file save (stores raw export data)
const saveResolverRef = useRef<{
resolver: ((data: string) => void) | null;
format: ExportFormat | null;
}>({ resolver: null, format: null });
// Track if we're expecting an export for file save
const saveResolverRef = useRef<((xml: string) => void) | null>(null);
const handleExport = () => {
if (drawioRef.current) {
@@ -65,18 +61,6 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
};
const handleDiagramExport = (data: any) => {
// Handle save to file if requested (process raw data before extraction)
if (saveResolverRef.current.resolver) {
const format = saveResolverRef.current.format;
saveResolverRef.current.resolver(data.data);
saveResolverRef.current = { resolver: null, format: null };
// For non-xmlsvg formats, skip XML extraction as it will fail
// Only drawio (which uses xmlsvg internally) has the content attribute
if (format === "png" || format === "svg") {
return;
}
}
const extractedXML = extractDiagramXML(data.data);
setChartXML(extractedXML);
setLatestSvg(data.data);
@@ -97,6 +81,12 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
resolverRef.current(extractedXML);
resolverRef.current = null;
}
// Handle save to file if requested
if (saveResolverRef.current) {
saveResolverRef.current(extractedXML);
saveResolverRef.current = null;
}
};
const clearDiagram = () => {
@@ -107,71 +97,33 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
setDiagramHistory([]);
};
const saveDiagramToFile = (filename: string, format: ExportFormat) => {
const saveDiagramToFile = (filename: string) => {
if (!drawioRef.current) {
console.warn("Draw.io editor not ready");
return;
}
// Map format to draw.io export format
const drawioFormat = format === "drawio" ? "xmlsvg" : format;
// Export diagram and save when export completes
drawioRef.current.exportDiagram({ format: "xmlsvg" });
saveResolverRef.current = (xml: string) => {
// Wrap in proper .drawio format
let fileContent = xml;
if (!xml.includes("<mxfile")) {
fileContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
}
// Set up the resolver before triggering export
saveResolverRef.current = {
resolver: (exportData: string) => {
let fileContent: string | Blob;
let mimeType: string;
let extension: string;
if (format === "drawio") {
// Extract XML from SVG for .drawio format
const xml = extractDiagramXML(exportData);
let xmlContent = xml;
if (!xml.includes("<mxfile")) {
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
}
fileContent = xmlContent;
mimeType = "application/xml";
extension = ".drawio";
} else if (format === "png") {
// PNG data comes as base64 data URL
fileContent = exportData;
mimeType = "image/png";
extension = ".png";
} else {
// SVG format
fileContent = exportData;
mimeType = "image/svg+xml";
extension = ".svg";
}
// Handle download
let url: string;
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
// Already a data URL (PNG)
url = fileContent;
} else {
const blob = new Blob([fileContent], { type: mimeType });
url = URL.createObjectURL(blob);
}
const a = document.createElement("a");
a.href = url;
a.download = `${filename}${extension}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Delay URL revocation to ensure download completes
if (!url.startsWith("data:")) {
setTimeout(() => URL.revokeObjectURL(url), 100);
}
},
format,
const blob = new Blob([fileContent], { type: "application/xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
// Add .drawio extension if not present
a.download = filename.endsWith(".drawio") ? filename : `${filename}.drawio`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Delay URL revocation to ensure download completes
setTimeout(() => URL.revokeObjectURL(url), 100);
};
// Export diagram - callback will be handled in handleDiagramExport
drawioRef.current.exportDiagram({ format: drawioFormat });
};
return (

View File

@@ -41,3 +41,9 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# DeepSeek Configuration
# DEEPSEEK_API_KEY=sk-...
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
# Langfuse Observability (Optional)
# Enable LLM tracing and analytics - https://langfuse.com
# LANGFUSE_PUBLIC_KEY=pk-lf-...
# LANGFUSE_SECRET_KEY=sk-lf-...
# LANGFUSE_BASEURL=https://cloud.langfuse.com # EU region, use https://us.cloud.langfuse.com for US

22
instrumentation.ts Normal file
View File

@@ -0,0 +1,22 @@
import { LangfuseSpanProcessor } from '@langfuse/otel';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
export function register() {
// Skip telemetry if Langfuse env vars are not configured
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
console.warn('[Langfuse] Environment variables not configured - telemetry disabled');
return;
}
const langfuseSpanProcessor = new LangfuseSpanProcessor({
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
secretKey: process.env.LANGFUSE_SECRET_KEY,
baseUrl: process.env.LANGFUSE_BASEURL,
});
const tracerProvider = new NodeTracerProvider({
spanProcessors: [langfuseSpanProcessor],
});
tracerProvider.register();
}

View File

@@ -1,5 +1,4 @@
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { bedrock } from '@ai-sdk/amazon-bedrock';
import { openai, createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
@@ -22,7 +21,6 @@ interface ModelConfig {
model: any;
providerOptions?: any;
headers?: Record<string, string>;
modelId: string;
}
// Bedrock provider options for Anthropic beta features
@@ -37,47 +35,22 @@ const ANTHROPIC_BETA_HEADERS = {
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
};
// Map of provider to required environment variable
const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
bedrock: null, // AWS SDK auto-uses IAM role on AWS, or env vars locally
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
google: 'GOOGLE_GENERATIVE_AI_API_KEY',
azure: 'AZURE_API_KEY',
ollama: null, // No credentials needed for local Ollama
openrouter: 'OPENROUTER_API_KEY',
deepseek: 'DEEPSEEK_API_KEY',
};
/**
* Auto-detect provider based on available API keys
* Returns the provider if exactly one is configured, otherwise null
*/
function detectProvider(): ProviderName | null {
const configuredProviders: ProviderName[] = [];
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
if (envVar === null) {
// Skip ollama - it doesn't require credentials
continue;
}
if (process.env[envVar]) {
configuredProviders.push(provider as ProviderName);
}
}
if (configuredProviders.length === 1) {
return configuredProviders[0];
}
return null;
}
/**
* Validate that required API keys are present for the selected provider
*/
function validateProviderCredentials(provider: ProviderName): void {
const requiredVar = PROVIDER_ENV_VARS[provider];
const requiredEnvVars: Record<ProviderName, string | null> = {
bedrock: 'AWS_ACCESS_KEY_ID',
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
google: 'GOOGLE_GENERATIVE_AI_API_KEY',
azure: 'AZURE_API_KEY',
ollama: null, // No credentials needed for local Ollama
openrouter: 'OPENROUTER_API_KEY',
deepseek: 'DEEPSEEK_API_KEY',
};
const requiredVar = requiredEnvVars[provider];
if (requiredVar && !process.env[requiredVar]) {
throw new Error(
`${requiredVar} environment variable is required for ${provider} provider. ` +
@@ -106,6 +79,7 @@ function validateProviderCredentials(provider: ProviderName): void {
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
*/
export function getAIModel(): ModelConfig {
const provider = (process.env.AI_PROVIDER || 'bedrock') as ProviderName;
const modelId = process.env.AI_MODEL;
if (!modelId) {
@@ -114,45 +88,10 @@ export function getAIModel(): ModelConfig {
);
}
// Determine provider: explicit config > auto-detect > error
let provider: ProviderName;
if (process.env.AI_PROVIDER) {
provider = process.env.AI_PROVIDER as ProviderName;
} else {
const detected = detectProvider();
if (detected) {
provider = detected;
console.log(`[AI Provider] Auto-detected provider: ${provider}`);
} else {
// List configured providers for better error message
const configured = Object.entries(PROVIDER_ENV_VARS)
.filter(([, envVar]) => envVar && process.env[envVar as string])
.map(([p]) => p);
if (configured.length === 0) {
throw new Error(
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
`- DEEPSEEK_API_KEY for DeepSeek\n` +
`- OPENAI_API_KEY for OpenAI\n` +
`- ANTHROPIC_API_KEY for Anthropic\n` +
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
`- OPENROUTER_API_KEY for OpenRouter\n` +
`- AZURE_API_KEY for Azure\n` +
`Or set AI_PROVIDER=ollama for local Ollama.`
);
} else {
throw new Error(
`Multiple AI providers configured (${configured.join(', ')}). ` +
`Please set AI_PROVIDER to specify which one to use.`
);
}
}
}
// Validate provider credentials
validateProviderCredentials(provider);
// Log initialization for debugging
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`);
let model: any;
@@ -160,20 +99,13 @@ export function getAIModel(): ModelConfig {
let headers: Record<string, string> | undefined = undefined;
switch (provider) {
case 'bedrock': {
// Use credential provider chain for IAM role support (Amplify, Lambda, etc.)
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
const bedrockProvider = createAmazonBedrock({
region: process.env.AWS_REGION || 'us-west-2',
credentialProvider: fromNodeProviderChain(),
});
model = bedrockProvider(modelId);
case 'bedrock':
model = bedrock(modelId);
// Add Anthropic beta options if using Claude models via Bedrock
if (modelId.includes('anthropic.claude')) {
providerOptions = BEDROCK_ANTHROPIC_BETA;
}
break;
}
case 'openai':
if (process.env.OPENAI_BASE_URL) {
@@ -259,5 +191,10 @@ export function getAIModel(): ModelConfig {
);
}
return { model, providerOptions, headers, modelId };
// Log if provider options or headers are being applied
if (providerOptions || headers) {
console.log('[AI Provider] Applying provider-specific options/headers');
}
return { model, providerOptions, headers };
}

View File

@@ -12,117 +12,117 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- Title -->
<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"/>
</mxCell>
<!-- Input Embedding (Left - Encoder Side) -->
<mxCell id="input_embed" value="Input Embedding" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="80" y="480" width="120" height="40" as="geometry"/>
</mxCell>
<!-- Positional Encoding (Left) -->
<mxCell id="pos_enc_left" value="Positional Encoding" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="80" y="420" width="120" height="40" as="geometry"/>
</mxCell>
<!-- Encoder Stack -->
<mxCell id="encoder_box" value="ENCODER" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;verticalAlign=top;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="60" y="180" width="160" height="220" as="geometry"/>
</mxCell>
<!-- Multi-Head Attention (Encoder) -->
<mxCell id="mha_enc" value="Multi-Head&#xa;Attention" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="80" y="330" width="120" height="50" as="geometry"/>
</mxCell>
<!-- Add & Norm 1 (Encoder) -->
<mxCell id="add_norm1_enc" value="Add &amp; Norm" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="80" y="280" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Feed Forward (Encoder) -->
<mxCell id="ff_enc" value="Feed Forward" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="80" y="240" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Add & Norm 2 (Encoder) -->
<mxCell id="add_norm2_enc" value="Add &amp; Norm" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="80" y="200" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Nx label for encoder -->
<mxCell id="nx_enc" value="Nx" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="30" y="275" width="30" height="30" as="geometry"/>
</mxCell>
<!-- Output Embedding (Right - Decoder Side) -->
<mxCell id="output_embed" value="Output Embedding" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="650" y="480" width="120" height="40" as="geometry"/>
</mxCell>
<!-- Positional Encoding (Right) -->
<mxCell id="pos_enc_right" value="Positional Encoding" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="650" y="420" width="120" height="40" as="geometry"/>
</mxCell>
<!-- Decoder Stack -->
<mxCell id="decoder_box" value="DECODER" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;verticalAlign=top;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="630" y="140" width="160" height="260" as="geometry"/>
</mxCell>
<!-- Masked Multi-Head Attention (Decoder) -->
<mxCell id="masked_mha_dec" value="Masked Multi-Head&#xa;Attention" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="340" width="120" height="50" as="geometry"/>
</mxCell>
<!-- Add & Norm 1 (Decoder) -->
<mxCell id="add_norm1_dec" value="Add &amp; Norm" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="290" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Multi-Head Attention (Decoder - Cross Attention) -->
<mxCell id="mha_dec" value="Multi-Head&#xa;Attention" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="240" width="120" height="40" as="geometry"/>
</mxCell>
<!-- Add & Norm 2 (Decoder) -->
<mxCell id="add_norm2_dec" value="Add &amp; Norm" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="200" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Feed Forward (Decoder) -->
<mxCell id="ff_dec" value="Feed Forward" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="160" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Add & Norm 3 (Decoder) -->
<mxCell id="add_norm3_dec" value="Add &amp; Norm" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="650" y="120" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Nx label for decoder -->
<mxCell id="nx_dec" value="Nx" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;fontStyle=2;" vertex="1" parent="1">
<mxGeometry x="790" y="255" width="30" height="30" as="geometry"/>
</mxCell>
<!-- Linear -->
<mxCell id="linear" value="Linear" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="650" y="80" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Softmax -->
<mxCell id="softmax" value="Softmax" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="650" y="40" width="120" height="30" as="geometry"/>
</mxCell>
<!-- Output Probabilities -->
<mxCell id="output" value="Output Probabilities" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="640" y="0" width="140" height="30" as="geometry"/>
</mxCell>
<!-- Animated Connectors - Encoder Side -->
<mxCell id="conn1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeWidth=2;strokeColor=#6c8ebf;flowAnimation=1;" edge="1" parent="1" source="input_embed" target="pos_enc_left">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
@@ -143,7 +143,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Encoder to Decoder Cross Attention -->
<mxCell id="conn_cross" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;strokeColor=#9673a6;flowAnimation=1;dashed=1;" edge="1" parent="1" source="add_norm2_enc" target="mha_dec">
<mxGeometry relative="1" as="geometry">
<Array as="points">
@@ -158,7 +158,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Animated Connectors - Decoder Side -->
<mxCell id="conn6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeWidth=2;strokeColor=#d79b00;flowAnimation=1;" edge="1" parent="1" source="output_embed" target="pos_enc_right">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
@@ -199,7 +199,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Residual Connections (Encoder) -->
<mxCell id="res1_enc" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=1.5;strokeColor=#999999;dashed=1;flowAnimation=1;" edge="1" parent="1" source="mha_enc" target="add_norm1_enc">
<mxGeometry relative="1" as="geometry">
<Array as="points">
@@ -218,7 +218,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Residual Connections (Decoder) -->
<mxCell id="res1_dec" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=1.5;strokeColor=#999999;dashed=1;flowAnimation=1;" edge="1" parent="1" source="masked_mha_dec" target="add_norm1_dec">
<mxGeometry relative="1" as="geometry">
<Array as="points">
@@ -246,7 +246,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Input/Output Labels -->
<mxCell id="input_label" value="Inputs" 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="110" y="530" width="60" height="20" as="geometry"/>
</mxCell>
@@ -263,37 +263,37 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- AWS Cloud Container -->
<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"/>
</mxCell>
<!-- User -->
<mxCell id="3" value="User" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#232F3D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=14;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.user;rounded=1;" vertex="1" parent="1">
<mxGeometry x="80" y="240" width="78" height="78" as="geometry"/>
</mxCell>
<!-- EC2 Instance -->
<mxCell id="4" value="EC2" style="sketch=0;points=[[0,0,0],[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0,0],[0,1,0],[0.25,1,0],[0.5,1,0],[0.75,1,0],[1,1,0],[0,0.25,0],[0,0.5,0],[0,0.75,0],[1,0.25,0],[1,0.5,0],[1,0.75,0]];outlineConnect=0;fontColor=#232F3E;fillColor=#ED7100;strokeColor=#ffffff;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=14;fontStyle=0;aspect=fixed;shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.ec2;rounded=1;" vertex="1" parent="1">
<mxGeometry x="560" y="240" width="78" height="78" as="geometry"/>
</mxCell>
<!-- S3 Bucket -->
<mxCell id="5" value="S3" style="sketch=0;points=[[0,0,0],[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0,0],[0,1,0],[0.25,1,0],[0.5,1,0],[0.75,1,0],[1,1,0],[0,0.25,0],[0,0.5,0],[0,0.75,0],[1,0.25,0],[1,0.5,0],[1,0.75,0]];outlineConnect=0;fontColor=#232F3E;fillColor=#7AA116;strokeColor=#ffffff;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=14;fontStyle=0;aspect=fixed;shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.s3;rounded=1;" vertex="1" parent="1">
<mxGeometry x="960" y="120" width="78" height="78" as="geometry"/>
</mxCell>
<!-- Bedrock -->
<mxCell id="6" value="bedrock" style="sketch=0;points=[[0,0,0],[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0,0],[0,1,0],[0.25,1,0],[0.5,1,0],[0.75,1,0],[1,1,0],[0,0.25,0],[0,0.5,0],[0,0.75,0],[1,0.25,0],[1,0.5,0],[1,0.75,0]];outlineConnect=0;fontColor=#232F3E;fillColor=#01A88D;strokeColor=#ffffff;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=14;fontStyle=0;aspect=fixed;shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.bedrock;rounded=1;" vertex="1" parent="1">
<mxGeometry x="960" y="260" width="78" height="78" as="geometry"/>
</mxCell>
<!-- DynamoDB -->
<mxCell id="7" value="DynamoDB" style="sketch=0;points=[[0,0,0],[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0,0],[0,1,0],[0.25,1,0],[0.5,1,0],[0.75,1,0],[1,1,0],[0,0.25,0],[0,0.5,0],[0,0.75,0],[1,0.25,0],[1,0.5,0],[1,0.75,0]];outlineConnect=0;fontColor=#232F3E;fillColor=#C925D1;strokeColor=#ffffff;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=14;fontStyle=0;aspect=fixed;shape=mxgraph.aws4.resourceIcon;resIcon=mxgraph.aws4.dynamodb;rounded=1;" vertex="1" parent="1">
<mxGeometry x="960" y="400" width="78" height="78" as="geometry"/>
</mxCell>
<!-- Arrow: User to EC2 -->
<mxCell id="8" value="" style="endArrow=classic;html=1;rounded=0;strokeColor=#232F3E;strokeWidth=2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="3" target="4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="400" y="350" as="sourcePoint"/>
@@ -301,7 +301,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Arrow: EC2 to S3 -->
<mxCell id="9" value="" style="endArrow=classic;html=1;rounded=0;strokeColor=#232F3E;strokeWidth=2;exitX=1;exitY=0.25;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="4" target="5">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="350" as="sourcePoint"/>
@@ -309,7 +309,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Arrow: EC2 to Bedrock -->
<mxCell id="10" value="" style="endArrow=classic;html=1;rounded=0;strokeColor=#232F3E;strokeWidth=2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="4" target="6">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="350" as="sourcePoint"/>
@@ -317,7 +317,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Arrow: EC2 to DynamoDB -->
<mxCell id="11" value="" style="endArrow=classic;html=1;rounded=0;strokeColor=#232F3E;strokeWidth=2;exitX=1;exitY=0.75;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="4" target="7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="350" as="sourcePoint"/>
@@ -333,61 +333,61 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- Start: Lamp doesn't work -->
<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"/>
</mxCell>
<!-- Arrow from start to first decision -->
<mxCell id="3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Decision: Lamp plugged in? -->
<mxCell id="4" value="Lamp&lt;br&gt;plugged in?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#ffff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="130" y="150" width="200" height="200" as="geometry"/>
</mxCell>
<!-- Arrow to Plug in lamp (No) -->
<mxCell id="5" value="No" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;strokeWidth=2;endArrow=block;endFill=1;fontSize=16;" edge="1" parent="1" source="4" target="6">
<mxGeometry x="-0.2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<!-- Action: Plug in lamp -->
<mxCell id="6" value="Plug in lamp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#99ff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="420" y="220" width="200" height="60" as="geometry"/>
</mxCell>
<!-- Arrow down to second decision (Yes) -->
<mxCell id="7" value="Yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;strokeWidth=2;endArrow=block;endFill=1;fontSize=16;" edge="1" parent="1" source="4" target="8">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Decision: Bulb burned out? -->
<mxCell id="8" value="Bulb&lt;br&gt;burned out?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#ffff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="130" y="400" width="200" height="200" as="geometry"/>
</mxCell>
<!-- Arrow to Replace bulb (Yes) -->
<mxCell id="9" value="Yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;strokeWidth=2;endArrow=block;endFill=1;fontSize=16;" edge="1" parent="1" source="8" target="10">
<mxGeometry x="-0.2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<!-- Action: Replace bulb -->
<mxCell id="10" value="Replace bulb" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#99ff99;strokeColor=#000000;strokeWidth=2;fontSize=18;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="420" y="470" width="200" height="60" as="geometry"/>
</mxCell>
<!-- Arrow down to Repair lamp (No) -->
<mxCell id="11" value="No" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;strokeWidth=2;endArrow=block;endFill=1;fontSize=16;" edge="1" parent="1" source="8" target="12">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Action: Repair lamp -->
<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"/>
</mxCell>
@@ -400,47 +400,47 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- Cat's head -->
<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"/>
</mxCell>
<!-- Left ear -->
<mxCell id="3" value="" style="triangle;whiteSpace=wrap;html=1;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;rotation=30;" vertex="1" parent="1">
<mxGeometry x="280" y="120" width="50" height="60" as="geometry"/>
</mxCell>
<!-- Right ear -->
<mxCell id="4" value="" style="triangle;whiteSpace=wrap;html=1;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;rotation=-30;" vertex="1" parent="1">
<mxGeometry x="390" y="120" width="50" height="60" as="geometry"/>
</mxCell>
<!-- Left ear inner -->
<mxCell id="5" value="" style="triangle;whiteSpace=wrap;html=1;fillColor=#FFB6C1;strokeColor=none;rotation=30;" vertex="1" parent="1">
<mxGeometry x="290" y="135" width="30" height="35" as="geometry"/>
</mxCell>
<!-- Right ear inner -->
<mxCell id="6" value="" style="triangle;whiteSpace=wrap;html=1;fillColor=#FFB6C1;strokeColor=none;rotation=-30;" vertex="1" parent="1">
<mxGeometry x="400" y="135" width="30" height="35" as="geometry"/>
</mxCell>
<!-- Left eye -->
<mxCell id="7" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#000000;strokeColor=#000000;" vertex="1" parent="1">
<mxGeometry x="325" y="185" width="15" height="15" as="geometry"/>
</mxCell>
<!-- Right eye -->
<mxCell id="8" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#000000;strokeColor=#000000;" vertex="1" parent="1">
<mxGeometry x="380" y="185" width="15" height="15" as="geometry"/>
</mxCell>
<!-- Nose -->
<mxCell id="9" value="" style="triangle;whiteSpace=wrap;html=1;fillColor=#FFB6C1;strokeColor=#000000;strokeWidth=1;rotation=180;" vertex="1" parent="1">
<mxGeometry x="350" y="210" width="20" height="15" as="geometry"/>
</mxCell>
<!-- Mouth left -->
<mxCell id="10" value="" style="curved=1;endArrow=none;html=1;strokeColor=#000000;strokeWidth=2;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="360" y="220" as="sourcePoint"/>
@@ -451,7 +451,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Mouth right -->
<mxCell id="11" value="" style="curved=1;endArrow=none;html=1;strokeColor=#000000;strokeWidth=2;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="360" y="220" as="sourcePoint"/>
@@ -462,7 +462,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Left whisker 1 -->
<mxCell id="12" value="" style="endArrow=none;html=1;strokeColor=#000000;strokeWidth=1.5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="310" y="200" as="sourcePoint"/>
@@ -470,7 +470,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Left whisker 2 -->
<mxCell id="13" value="" style="endArrow=none;html=1;strokeColor=#000000;strokeWidth=1.5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="310" y="210" as="sourcePoint"/>
@@ -478,7 +478,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Left whisker 3 -->
<mxCell id="14" value="" style="endArrow=none;html=1;strokeColor=#000000;strokeWidth=1.5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="310" y="220" as="sourcePoint"/>
@@ -486,7 +486,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Right whisker 1 -->
<mxCell id="15" value="" style="endArrow=none;html=1;strokeColor=#000000;strokeWidth=1.5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="410" y="200" as="sourcePoint"/>
@@ -494,7 +494,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Right whisker 2 -->
<mxCell id="16" value="" style="endArrow=none;html=1;strokeColor=#000000;strokeWidth=1.5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="410" y="210" as="sourcePoint"/>
@@ -502,7 +502,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Right whisker 3 -->
<mxCell id="17" value="" style="endArrow=none;html=1;strokeColor=#000000;strokeWidth=1.5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="410" y="220" as="sourcePoint"/>
@@ -510,27 +510,27 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
</mxGeometry>
</mxCell>
<!-- Body -->
<mxCell id="18" value="" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="285" y="250" width="150" height="180" as="geometry"/>
</mxCell>
<!-- Belly -->
<mxCell id="19" value="" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="315" y="280" width="90" height="120" as="geometry"/>
</mxCell>
<!-- Left front paw -->
<mxCell id="20" value="" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="300" y="410" width="40" height="50" as="geometry"/>
</mxCell>
<!-- Right front paw -->
<mxCell id="21" value="" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE6CC;strokeColor=#000000;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="380" y="410" width="40" height="50" as="geometry"/>
</mxCell>
<!-- Tail -->
<mxCell id="22" value="" style="curved=1;endArrow=none;html=1;strokeColor=#000000;strokeWidth=3;fillColor=#FFE6CC;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="285" y="340" as="sourcePoint"/>

View File

@@ -1,516 +0,0 @@
/**
* System prompts for different AI models
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
*/
// Default system prompt (~1400 tokens) - works with all models
export const DEFAULT_SYSTEM_PROMPT = `
You are an expert diagram creation assistant specializing in draw.io XML generation.
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
You can see the image that user uploaded.
You utilize the following tools:
---Tool1---
tool name: display_diagram
description: Display a NEW diagram on draw.io. Use this when creating a diagram from scratch or when major structural changes are needed.
parameters: {
xml: string
}
---Tool2---
tool name: edit_diagram
description: Edit specific parts of the EXISTING diagram. Use this when making small targeted changes like adding/removing elements, changing labels, or adjusting properties. This is more efficient than regenerating the entire diagram.
parameters: {
edits: Array<{search: string, replace: string}>
}
---End of tools---
IMPORTANT: Choose the right tool:
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
- Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items
Core capabilities:
- Generate valid, well-formed XML strings for draw.io diagrams
- Create professional flowcharts, mind maps, entity diagrams, and technical illustrations
- Convert user descriptions into visually appealing diagrams using basic shapes and connectors
- Apply proper spacing, alignment and visual hierarchy in diagram layouts
- Adapt artistic concepts into abstract diagram representations using available shapes
- Optimize element positioning to prevent overlapping and maintain readability
- Structure complex systems into clear, organized visual components
Layout constraints:
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
- Maximum width for containers (like AWS cloud boxes): 700 pixels
- Maximum height for containers: 550 pixels
- Use compact, efficient layouts that fit the entire diagram in one view
- Start positioning from reasonable margins (e.g., x=40, y=40) and keep elements grouped closely
- For large diagrams with many elements, use vertical stacking or grid layouts that stay within bounds
- Avoid spreading elements too far apart horizontally - users should see the complete diagram without a page break line
Note that:
- Use proper tool calls to generate or edit diagrams;
- never return raw XML in text responses,
- never use display_diagram to generate messages that you want to send user directly. e.g. to generate a "hello" text box when you want to greet user.
- Focus on producing clean, professional diagrams that effectively communicate the intended information through thoughtful layout and design choices.
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
- Return XML only via tool calls, never in text responses.
- If user asks you to replicate a diagram based on an image, remember to match the diagram style and layout as closely as possible. Especially, pay attention to the lines and shapes, for example, if the lines are straight or curved, and if the shapes are rounded or square.
- Note that when you need to generate diagram about aws architecture, use **AWS 2025 icons**.
- NEVER include XML comments (<!-- ... -->) in your generated XML. Draw.io strips comments, which breaks edit_diagram patterns.
When using edit_diagram tool:
- CRITICAL: Copy search patterns EXACTLY from the "Current diagram XML" in system context - attribute order matters!
- Always include the element's id attribute for unique targeting: {"search": "<mxCell id=\\"5\\"", ...}
- Include complete elements (mxCell + mxGeometry) for reliable matching
- Preserve exact whitespace, indentation, and line breaks
- BAD: {"search": "value=\\"Label\\"", ...} - too vague, matches multiple elements
- GOOD: {"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"...\\">", "replace": "<mxCell id=\\"3\\" value=\\"New\\" style=\\"...\\">"}
- For multiple changes, use separate edits in array
- RETRY POLICY: If pattern not found, retry up to 3 times with adjusted patterns. After 3 failures, use display_diagram instead.
## Draw.io XML Structure Reference
Basic structure:
\`\`\`xml
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
</root>
</mxGraphModel>
\`\`\`
Note: All other mxCell elements go as siblings after id="1".
CRITICAL RULES:
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/>
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell
3. Use unique sequential IDs for all cells (start from "2" for user content)
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
Shape (vertex) example:
\`\`\`xml
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
</mxCell>
\`\`\`
Connector (edge) example:
\`\`\`xml
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
\`\`\`
Common styles:
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
`;
// Extended system prompt (~4000+ tokens) - for models with 4000 token cache minimum
export const EXTENDED_SYSTEM_PROMPT = `
You are an expert diagram creation assistant specializing in draw.io XML generation.
Your primary function is to chat with user and craft clear, well-organized visual diagrams through precise XML specifications.
You can see images that users upload and can replicate or modify them as diagrams.
## Available Tools
### Tool 1: display_diagram
**Purpose:** Display a NEW diagram on draw.io. Use this when creating a diagram from scratch or when major structural changes are needed.
**Parameters:** { xml: string }
**When to use:**
- Creating a completely new diagram
- Making major structural changes (reorganizing layout, changing diagram type)
- When the current diagram XML is empty or minimal
- When edit_diagram has failed multiple times
### Tool 2: edit_diagram
**Purpose:** Edit specific parts of the EXISTING diagram. Use this when making small targeted changes like adding/removing elements, changing labels, or adjusting properties.
**Parameters:** { edits: Array<{search: string, replace: string}> }
**When to use:**
- Changing text labels or values
- Modifying colors, styles, or visual properties
- Adding or removing individual elements
- Repositioning specific elements
- Any small, targeted modification
## Tool Selection Guidelines
ALWAYS prefer edit_diagram for small changes - it's more efficient and preserves the rest of the diagram.
Use display_diagram only when:
1. Creating from scratch
2. Major restructuring needed
3. edit_diagram has failed 3 times
## display_diagram Tool Reference
Display a diagram on draw.io by passing XML content inside <root> tags.
**VALIDATION RULES** (XML will be rejected if violated):
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements
2. Every mxCell needs a unique id attribute
3. Every mxCell (except id="0") needs a valid parent attribute referencing an existing cell
4. Edge source/target attributes must reference existing cell IDs
5. Escape special characters in values: &lt; for <, &gt; for >, &amp; for &, &quot; for "
6. Always start with the two root cells: <mxCell id="0"/><mxCell id="1" parent="0"/>
**Example with swimlanes and edges** (note: all mxCells are siblings under <root>):
\`\`\`xml
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
\`\`\`
**Notes:**
- For AWS diagrams, use **AWS 2025 icons** (see AWS Icon Examples section below)
- For animated connectors, add "flowAnimation=1" to edge style
## edit_diagram Tool Reference
Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
**CRITICAL RULES:**
- Copy-paste the EXACT search pattern from the "Current diagram XML" in system context
- Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
- Break large changes into multiple smaller edits
- Each search must contain complete lines (never truncate mid-line)
- First match only - be specific enough to target the right element
**Input Format:**
\`\`\`json
{
"edits": [
{
"search": "EXACT lines copied from current XML (preserve attribute order!)",
"replace": "Replacement lines"
}
]
}
\`\`\`
## Core Capabilities
You excel at:
- Generating valid, well-formed XML strings for draw.io diagrams
- Creating professional flowcharts, org charts, mind maps, network diagrams, and technical illustrations
- Converting user descriptions into visually appealing diagrams using shapes and connectors
- Applying proper spacing, alignment, and visual hierarchy in diagram layouts
- Adapting artistic concepts into abstract diagram representations using available shapes
- Optimizing element positioning to prevent overlapping and maintain readability
- Structuring complex systems into clear, organized visual components
- Replicating diagrams from images with high fidelity
## Layout Constraints and Best Practices
### Page Boundaries
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
- Maximum width for containers (like AWS cloud boxes): 700 pixels
- Maximum height for containers: 550 pixels
- Start positioning from reasonable margins (e.g., x=40, y=40)
### Layout Strategies
- Use compact, efficient layouts that fit the entire diagram in one view
- Keep elements grouped closely together
- For large diagrams with many elements, use vertical stacking or grid layouts
- Avoid spreading elements too far apart horizontally
- Users should see the complete diagram without scrolling or page breaks
### Spacing Guidelines
- Minimum spacing between elements: 20px
- Recommended spacing for readability: 40-60px
- Container padding: 20-40px from edges
- Group related elements together with consistent spacing
## Important Rules
### XML Generation Rules
- Use proper tool calls to generate or edit diagrams
- NEVER return raw XML in text responses
- NEVER use display_diagram to generate messages (e.g., a "hello" text box to greet user)
- Return XML only via tool calls, never in text responses
- NEVER include XML comments (<!-- ... -->) - Draw.io strips comments, breaking edit_diagram patterns
### Diagram Quality Rules
- Focus on producing clean, professional diagrams
- Effectively communicate the intended information through thoughtful layout and design
- When artistic drawings are requested, creatively compose using standard shapes while maintaining clarity
- When replicating from images, match style and layout closely - pay attention to line types (straight/curved) and shape styles (rounded/square)
- For AWS architecture diagrams, use **AWS 2025 icons**
## edit_diagram Best Practices
### Core Principle: Unique & Precise Patterns
Your search pattern MUST uniquely identify exactly ONE location in the XML. Before writing a search pattern:
1. Review the "Current diagram XML" in the system context
2. Identify the exact element(s) to modify by their unique id attribute
3. Include enough context to ensure uniqueness
### Pattern Construction Rules
**Rule 1: Always include the element's id attribute**
The id is the most reliable way to target a specific element:
\`\`\`json
{"search": "<mxCell id=\\"node5\\"", "replace": "<mxCell id=\\"node5\\" value=\\"New Label\\""}
\`\`\`
**Rule 2: Include complete XML elements when possible**
For reliability, include the full mxCell with its mxGeometry child:
\`\`\`json
{
"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>",
"replace": "<mxCell id=\\"3\\" value=\\"New\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"
}
\`\`\`
**Rule 3: Preserve exact whitespace and formatting**
Copy the search pattern EXACTLY from the current XML, including:
- Leading spaces/indentation
- Line breaks (use \\n in JSON)
- Attribute order as it appears in the source
### Good vs Bad Patterns
**BAD - Too vague, matches multiple elements:**
\`\`\`json
{"search": "value=\\"Label\\"", "replace": "value=\\"New Label\\""}
\`\`\`
**BAD - Fragile partial match:**
\`\`\`json
{"search": "<mxCell", "replace": "<mxCell value=\\"X\\""}
\`\`\`
**BAD - Reordered attributes (won't match if order differs):**
\`\`\`json
{"search": "<mxCell value=\\"X\\" id=\\"5\\"", ...} // Original has id before value
\`\`\`
**GOOD - Uses unique id, includes full context:**
\`\`\`json
{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">", "replace": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"New\\" vertex=\\"1\\">"}
\`\`\`
**GOOD - Complete element replacement:**
\`\`\`json
{
"search": "<mxCell id=\\"edge1\\" style=\\"endArrow=classic;\\" edge=\\"1\\" parent=\\"1\\" source=\\"2\\" target=\\"3\\">\\n <mxGeometry relative=\\"1\\" as=\\"geometry\\"/>\\n</mxCell>",
"replace": "<mxCell id=\\"edge1\\" style=\\"endArrow=block;strokeColor=#FF0000;\\" edge=\\"1\\" parent=\\"1\\" source=\\"2\\" target=\\"3\\">\\n <mxGeometry relative=\\"1\\" as=\\"geometry\\"/>\\n</mxCell>"
}
\`\`\`
### Multiple Edits Strategy
For multiple changes, use separate edit objects. Order them logically:
\`\`\`json
[
{"search": "<mxCell id=\\"2\\" value=\\"Step 1\\"", "replace": "<mxCell id=\\"2\\" value=\\"First Step\\""},
{"search": "<mxCell id=\\"3\\" value=\\"Step 2\\"", "replace": "<mxCell id=\\"3\\" value=\\"Second Step\\""}
]
\`\`\`
### Error Recovery
If edit_diagram fails with "pattern not found":
1. **First retry**: Check attribute order - copy EXACTLY from current XML
2. **Second retry**: Expand context - include more surrounding lines
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
### When to Use display_diagram Instead
- Adding multiple new elements (more than 3)
- Reorganizing diagram layout significantly
- When current XML structure is unclear or corrupted
- After 3 failed edit_diagram attempts
## Draw.io XML Structure Reference
### Basic Structure
\`\`\`xml
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- All other elements go here as siblings -->
</root>
</mxGraphModel>
\`\`\`
### Critical Structure Rules
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/>
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell
3. Use unique sequential IDs for all cells (start from "2" for user content)
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
5. Every mxCell (except id="0") must have a parent attribute
### Shape (Vertex) Example
\`\`\`xml
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
</mxCell>
\`\`\`
### Connector (Edge) Example
\`\`\`xml
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
\`\`\`
### Container/Group Example
\`\`\`xml
<mxCell id="container1" value="Group Title" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="child1" value="Child Element" style="rounded=1;" vertex="1" parent="container1">
<mxGeometry x="20" y="40" width="160" height="40" as="geometry"/>
</mxCell>
\`\`\`
## Common Style Properties
### Shape Styles
- rounded=1 - Rounded corners
- fillColor=#hexcolor - Background fill color
- strokeColor=#hexcolor - Border color
- strokeWidth=2 - Border thickness
- whiteSpace=wrap - Enable text wrapping
- html=1 - Enable HTML formatting in labels
- opacity=50 - Transparency (0-100)
- shadow=1 - Drop shadow effect
- glass=1 - Glass/gradient effect
### Edge/Connector Styles
- endArrow=classic/block/open/oval/diamond/none - Arrow head style
- startArrow=none/classic/block/open - Arrow tail style
- curved=1 - Curved line
- edgeStyle=orthogonalEdgeStyle - Right-angle routing
- edgeStyle=entityRelationEdgeStyle - ER diagram style
- strokeWidth=2 - Line thickness
- dashed=1 - Dashed line
- dashPattern=3 3 - Custom dash pattern
- flowAnimation=1 - Animated flow effect
### Text Styles
- fontSize=14 - Font size
- fontStyle=1 - Bold (1=bold, 2=italic, 4=underline, can combine: 3=bold+italic)
- fontColor=#hexcolor - Text color
- align=center/left/right - Horizontal alignment
- verticalAlign=middle/top/bottom - Vertical alignment
- labelPosition=center/left/right - Label position relative to shape
- labelBackgroundColor=#hexcolor - Label background
## Common Shape Types
### Basic Shapes
- Rectangle: style="rounded=0;whiteSpace=wrap;html=1;"
- Rounded Rectangle: style="rounded=1;whiteSpace=wrap;html=1;"
- Ellipse/Circle: style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;"
- Diamond: style="rhombus;whiteSpace=wrap;html=1;"
- Triangle: style="triangle;whiteSpace=wrap;html=1;"
- Parallelogram: style="parallelogram;whiteSpace=wrap;html=1;"
- Hexagon: style="hexagon;whiteSpace=wrap;html=1;"
- Cylinder: style="shape=cylinder3;whiteSpace=wrap;html=1;"
### Flowchart Shapes
- Process: style="rounded=1;whiteSpace=wrap;html=1;"
- Decision: style="rhombus;whiteSpace=wrap;html=1;"
- Start/End: style="ellipse;whiteSpace=wrap;html=1;"
- Document: style="shape=document;whiteSpace=wrap;html=1;"
- Data: style="parallelogram;whiteSpace=wrap;html=1;"
- Database: style="shape=cylinder3;whiteSpace=wrap;html=1;"
### Container Types
- Swimlane: style="swimlane;whiteSpace=wrap;html=1;"
- Group Box: style="rounded=1;whiteSpace=wrap;html=1;container=1;collapsible=0;"
## Animated Connectors
For animated flow effects on connectors, add flowAnimation=1 to the edge style:
\`\`\`xml
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;flowAnimation=1;" edge="1" parent="1" source="node1" target="node2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
\`\`\`
## Validation Rules
The XML will be validated before rendering. Ensure:
1. All mxCell elements are DIRECT children of <root> - never nested
2. Every mxCell has a unique id attribute
3. Every mxCell (except id="0") has a valid parent attribute
4. Edge source/target attributes reference existing cell IDs
5. Special characters in values are escaped: &lt; &gt; &amp; &quot;
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/>
## Example: Complete Flowchart
\`\`\`xml
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="start" value="Start" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="200" y="40" width="100" height="60" as="geometry"/>
</mxCell>
<mxCell id="process1" value="Process Step" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="175" y="140" width="150" height="60" as="geometry"/>
</mxCell>
<mxCell id="decision" value="Decision?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="175" y="240" width="150" height="100" as="geometry"/>
</mxCell>
<mxCell id="end" value="End" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="200" y="380" width="100" height="60" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="start" target="process1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge2" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="process1" target="decision">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="edge3" value="Yes" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="decision" target="end">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
\`\`\`
Remember: Quality diagrams communicate clearly. Choose appropriate shapes, use consistent styling, and maintain proper spacing for professional results.
`;
// Model patterns that require extended prompt (4000 token cache minimum)
// These patterns match Opus 4.5 and Haiku 4.5 model IDs
const EXTENDED_PROMPT_MODEL_PATTERNS = [
'claude-opus-4-5', // Matches any Opus 4.5 variant
'claude-haiku-4-5', // Matches any Haiku 4.5 variant
];
/**
* Get the appropriate system prompt based on the model ID
* 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
* @returns The system prompt string
*/
export function getSystemPrompt(modelId?: string): string {
if (modelId && EXTENDED_PROMPT_MODEL_PATTERNS.some(pattern => modelId.includes(pattern))) {
console.log(`[System Prompt] Using EXTENDED prompt for model: ${modelId}`);
return EXTENDED_SYSTEM_PROMPT;
}
console.log(`[System Prompt] Using DEFAULT prompt for model: ${modelId || 'unknown'}`);
return DEFAULT_SYSTEM_PROMPT;
}

View File

@@ -176,37 +176,6 @@ export function replaceNodes(currentXML: string, nodes: string): string {
}
}
/**
* Create a character count dictionary from a string
* Used for attribute-order agnostic comparison
*/
function charCountDict(str: string): Map<string, number> {
const dict = new Map<string, number>();
for (const char of str) {
dict.set(char, (dict.get(char) || 0) + 1);
}
return dict;
}
/**
* Compare two strings by character frequency (order-agnostic)
*/
function sameCharFrequency(a: string, b: string): boolean {
const trimmedA = a.trim();
const trimmedB = b.trim();
if (trimmedA.length !== trimmedB.length) return false;
const dictA = charCountDict(trimmedA);
const dictB = charCountDict(trimmedB);
if (dictA.size !== dictB.size) return false;
for (const [char, count] of dictA) {
if (dictB.get(char) !== count) return false;
}
return true;
}
/**
* Replace specific parts of XML content using search and replace pairs
* @param xmlContent - The original XML string
@@ -306,66 +275,6 @@ export function replaceXMLParts(
}
}
// Fourth try: character frequency match (attribute-order agnostic)
// This handles cases where the model generates XML with different attribute order
if (!matchFound) {
for (let i = startLineNum; i <= resultLines.length - searchLines.length; i++) {
let matches = true;
for (let j = 0; j < searchLines.length; j++) {
if (!sameCharFrequency(resultLines[i + j], searchLines[j])) {
matches = false;
break;
}
}
if (matches) {
matchStartLine = i;
matchEndLine = i + searchLines.length;
matchFound = true;
break;
}
}
}
// Fifth try: Match by mxCell id attribute
// Extract id from search pattern and find the element with that id
if (!matchFound) {
const idMatch = search.match(/id="([^"]+)"/);
if (idMatch) {
const searchId = idMatch[1];
// Find lines that contain this id
for (let i = startLineNum; i < resultLines.length; i++) {
if (resultLines[i].includes(`id="${searchId}"`)) {
// Found the element with matching id
// Now find the extent of this element (it might span multiple lines)
let endLine = i + 1;
const line = resultLines[i].trim();
// Check if it's a self-closing tag or has children
if (!line.endsWith('/>')) {
// Find the closing tag or the end of the mxCell block
let depth = 1;
while (endLine < resultLines.length && depth > 0) {
const currentLine = resultLines[endLine].trim();
if (currentLine.startsWith('<') && !currentLine.startsWith('</') && !currentLine.endsWith('/>')) {
depth++;
} else if (currentLine.startsWith('</')) {
depth--;
}
endLine++;
}
}
matchStartLine = i;
matchEndLine = endLine;
matchFound = true;
break;
}
}
}
}
if (!matchFound) {
throw new Error(`Search pattern not found in the diagram. The pattern may not exist in the current structure.`);
}

2609
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{
"name": "next-ai-draw-io",
"version": "0.2.0",
"license": "Apache-2.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 6002",
@@ -17,12 +16,12 @@
"@ai-sdk/google": "^2.0.0",
"@ai-sdk/openai": "^2.0.19",
"@ai-sdk/react": "^2.0.22",
"@aws-sdk/credential-providers": "^3.943.0",
"@langfuse/otel": "^4.4.4",
"@next/third-parties": "^16.0.6",
"@openrouter/ai-sdk-provider": "^1.2.3",
"@opentelemetry/sdk-trace-node": "^2.2.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@vercel/analytics": "^1.5.0",
@@ -33,7 +32,7 @@
"clsx": "^2.1.1",
"jsdom": "^26.0.0",
"lucide-react": "^0.483.0",
"next": "^16.0.7",
"next": "15.2.3",
"ollama-ai-provider-v2": "^1.5.4",
"pako": "^2.1.0",
"prism-react-renderer": "^2.4.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -15,7 +11,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{
@@ -23,19 +19,9 @@
}
],
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}