diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b066920..8fcc355 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,11 @@ { "permissions": { "allow": [ - "Bash(npm update:*)" + "Bash(npm update:*)", + "WebFetch(domain:ai-sdk.dev)", + "mcp__ide__getDiagnostics", + "Bash(npx tsc:*)", + "WebFetch(domain:sdk.vercel.ai)" ], "deny": [], "ask": [] diff --git a/.gitignore b/.gitignore index 55f3d80..af5de8c 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts push-via-ec2.sh +.claude/settings.local.json diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 950a4f9..b291a0b 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -3,8 +3,6 @@ import { openai } from '@ai-sdk/openai'; import { google } from '@ai-sdk/google'; import { smoothStream, streamText, convertToModelMessages } from 'ai'; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; -import { readFileSync } from 'fs'; -import { resolve } from 'path'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { createOpenAI } from '@ai-sdk/openai'; @@ -13,18 +11,16 @@ import { replaceXMLParts } from "@/lib/utils"; export const maxDuration = 60 const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }); -// Read the XML guide from file + export async function POST(req: Request) { - const body = await req.json(); + try { + const { messages, xml } = await req.json(); - // Extract messages and xml directly from the body - const { messages, xml } = body; - const guide = readFileSync(resolve('./app/api/chat/xml_guide.md'), 'utf8'); - - // Read and escape the guide content - const systemMessage = ` -You are an expert diagram creation assistant specializing in draw.io XML generation. Your primary function is crafting clear, well-organized visual diagrams through precise XML specifications. + const systemMessage = ` +You are an expert diagram creation assistant specializing in draw.io XML generation. +Your primary function is crafting clear, well-organized visual diagrams through precise XML specifications. You can see the image that user uploaded. +When you need to generate diagram about aws architecture, use AWS 2025 icons. You utilize the following tools: ---Tool1--- tool name: display_diagram @@ -60,16 +56,17 @@ When using edit_diagram tool: - Example GOOD edit: {"search": " ", "replace": " "} - 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"}] - -here is a guide for the XML format: ${guide} `; - const lastMessage = messages[messages.length - 1]; + 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 text from the last message parts + const lastMessageText = lastMessage.parts?.find((part: any) => part.type === 'text')?.text || ''; - const formattedContent = ` + // 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 || ''} @@ -79,99 +76,118 @@ User input: ${lastMessageText} """`; - // Convert UIMessages to ModelMessages and add system message - const modelMessages = convertToModelMessages(messages); - let enhancedMessages = [...modelMessages]; + // Convert UIMessages to ModelMessages and add system message + const modelMessages = convertToModelMessages(messages); + let enhancedMessages = [...modelMessages]; - // 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') { - enhancedMessages = [ - ...enhancedMessages.slice(0, -1), - { ...lastModelMessage, content: formattedContent } - ]; + // 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 } + ]; + } } - } - console.log("Enhanced messages:", enhancedMessages); + console.log("Enhanced messages:", enhancedMessages); - const result = streamText({ - // model: google("gemini-2.5-flash-preview-05-20"), - // model: google("gemini-2.5-pro"), - // model: bedrock('anthropic.claude-sonnet-4-20250514-v1:0'), - system: systemMessage, - model: bedrock('global.anthropic.claude-sonnet-4-5-20250929-v1:0'), - // model: openrouter('moonshotai/kimi-k2:free'), - // model: model, - // providerOptions: { - // google: { - // thinkingConfig: { - // thinkingBudget: 128, - // }, - // } - // }, - // providerOptions: { - // openai: { - // reasoningEffort: "minimal" - // }, - // }, - messages: enhancedMessages, - tools: { - // Client-side tool that will be executed on the client - display_diagram: { - description: `Display a diagram on draw.io. You only need to pass the nodes inside the tag (including the tag itself) in the XML string. - For example: - - - - - + const result = streamText({ + // model: google("gemini-2.5-flash-preview-05-20"), + // model: google("gemini-2.5-pro"), + // model: bedrock('anthropic.claude-sonnet-4-20250514-v1:0'), + system: systemMessage, + model: bedrock('global.anthropic.claude-sonnet-4-5-20250929-v1:0'), + // model: openrouter('moonshotai/kimi-k2:free'), + // model: model, + // providerOptions: { + // google: { + // thinkingConfig: { + // thinkingBudget: 128, + // }, + // } + // }, + // providerOptions: { + // openai: { + // reasoningEffort: "minimal" + // }, + // }, + messages: enhancedMessages, + tools: { + // Client-side tool that will be executed on the client + display_diagram: { + description: `Display a diagram on draw.io. You only need to pass the nodes inside the tag (including the tag itself) in the XML string. + For example: + + + - - `, - 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. - + + + + `, + 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: - 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 to search for (including whitespace and indentation)"), - 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 + // Error handler function to provide detailed error messages + function errorHandler(error: unknown) { + if (error == null) { + return 'unknown error'; + } - function errorHandler(error: unknown) { - if (error == null) { - return 'unknown error'; + if (typeof error === 'string') { + return error; + } + + if (error instanceof Error) { + return error.message; + } + + return JSON.stringify(error); } - if (typeof error === 'string') { - return error; - } - - if (error instanceof Error) { - return error.message; - } - - return JSON.stringify(error); + return result.toUIMessageStreamResponse({ + onError: errorHandler, + }); + } catch (error) { + console.error('Error in chat route:', error); + return Response.json( + { error: 'Internal server error' }, + { status: 500 } + ); } - - return result.toUIMessageStreamResponse({ - onError: errorHandler, - }); }