mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
1 Commits
chore/add-
...
fix/bug-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6931077f4 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug to help us improve
|
||||
title: '[Bug] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your thoughts.
|
||||
|
||||
## Bug Description
|
||||
A brief description of the issue.
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll to '...'
|
||||
4. See error
|
||||
|
||||
## Expected Behavior
|
||||
What you expected to happen.
|
||||
|
||||
## Actual Behavior
|
||||
What actually happened.
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain the problem.
|
||||
|
||||
## Environment
|
||||
- OS: [e.g. Windows 11, macOS 14]
|
||||
- Browser: [e.g. Chrome 120, Safari 17]
|
||||
- Version: [e.g. 1.0.0]
|
||||
|
||||
## Additional Context
|
||||
Any other information about the problem.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/DayuanJiang/next-ai-draw-io/discussions
|
||||
about: Have questions or ideas? Feel free to start a discussion
|
||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature for this project
|
||||
title: '[Feature] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
|
||||
|
||||
## Feature Description
|
||||
A brief description of the feature you'd like.
|
||||
|
||||
## Problem Context
|
||||
Is this related to a problem? Please describe.
|
||||
e.g. I'm always frustrated when [...]
|
||||
|
||||
## Proposed Solution
|
||||
How you'd like this feature to work.
|
||||
|
||||
## Alternatives Considered
|
||||
Any alternative solutions or features you've considered.
|
||||
|
||||
## Additional Context
|
||||
Any other information or screenshots about the feature request.
|
||||
14
README.md
14
README.md
@@ -88,7 +88,6 @@ Diagrams are represented as XML that can be rendered in draw.io. The AI processe
|
||||
- Ollama
|
||||
- OpenRouter
|
||||
- DeepSeek
|
||||
- SiliconFlow
|
||||
|
||||
All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
||||
|
||||
@@ -116,14 +115,6 @@ docker run -d -p 3000:3000 \
|
||||
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||
```
|
||||
|
||||
Or use an env file (create one from `env.example`):
|
||||
|
||||
```bash
|
||||
cp env.example .env
|
||||
# Edit .env with your configuration
|
||||
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
Replace the environment variables with your preferred AI provider configuration. See [Multi-Provider Support](#multi-provider-support) for available options.
|
||||
@@ -141,6 +132,8 @@ cd next-ai-draw-io
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. Configure your AI provider:
|
||||
@@ -153,10 +146,9 @@ cp env.example .env.local
|
||||
|
||||
Edit `.env.local` and configure your chosen provider:
|
||||
|
||||
- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||
- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||
- Set `AI_MODEL` to the specific model you want to use
|
||||
- Add the required API keys for your provider
|
||||
- `TEMPERATURE`: Optional temperature setting (e.g., `0` for deterministic output). Leave unset for models that don't support it (e.g., reasoning models).
|
||||
- `ACCESS_CODE_LIST`: Optional access password(s), can be comma-separated for multiple passwords.
|
||||
|
||||
> Warning: If you do not set `ACCESS_CODE_LIST`, anyone can access your deployed site directly, which may lead to rapid depletion of your token. It is recommended to set this option.
|
||||
|
||||
@@ -88,7 +88,6 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- Ollama
|
||||
- OpenRouter
|
||||
- DeepSeek
|
||||
- SiliconFlow
|
||||
|
||||
除AWS Bedrock和OpenRouter外,所有提供商都支持自定义端点。
|
||||
|
||||
@@ -147,10 +146,9 @@ cp env.example .env.local
|
||||
|
||||
编辑 `.env.local` 并配置您选择的提供商:
|
||||
|
||||
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||
- 将 `AI_MODEL` 设置为您要使用的特定模型
|
||||
- 添加您的提供商所需的API密钥
|
||||
- `TEMPERATURE`:可选的温度设置(例如 `0` 表示确定性输出)。对于不支持此参数的模型(如推理模型),请不要设置。
|
||||
- `ACCESS_CODE_LIST` 访问密码,可选,可以使用逗号隔开多个密码。
|
||||
|
||||
> 警告:如果不填写 `ACCESS_CODE_LIST`,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。
|
||||
|
||||
@@ -88,7 +88,6 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- Ollama
|
||||
- OpenRouter
|
||||
- DeepSeek
|
||||
- SiliconFlow
|
||||
|
||||
AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタムエンドポイントをサポートしています。
|
||||
|
||||
@@ -147,10 +146,9 @@ cp env.example .env.local
|
||||
|
||||
`.env.local`を編集して選択したプロバイダーを設定:
|
||||
|
||||
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||
- `AI_MODEL`を使用する特定のモデルに設定
|
||||
- プロバイダーに必要なAPIキーを追加
|
||||
- `TEMPERATURE`:オプションの温度設定(例:`0`で決定論的な出力)。温度をサポートしないモデル(推論モデルなど)では設定しないでください。
|
||||
- `ACCESS_CODE_LIST` アクセスパスワード(オプション)。カンマ区切りで複数のパスワードを指定できます。
|
||||
|
||||
> 警告:`ACCESS_CODE_LIST`を設定しない場合、誰でもデプロイされたサイトに直接アクセスできるため、トークンが急速に消費される可能性があります。このオプションを設定することをお勧めします。
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
convertToModelMessages,
|
||||
createUIMessageStream,
|
||||
createUIMessageStreamResponse,
|
||||
stepCountIs,
|
||||
streamText,
|
||||
} from "ai"
|
||||
import { z } from "zod"
|
||||
@@ -64,35 +63,6 @@ function isMinimalDiagram(xml: string): boolean {
|
||||
return !stripped.includes('id="2"')
|
||||
}
|
||||
|
||||
// Helper function to fix tool call inputs for Bedrock API
|
||||
// Bedrock requires toolUse.input to be a JSON object, not a string
|
||||
function fixToolCallInputs(messages: any[]): any[] {
|
||||
return messages.map((msg) => {
|
||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
||||
return msg
|
||||
}
|
||||
const fixedContent = msg.content.map((part: any) => {
|
||||
if (part.type === "tool-call") {
|
||||
if (typeof part.input === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(part.input)
|
||||
return { ...part, input: parsed }
|
||||
} catch {
|
||||
// If parsing fails, wrap the string in an object
|
||||
return { ...part, input: { rawInput: part.input } }
|
||||
}
|
||||
}
|
||||
// Input is already an object, but verify it's not null/undefined
|
||||
if (part.input === null || part.input === undefined) {
|
||||
return { ...part, input: {} }
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
return { ...msg, content: fixedContent }
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create cached stream response
|
||||
function createCachedStreamResponse(xml: string): Response {
|
||||
const toolCallId = `cached-${Date.now()}`
|
||||
@@ -177,44 +147,20 @@ async function handleChatRequest(req: Request): Promise<Response> {
|
||||
const isFirstMessage = messages.length === 1
|
||||
const isEmptyDiagram = !xml || xml.trim() === "" || isMinimalDiagram(xml)
|
||||
|
||||
// DEBUG: Log cache check conditions
|
||||
console.log("[Cache DEBUG] messages.length:", messages.length)
|
||||
console.log("[Cache DEBUG] isFirstMessage:", isFirstMessage)
|
||||
console.log("[Cache DEBUG] xml length:", xml?.length || 0)
|
||||
console.log("[Cache DEBUG] xml preview:", xml?.substring(0, 200))
|
||||
console.log("[Cache DEBUG] isEmptyDiagram:", isEmptyDiagram)
|
||||
|
||||
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")
|
||||
|
||||
console.log("[Cache DEBUG] textPart?.text:", textPart?.text)
|
||||
console.log("[Cache DEBUG] hasFilePart:", !!filePart)
|
||||
|
||||
const cached = findCachedResponse(textPart?.text || "", !!filePart)
|
||||
|
||||
console.log("[Cache DEBUG] cached found:", !!cached)
|
||||
|
||||
if (cached) {
|
||||
console.log(
|
||||
"[Cache] Returning cached response for:",
|
||||
textPart?.text,
|
||||
)
|
||||
return createCachedStreamResponse(cached.xml)
|
||||
} else {
|
||||
console.log("[Cache DEBUG] No cache match - checking why...")
|
||||
console.log(
|
||||
"[Cache DEBUG] Exact promptText:",
|
||||
JSON.stringify(textPart?.text),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.log("[Cache DEBUG] Skipping cache check - conditions not met")
|
||||
if (!isFirstMessage)
|
||||
console.log("[Cache DEBUG] Reason: not first message")
|
||||
if (!isEmptyDiagram)
|
||||
console.log("[Cache DEBUG] Reason: diagram not empty")
|
||||
}
|
||||
// === CACHE CHECK END ===
|
||||
|
||||
@@ -243,34 +189,9 @@ ${lastMessageText}
|
||||
// Convert UIMessages to ModelMessages and add system message
|
||||
const modelMessages = convertToModelMessages(messages)
|
||||
|
||||
// Debug: log raw messages to see what's coming in
|
||||
console.log(
|
||||
"[DEBUG] Raw UI messages:",
|
||||
JSON.stringify(
|
||||
messages.map((m: any, i: number) => ({
|
||||
index: i,
|
||||
role: m.role,
|
||||
partsCount: m.parts?.length,
|
||||
parts: m.parts?.map((p: any) => ({
|
||||
type: p.type,
|
||||
toolName: p.toolName,
|
||||
toolCallId: p.toolCallId,
|
||||
state: p.state,
|
||||
inputType: p.input ? typeof p.input : undefined,
|
||||
input: p.input,
|
||||
})),
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
// Fix tool call inputs for Bedrock API (requires JSON objects, not strings)
|
||||
const fixedMessages = fixToolCallInputs(modelMessages)
|
||||
|
||||
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
||||
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
||||
let enhancedMessages = fixedMessages.filter(
|
||||
let enhancedMessages = modelMessages.filter(
|
||||
(msg: any) =>
|
||||
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
||||
)
|
||||
@@ -346,7 +267,6 @@ ${lastMessageText}
|
||||
|
||||
const result = streamText({
|
||||
model,
|
||||
stopWhen: stepCountIs(5),
|
||||
messages: allMessages,
|
||||
...(providerOptions && { providerOptions }),
|
||||
...(headers && { headers }),
|
||||
@@ -357,32 +277,6 @@ ${lastMessageText}
|
||||
userId,
|
||||
}),
|
||||
}),
|
||||
// Repair malformed tool calls (model sometimes generates invalid JSON with unescaped quotes)
|
||||
experimental_repairToolCall: async ({ toolCall }) => {
|
||||
// The toolCall.input contains the raw JSON string that failed to parse
|
||||
const rawJson =
|
||||
typeof toolCall.input === "string" ? toolCall.input : null
|
||||
|
||||
if (rawJson) {
|
||||
try {
|
||||
// Fix unescaped quotes: x="520" should be x=\"520\"
|
||||
const fixed = rawJson.replace(
|
||||
/([a-zA-Z])="(\d+)"/g,
|
||||
'$1=\\"$2\\"',
|
||||
)
|
||||
const parsed = JSON.parse(fixed)
|
||||
return {
|
||||
type: "tool-call" as const,
|
||||
toolCallId: toolCall.toolCallId,
|
||||
toolName: toolCall.toolName,
|
||||
input: JSON.stringify(parsed),
|
||||
}
|
||||
} catch {
|
||||
// Repair failed, return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
onFinish: ({ text, usage, providerMetadata }) => {
|
||||
console.log(
|
||||
"[Cache] Full providerMetadata:",
|
||||
@@ -448,9 +342,7 @@ 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
|
||||
|
||||
⚠️ JSON ESCAPING: Every " inside string values MUST be escaped as \\". Example: x=\\"100\\" y=\\"200\\" - BOTH quotes need backslashes!`,
|
||||
- First match only - be specific enough to target the right element`,
|
||||
inputSchema: z.object({
|
||||
edits: z
|
||||
.array(
|
||||
@@ -471,9 +363,7 @@ IMPORTANT: Keep edits concise:
|
||||
}),
|
||||
},
|
||||
},
|
||||
...(process.env.TEMPERATURE !== undefined && {
|
||||
temperature: parseFloat(process.env.TEMPERATURE),
|
||||
}),
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
return result.toUIMessageStreamResponse()
|
||||
|
||||
64
app/page.tsx
64
app/page.tsx
@@ -12,30 +12,23 @@ import {
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
|
||||
export default function Home() {
|
||||
const { drawioRef, handleDiagramExport, onDrawioLoad } = useDiagram()
|
||||
const { drawioRef, handleDiagramExport } = useDiagram()
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||
const [isThemeLoaded, setIsThemeLoaded] = useState(false)
|
||||
|
||||
// Load theme from localStorage after mount to avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("drawio-theme")
|
||||
if (saved === "min" || saved === "sketch") {
|
||||
setDrawioUi(saved)
|
||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem("drawio-theme")
|
||||
if (saved === "min" || saved === "sketch") return saved
|
||||
}
|
||||
setIsThemeLoaded(true)
|
||||
}, [])
|
||||
const [closeProtection, setCloseProtection] = useState(false)
|
||||
|
||||
// Load close protection setting from localStorage after mount
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
|
||||
// Default to false since auto-save handles persistence
|
||||
if (saved === "true") {
|
||||
setCloseProtection(true)
|
||||
return "min"
|
||||
})
|
||||
const [closeProtection, setCloseProtection] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
|
||||
return saved !== "false" // Default to true
|
||||
}
|
||||
}, [])
|
||||
return true
|
||||
})
|
||||
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -103,25 +96,18 @@ export default function Home() {
|
||||
}`}
|
||||
>
|
||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
||||
{isThemeLoaded ? (
|
||||
<DrawIoEmbed
|
||||
key={drawioUi}
|
||||
ref={drawioRef}
|
||||
onExport={handleDiagramExport}
|
||||
onLoad={onDrawioLoad}
|
||||
urlParameters={{
|
||||
ui: drawioUi,
|
||||
spin: true,
|
||||
libraries: false,
|
||||
saveAndExit: false,
|
||||
noExitBtn: true,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
<DrawIoEmbed
|
||||
key={drawioUi}
|
||||
ref={drawioRef}
|
||||
onExport={handleDiagramExport}
|
||||
urlParameters={{
|
||||
ui: drawioUi,
|
||||
spin: true,
|
||||
libraries: false,
|
||||
saveAndExit: false,
|
||||
noExitBtn: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
@@ -185,8 +185,7 @@ export function ChatMessageDisplay({
|
||||
const validationError = validateMxCellStructure(replacedXML)
|
||||
if (!validationError) {
|
||||
previousXML.current = convertedXml
|
||||
// Skip validation in loadDiagram since we already validated above
|
||||
onDisplayChart(replacedXML, true)
|
||||
onDisplayChart(replacedXML)
|
||||
} else {
|
||||
console.log(
|
||||
"[ChatMessageDisplay] XML validation failed:",
|
||||
@@ -509,209 +508,139 @@ export function ChatMessageDisplay({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Render parts in order, grouping consecutive text/file parts into bubbles */
|
||||
(() => {
|
||||
const parts = message.parts || []
|
||||
const groups: {
|
||||
type: "content" | "tool"
|
||||
parts: typeof parts
|
||||
startIndex: number
|
||||
}[] = []
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
const isToolPart =
|
||||
part.type?.startsWith(
|
||||
"tool-",
|
||||
)
|
||||
const isContentPart =
|
||||
part.type === "text" ||
|
||||
part.type === "file"
|
||||
|
||||
if (isToolPart) {
|
||||
groups.push({
|
||||
type: "tool",
|
||||
parts: [part],
|
||||
startIndex: index,
|
||||
})
|
||||
} else if (isContentPart) {
|
||||
const lastGroup =
|
||||
groups[
|
||||
groups.length - 1
|
||||
]
|
||||
if (
|
||||
lastGroup?.type ===
|
||||
"content"
|
||||
) {
|
||||
lastGroup.parts.push(
|
||||
part,
|
||||
)
|
||||
} else {
|
||||
groups.push({
|
||||
type: "content",
|
||||
parts: [part],
|
||||
startIndex: index,
|
||||
})
|
||||
}
|
||||
/* Text content in bubble */
|
||||
message.parts?.some(
|
||||
(part) =>
|
||||
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"
|
||||
: message.role ===
|
||||
"system"
|
||||
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
||||
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
||||
role={
|
||||
message.role === "user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
? "button"
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
|
||||
return groups.map(
|
||||
(group, groupIndex) => {
|
||||
if (group.type === "tool") {
|
||||
return renderToolPart(
|
||||
group
|
||||
.parts[0] as ToolPartLike,
|
||||
tabIndex={
|
||||
message.role === "user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
? 0
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
) {
|
||||
setEditingMessageId(
|
||||
message.id,
|
||||
)
|
||||
setEditText(
|
||||
userMessageText,
|
||||
)
|
||||
}
|
||||
|
||||
// Content bubble
|
||||
return (
|
||||
<div
|
||||
key={`${message.id}-content-${group.startIndex}`}
|
||||
className={`px-4 py-3 text-sm leading-relaxed ${
|
||||
message.role ===
|
||||
"user"
|
||||
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
||||
: message.role ===
|
||||
"system"
|
||||
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
||||
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""} ${groupIndex > 0 ? "mt-3" : ""}`}
|
||||
role={
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
? "button"
|
||||
: undefined
|
||||
}
|
||||
tabIndex={
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
? 0
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
) {
|
||||
setEditingMessageId(
|
||||
message.id,
|
||||
)
|
||||
setEditText(
|
||||
userMessageText,
|
||||
)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
(e.key ===
|
||||
"Enter" ||
|
||||
e.key ===
|
||||
" ") &&
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
) {
|
||||
e.preventDefault()
|
||||
setEditingMessageId(
|
||||
message.id,
|
||||
)
|
||||
setEditText(
|
||||
userMessageText,
|
||||
)
|
||||
}
|
||||
}}
|
||||
title={
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
? "Click to edit"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{group.parts.map(
|
||||
(
|
||||
part,
|
||||
partIndex,
|
||||
) => {
|
||||
if (
|
||||
part.type ===
|
||||
"text"
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={`${message.id}-text-${group.startIndex}-${partIndex}`}
|
||||
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
||||
message.role ===
|
||||
"user"
|
||||
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
|
||||
: "dark:prose-invert"
|
||||
}`}
|
||||
>
|
||||
<ReactMarkdown>
|
||||
{
|
||||
(
|
||||
part as {
|
||||
text: string
|
||||
}
|
||||
)
|
||||
.text
|
||||
}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (
|
||||
part.type ===
|
||||
"file"
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={`${message.id}-file-${group.startIndex}-${partIndex}`}
|
||||
className="mt-2"
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
(
|
||||
part as {
|
||||
url: string
|
||||
}
|
||||
)
|
||||
.url
|
||||
}
|
||||
width={
|
||||
200
|
||||
}
|
||||
height={
|
||||
200
|
||||
}
|
||||
alt={`Uploaded diagram or image for AI analysis`}
|
||||
className="rounded-lg border border-white/20"
|
||||
style={{
|
||||
objectFit:
|
||||
"contain",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
})()
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
(e.key === "Enter" ||
|
||||
e.key === " ") &&
|
||||
message.role ===
|
||||
"user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
) {
|
||||
e.preventDefault()
|
||||
setEditingMessageId(
|
||||
message.id,
|
||||
)
|
||||
setEditText(
|
||||
userMessageText,
|
||||
)
|
||||
}
|
||||
}}
|
||||
title={
|
||||
message.role === "user" &&
|
||||
isLastUserMessage &&
|
||||
onEditMessage
|
||||
? "Click to edit"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{message.parts?.map(
|
||||
(part, index) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<div
|
||||
key={`${message.id}-text-${index}`}
|
||||
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
||||
message.role ===
|
||||
"user"
|
||||
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
|
||||
: "dark:prose-invert"
|
||||
}`}
|
||||
>
|
||||
<ReactMarkdown>
|
||||
{
|
||||
part.text
|
||||
}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
case "file":
|
||||
return (
|
||||
<div
|
||||
key={`${message.id}-file-${part.url}`}
|
||||
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) => {
|
||||
if (part.type?.startsWith("tool-")) {
|
||||
return renderToolPart(
|
||||
part as ToolPartLike,
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
{/* Action buttons for assistant messages */}
|
||||
{message.role === "assistant" && (
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
@@ -746,14 +675,9 @@ export function ChatMessageDisplay({
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
{/* Regenerate button - only on last assistant message, not for cached examples */}
|
||||
{/* Regenerate button - only on last assistant message */}
|
||||
{onRegenerate &&
|
||||
isLastAssistantMessage &&
|
||||
!message.parts?.some((p: any) =>
|
||||
p.toolCallId?.startsWith(
|
||||
"cached-",
|
||||
),
|
||||
) && (
|
||||
isLastAssistantMessage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
import {
|
||||
DefaultChatTransport,
|
||||
lastAssistantMessageIsCompleteWithToolCalls,
|
||||
} from "ai"
|
||||
import { DefaultChatTransport } from "ai"
|
||||
import {
|
||||
CheckCircle,
|
||||
PanelRightClose,
|
||||
@@ -14,7 +11,7 @@ import {
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import type React from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { flushSync } from "react-dom"
|
||||
import { FaGithub } from "react-icons/fa"
|
||||
import { Toaster } from "sonner"
|
||||
@@ -24,16 +21,8 @@ import {
|
||||
SettingsDialog,
|
||||
STORAGE_ACCESS_CODE_KEY,
|
||||
} from "@/components/settings-dialog"
|
||||
|
||||
// localStorage keys for persistence
|
||||
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
||||
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
||||
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
||||
const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
||||
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { findCachedResponse } from "@/lib/cached-responses"
|
||||
import { formatXML } from "@/lib/utils"
|
||||
import { formatXML, validateMxCellStructure } from "@/lib/utils"
|
||||
import { ChatMessageDisplay } from "./chat-message-display"
|
||||
|
||||
interface ChatPanelProps {
|
||||
@@ -60,7 +49,6 @@ export default function ChatPanel({
|
||||
resolverRef,
|
||||
chartXML,
|
||||
clearDiagram,
|
||||
isDrawioReady,
|
||||
} = useDiagram()
|
||||
|
||||
const onFetchChart = (saveToHistory = true) => {
|
||||
@@ -92,7 +80,7 @@ export default function ChatPanel({
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||
const [, setAccessCodeRequired] = useState(false)
|
||||
const [accessCodeRequired, setAccessCodeRequired] = useState(false)
|
||||
const [input, setInput] = useState("")
|
||||
|
||||
// Check if access code is required on mount
|
||||
@@ -103,151 +91,97 @@ export default function ChatPanel({
|
||||
.catch(() => setAccessCodeRequired(false))
|
||||
}, [])
|
||||
|
||||
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
|
||||
const [sessionId, setSessionId] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem(STORAGE_SESSION_ID_KEY)
|
||||
if (saved) return saved
|
||||
}
|
||||
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
})
|
||||
// Generate a unique session ID for Langfuse tracing
|
||||
const [sessionId, setSessionId] = useState(
|
||||
() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||
)
|
||||
|
||||
// Store XML snapshots for each user message (keyed by message index)
|
||||
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map())
|
||||
|
||||
// Flag to track if we've restored from localStorage
|
||||
const hasRestoredRef = useRef(false)
|
||||
|
||||
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
|
||||
const chartXMLRef = useRef(chartXML)
|
||||
useEffect(() => {
|
||||
chartXMLRef.current = chartXML
|
||||
}, [chartXML])
|
||||
|
||||
// Ref to hold stop function for use in onToolCall (avoids stale closure)
|
||||
const stopRef = useRef<(() => void) | null>(null)
|
||||
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,
|
||||
addToolOutput,
|
||||
stop,
|
||||
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 validationError = validateMxCellStructure(xml)
|
||||
|
||||
// loadDiagram validates and returns error if invalid
|
||||
const validationError = onDisplayChart(xml)
|
||||
|
||||
if (validationError) {
|
||||
console.warn(
|
||||
"[display_diagram] Validation error:",
|
||||
validationError,
|
||||
)
|
||||
// Return error to model - sendAutomaticallyWhen will trigger retry
|
||||
const errorMessage = `${validationError}
|
||||
|
||||
Please fix the XML issues and call display_diagram again with corrected XML.
|
||||
|
||||
Your failed XML:
|
||||
\`\`\`xml
|
||||
${xml}
|
||||
\`\`\``
|
||||
addToolOutput({
|
||||
tool: "display_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: errorMessage,
|
||||
})
|
||||
} else {
|
||||
// Success - diagram will be rendered by chat-message-display
|
||||
addToolOutput({
|
||||
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 {
|
||||
console.log("[edit_diagram] Starting...")
|
||||
// Use chartXML from ref directly - more reliable than export
|
||||
// especially on Vercel where DrawIO iframe may have latency issues
|
||||
// Using ref to avoid stale closure in callback
|
||||
const cachedXML = chartXMLRef.current
|
||||
if (cachedXML) {
|
||||
currentXml = cachedXML
|
||||
console.log(
|
||||
"[edit_diagram] Using cached chartXML, length:",
|
||||
currentXml.length,
|
||||
)
|
||||
} else {
|
||||
// Fallback to export only if no cached XML
|
||||
console.log(
|
||||
"[edit_diagram] No cached XML, fetching from DrawIO...",
|
||||
)
|
||||
currentXml = await onFetchChart(false)
|
||||
console.log(
|
||||
"[edit_diagram] Got XML from export, length:",
|
||||
currentXml.length,
|
||||
)
|
||||
}
|
||||
|
||||
// loadDiagram validates and returns error if invalid
|
||||
const validationError = onDisplayChart(editedXml)
|
||||
if (validationError) {
|
||||
console.warn(
|
||||
"[edit_diagram] Validation error:",
|
||||
validationError,
|
||||
)
|
||||
addToolOutput({
|
||||
const { replaceXMLParts } = await import("@/lib/utils")
|
||||
const editedXml = replaceXMLParts(currentXml, edits)
|
||||
|
||||
onDisplayChart(editedXml)
|
||||
|
||||
addToolResult({
|
||||
tool: "edit_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: `Edit produced invalid XML: ${validationError}
|
||||
|
||||
Current diagram XML:
|
||||
\`\`\`xml
|
||||
${currentXml}
|
||||
\`\`\`
|
||||
|
||||
Please fix the edit to avoid structural issues (e.g., duplicate IDs, invalid references).`,
|
||||
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
||||
})
|
||||
return
|
||||
}
|
||||
console.log("[edit_diagram] Success")
|
||||
} catch (error) {
|
||||
console.error("[edit_diagram] Failed:", error)
|
||||
|
||||
addToolOutput({
|
||||
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)
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error)
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Use addToolOutput with state: 'output-error' for proper error signaling
|
||||
addToolOutput({
|
||||
tool: "edit_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
state: "output-error",
|
||||
errorText: `Edit failed: ${errorMessage}
|
||||
addToolResult({
|
||||
tool: "edit_diagram",
|
||||
toolCallId: toolCall.toolCallId,
|
||||
output: `Edit failed: ${errorMessage}
|
||||
|
||||
Current diagram XML:
|
||||
\`\`\`xml
|
||||
@@ -255,238 +189,47 @@ ${currentXml || "No XML available"}
|
||||
\`\`\`
|
||||
|
||||
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// Silence access code error in console since it's handled by UI
|
||||
if (!error.message.includes("Invalid or missing access code")) {
|
||||
console.error("Chat error:", error)
|
||||
}
|
||||
|
||||
// Add system message for error so it can be cleared
|
||||
setMessages((currentMessages) => {
|
||||
const errorMessage = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: "system" as const,
|
||||
content: error.message,
|
||||
parts: [{ type: "text" as const, text: error.message }],
|
||||
},
|
||||
onError: (error) => {
|
||||
// Silence access code error in console since it's handled by UI
|
||||
if (!error.message.includes("Invalid or missing access code")) {
|
||||
console.error("Chat error:", error)
|
||||
}
|
||||
return [...currentMessages, errorMessage]
|
||||
})
|
||||
|
||||
if (error.message.includes("Invalid or missing access code")) {
|
||||
// Show settings button and open dialog to help user fix it
|
||||
setAccessCodeRequired(true)
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
},
|
||||
// Auto-resubmit when all tool results are available (including errors)
|
||||
// This enables the model to retry when a tool returns an error
|
||||
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
||||
})
|
||||
// Add system message for error so it can be cleared
|
||||
setMessages((currentMessages) => {
|
||||
const errorMessage = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: "system" as const,
|
||||
content: error.message,
|
||||
parts: [{ type: "text" as const, text: error.message }],
|
||||
}
|
||||
return [...currentMessages, errorMessage]
|
||||
})
|
||||
|
||||
// Update stopRef so onToolCall can access it
|
||||
stopRef.current = stop
|
||||
|
||||
// Ref to track latest messages for unload persistence
|
||||
const messagesRef = useRef(messages)
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
}, [messages])
|
||||
if (error.message.includes("Invalid or missing access code")) {
|
||||
// Show settings button and open dialog to help user fix it
|
||||
setAccessCodeRequired(true)
|
||||
setShowSettingsDialog(true)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Restore messages and XML snapshots from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (hasRestoredRef.current) return
|
||||
hasRestoredRef.current = true
|
||||
|
||||
try {
|
||||
// Restore messages
|
||||
const savedMessages = localStorage.getItem(STORAGE_MESSAGES_KEY)
|
||||
if (savedMessages) {
|
||||
const parsed = JSON.parse(savedMessages)
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setMessages(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore XML snapshots
|
||||
const savedSnapshots = localStorage.getItem(
|
||||
STORAGE_XML_SNAPSHOTS_KEY,
|
||||
)
|
||||
if (savedSnapshots) {
|
||||
const parsed = JSON.parse(savedSnapshots)
|
||||
xmlSnapshotsRef.current = new Map(parsed)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to restore from localStorage:", error)
|
||||
}
|
||||
}, [setMessages])
|
||||
|
||||
// Restore diagram XML when DrawIO becomes ready
|
||||
const hasDiagramRestoredRef = useRef(false)
|
||||
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[ChatPanel] isDrawioReady:",
|
||||
isDrawioReady,
|
||||
"hasDiagramRestored:",
|
||||
hasDiagramRestoredRef.current,
|
||||
)
|
||||
if (!isDrawioReady || hasDiagramRestoredRef.current) return
|
||||
hasDiagramRestoredRef.current = true
|
||||
|
||||
try {
|
||||
const savedDiagramXml = localStorage.getItem(
|
||||
STORAGE_DIAGRAM_XML_KEY,
|
||||
)
|
||||
console.log(
|
||||
"[ChatPanel] Restoring diagram, has saved XML:",
|
||||
!!savedDiagramXml,
|
||||
)
|
||||
if (savedDiagramXml) {
|
||||
console.log(
|
||||
"[ChatPanel] Loading saved diagram XML, length:",
|
||||
savedDiagramXml.length,
|
||||
)
|
||||
// Skip validation for trusted saved diagrams
|
||||
onDisplayChart(savedDiagramXml, true)
|
||||
chartXMLRef.current = savedDiagramXml
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to restore diagram from localStorage:", error)
|
||||
}
|
||||
|
||||
// Allow saving after restore is complete
|
||||
setTimeout(() => {
|
||||
console.log("[ChatPanel] Enabling diagram save")
|
||||
setCanSaveDiagram(true)
|
||||
}, 500)
|
||||
}, [isDrawioReady, onDisplayChart])
|
||||
|
||||
// Save messages to localStorage whenever they change
|
||||
useEffect(() => {
|
||||
if (!hasRestoredRef.current) return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_MESSAGES_KEY, JSON.stringify(messages))
|
||||
} catch (error) {
|
||||
console.error("Failed to save messages to localStorage:", error)
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
// Save XML snapshots to localStorage whenever they change
|
||||
const saveXmlSnapshots = useCallback(() => {
|
||||
try {
|
||||
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
||||
localStorage.setItem(
|
||||
STORAGE_XML_SNAPSHOTS_KEY,
|
||||
JSON.stringify(snapshotsArray),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to save XML snapshots to localStorage:",
|
||||
error,
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save session ID to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
||||
}, [sessionId])
|
||||
|
||||
// Save current diagram XML to localStorage whenever it changes
|
||||
// Only save after initial restore is complete and if it's not an empty diagram
|
||||
useEffect(() => {
|
||||
if (!canSaveDiagram) return
|
||||
// Don't save empty diagrams (check for minimal content)
|
||||
if (chartXML && chartXML.length > 300) {
|
||||
console.log(
|
||||
"[ChatPanel] Saving diagram to localStorage, length:",
|
||||
chartXML.length,
|
||||
)
|
||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||
}
|
||||
}, [chartXML, canSaveDiagram])
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
// Save state right before page unload (refresh/close)
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = () => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STORAGE_MESSAGES_KEY,
|
||||
JSON.stringify(messagesRef.current),
|
||||
)
|
||||
localStorage.setItem(
|
||||
STORAGE_XML_SNAPSHOTS_KEY,
|
||||
JSON.stringify(
|
||||
Array.from(xmlSnapshotsRef.current.entries()),
|
||||
),
|
||||
)
|
||||
const xml = chartXMLRef.current
|
||||
if (xml && xml.length > 300) {
|
||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xml)
|
||||
}
|
||||
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist state before unload:", error)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload)
|
||||
return () =>
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||
}, [sessionId])
|
||||
|
||||
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const isProcessing = status === "streaming" || status === "submitted"
|
||||
if (input.trim() && !isProcessing) {
|
||||
// Check if input matches a cached example (only when no messages yet)
|
||||
if (messages.length === 0) {
|
||||
const cached = findCachedResponse(
|
||||
input.trim(),
|
||||
files.length > 0,
|
||||
)
|
||||
if (cached) {
|
||||
// Add user message and fake assistant response to messages
|
||||
// The chat-message-display useEffect will handle displaying the diagram
|
||||
const toolCallId = `cached-${Date.now()}`
|
||||
setMessages([
|
||||
{
|
||||
id: `user-${Date.now()}`,
|
||||
role: "user" as const,
|
||||
parts: [{ type: "text" as const, text: input }],
|
||||
},
|
||||
{
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: "assistant" as const,
|
||||
parts: [
|
||||
{
|
||||
type: "tool-display_diagram" as const,
|
||||
toolCallId,
|
||||
state: "output-available" as const,
|
||||
input: { xml: cached.xml },
|
||||
output: "Successfully displayed the diagram.",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any)
|
||||
setInput("")
|
||||
setFiles([])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let chartXml = await onFetchChart()
|
||||
chartXml = formatXML(chartXml)
|
||||
@@ -517,7 +260,6 @@ 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)
|
||||
saveXmlSnapshots()
|
||||
|
||||
const accessCode =
|
||||
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||
@@ -584,8 +326,8 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
return
|
||||
}
|
||||
|
||||
// Restore the diagram to the saved state (skip validation for trusted snapshots)
|
||||
onDisplayChart(savedXml, true)
|
||||
// Restore the diagram to the saved state
|
||||
onDisplayChart(savedXml)
|
||||
|
||||
// Update ref directly to ensure edit_diagram has the correct XML
|
||||
chartXMLRef.current = savedXml
|
||||
@@ -596,7 +338,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
xmlSnapshotsRef.current.delete(key)
|
||||
}
|
||||
}
|
||||
saveXmlSnapshots()
|
||||
|
||||
// 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
|
||||
@@ -638,8 +379,8 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
return
|
||||
}
|
||||
|
||||
// Restore the diagram to the saved state (skip validation for trusted snapshots)
|
||||
onDisplayChart(savedXml, true)
|
||||
// Restore the diagram to the saved state
|
||||
onDisplayChart(savedXml)
|
||||
|
||||
// Update ref directly to ensure edit_diagram has the correct XML
|
||||
chartXMLRef.current = savedXml
|
||||
@@ -650,7 +391,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
xmlSnapshotsRef.current.delete(key)
|
||||
}
|
||||
}
|
||||
saveXmlSnapshots()
|
||||
|
||||
// Create new parts with updated text
|
||||
const newParts = message.parts?.map((part: any) => {
|
||||
@@ -817,19 +557,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
onClearChat={() => {
|
||||
setMessages([])
|
||||
clearDiagram()
|
||||
const newSessionId = `session-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 9)}`
|
||||
setSessionId(newSessionId)
|
||||
xmlSnapshotsRef.current.clear()
|
||||
// Clear localStorage
|
||||
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
||||
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
||||
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
||||
localStorage.setItem(
|
||||
STORAGE_SESSION_ID_KEY,
|
||||
newSessionId,
|
||||
setSessionId(
|
||||
`session-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 9)}`,
|
||||
)
|
||||
xmlSnapshotsRef.current.clear()
|
||||
}}
|
||||
files={files}
|
||||
onFileChange={handleFileChange}
|
||||
|
||||
@@ -32,8 +32,7 @@ export function HistoryDialog({
|
||||
|
||||
const handleConfirmRestore = () => {
|
||||
if (selectedIndex !== null) {
|
||||
// Skip validation for trusted history snapshots
|
||||
onDisplayChart(diagramHistory[selectedIndex].xml, true)
|
||||
onDisplayChart(diagramHistory[selectedIndex].xml)
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import type React from "react"
|
||||
import { createContext, useContext, useRef, useState } from "react"
|
||||
import type { DrawIoEmbedRef } from "react-drawio"
|
||||
import type { ExportFormat } from "@/components/save-dialog"
|
||||
import { extractDiagramXML, validateMxCellStructure } from "../lib/utils"
|
||||
import { extractDiagramXML } from "../lib/utils"
|
||||
|
||||
interface DiagramContextType {
|
||||
chartXML: string
|
||||
latestSvg: string
|
||||
diagramHistory: { svg: string; xml: string }[]
|
||||
loadDiagram: (chart: string, skipValidation?: boolean) => string | null
|
||||
loadDiagram: (chart: string) => void
|
||||
handleExport: () => void
|
||||
handleExportWithoutHistory: () => void
|
||||
resolverRef: React.Ref<((value: string) => void) | null>
|
||||
@@ -22,8 +22,6 @@ interface DiagramContextType {
|
||||
format: ExportFormat,
|
||||
sessionId?: string,
|
||||
) => void
|
||||
isDrawioReady: boolean
|
||||
onDrawioLoad: () => void
|
||||
}
|
||||
|
||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||
@@ -34,20 +32,10 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
const [diagramHistory, setDiagramHistory] = useState<
|
||||
{ svg: string; xml: string }[]
|
||||
>([])
|
||||
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||
const hasCalledOnLoadRef = useRef(false)
|
||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
||||
const resolverRef = useRef<((value: string) => void) | null>(null)
|
||||
// Track if we're expecting an export for history (user-initiated)
|
||||
const expectHistoryExportRef = useRef<boolean>(false)
|
||||
|
||||
const onDrawioLoad = () => {
|
||||
// Only set ready state once to prevent infinite loops
|
||||
if (hasCalledOnLoadRef.current) return
|
||||
hasCalledOnLoadRef.current = true
|
||||
console.log("[DiagramContext] DrawIO loaded, setting ready state")
|
||||
setIsDrawioReady(true)
|
||||
}
|
||||
// Track if we're expecting an export for file save (stores raw export data)
|
||||
const saveResolverRef = useRef<{
|
||||
resolver: ((data: string) => void) | null
|
||||
@@ -73,29 +61,12 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
const loadDiagram = (
|
||||
chart: string,
|
||||
skipValidation?: boolean,
|
||||
): string | null => {
|
||||
// Validate XML structure before loading (unless skipped for internal use)
|
||||
if (!skipValidation) {
|
||||
const validationError = validateMxCellStructure(chart)
|
||||
if (validationError) {
|
||||
console.warn("[loadDiagram] Validation error:", validationError)
|
||||
return validationError
|
||||
}
|
||||
}
|
||||
|
||||
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
|
||||
setChartXML(chart)
|
||||
|
||||
const loadDiagram = (chart: string) => {
|
||||
if (drawioRef.current) {
|
||||
drawioRef.current.load({
|
||||
xml: chart,
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const handleDiagramExport = (data: any) => {
|
||||
@@ -135,8 +106,8 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const clearDiagram = () => {
|
||||
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
||||
// Skip validation for trusted internal template (loadDiagram also sets chartXML)
|
||||
loadDiagram(emptyDiagram, true)
|
||||
loadDiagram(emptyDiagram)
|
||||
setChartXML(emptyDiagram)
|
||||
setLatestSvg("")
|
||||
setDiagramHistory([])
|
||||
}
|
||||
@@ -249,8 +220,6 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
handleDiagramExport,
|
||||
clearDiagram,
|
||||
saveDiagramToFile,
|
||||
isDrawioReady,
|
||||
onDrawioLoad,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -63,19 +63,6 @@ Optional custom endpoint:
|
||||
DEEPSEEK_BASE_URL=https://your-custom-endpoint
|
||||
```
|
||||
|
||||
### SiliconFlow (OpenAI-compatible)
|
||||
|
||||
```bash
|
||||
SILICONFLOW_API_KEY=your_api_key
|
||||
AI_MODEL=deepseek-ai/DeepSeek-V3 # example; use any SiliconFlow model id
|
||||
```
|
||||
|
||||
Optional custom endpoint (defaults to the recommended domain):
|
||||
|
||||
```bash
|
||||
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflow.cn/v1
|
||||
```
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
```bash
|
||||
@@ -133,7 +120,7 @@ If you only configure **one** provider's API key, the system will automatically
|
||||
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
||||
|
||||
```bash
|
||||
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama
|
||||
AI_PROVIDER=google # or: openai, anthropic, deepseek, azure, bedrock, openrouter, ollama
|
||||
```
|
||||
|
||||
## Model Capability Requirements
|
||||
@@ -146,20 +133,6 @@ This task requires exceptionally strong model capabilities, as it involves gener
|
||||
|
||||
**Note on Ollama**: While Ollama is supported as a provider, it's generally not practical for this use case unless you're running high-capability models like DeepSeek R1 or Qwen3-235B locally.
|
||||
|
||||
## Temperature Setting
|
||||
|
||||
You can optionally configure the temperature via environment variable:
|
||||
|
||||
```bash
|
||||
TEMPERATURE=0 # More deterministic output (recommended for diagrams)
|
||||
```
|
||||
|
||||
**Important**: Leave `TEMPERATURE` unset for models that don't support temperature settings, such as:
|
||||
- GPT-5.1 and other reasoning models
|
||||
- Some specialized models
|
||||
|
||||
When unset, the model uses its default behavior.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- **Best experience**: Use models with vision support (GPT-4o, Claude, Gemini) for image-to-diagram features
|
||||
|
||||
12
env.example
12
env.example
@@ -1,6 +1,6 @@
|
||||
# AI Provider Configuration
|
||||
# AI_PROVIDER: Which provider to use
|
||||
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow
|
||||
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek
|
||||
# Default: bedrock
|
||||
AI_PROVIDER=bedrock
|
||||
|
||||
@@ -42,21 +42,11 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# DEEPSEEK_API_KEY=sk-...
|
||||
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
|
||||
|
||||
# SiliconFlow Configuration (OpenAI-compatible)
|
||||
# Base domain can be .com or .cn, defaults to https://api.siliconflow.com/v1
|
||||
# SILICONFLOW_API_KEY=sk-...
|
||||
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
||||
|
||||
# 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
|
||||
|
||||
# Temperature (Optional)
|
||||
# Controls randomness in AI responses. Lower = more deterministic.
|
||||
# Leave unset for models that don't support temperature (e.g., GPT-5.1 reasoning models)
|
||||
# TEMPERATURE=0
|
||||
|
||||
# Access Control (Optional)
|
||||
# ACCESS_CODE_LIST=your-secret-code,another-code
|
||||
|
||||
@@ -17,7 +17,6 @@ export type ProviderName =
|
||||
| "ollama"
|
||||
| "openrouter"
|
||||
| "deepseek"
|
||||
| "siliconflow"
|
||||
|
||||
interface ModelConfig {
|
||||
model: any
|
||||
@@ -48,7 +47,6 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
||||
ollama: null, // No credentials needed for local Ollama
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
siliconflow: "SILICONFLOW_API_KEY",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +90,7 @@ function validateProviderCredentials(provider: ProviderName): void {
|
||||
* Get the AI model based on environment variables
|
||||
*
|
||||
* Environment variables:
|
||||
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||
* - AI_MODEL: The model ID/name for the selected provider
|
||||
*
|
||||
* Provider-specific env vars:
|
||||
@@ -106,8 +104,6 @@ function validateProviderCredentials(provider: ProviderName): void {
|
||||
* - OPENROUTER_API_KEY: OpenRouter API key
|
||||
* - DEEPSEEK_API_KEY: DeepSeek API key
|
||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||
* - SILICONFLOW_API_KEY: SiliconFlow API key
|
||||
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
|
||||
*/
|
||||
export function getAIModel(): ModelConfig {
|
||||
const modelId = process.env.AI_MODEL
|
||||
@@ -143,7 +139,6 @@ export function getAIModel(): ModelConfig {
|
||||
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
||||
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
||||
`- AZURE_API_KEY for Azure\n` +
|
||||
`- SILICONFLOW_API_KEY for SiliconFlow\n` +
|
||||
`Or set AI_PROVIDER=ollama for local Ollama.`,
|
||||
)
|
||||
} else {
|
||||
@@ -264,20 +259,9 @@ export function getAIModel(): ModelConfig {
|
||||
}
|
||||
break
|
||||
|
||||
case "siliconflow": {
|
||||
const siliconflowProvider = createOpenAI({
|
||||
apiKey: process.env.SILICONFLOW_API_KEY,
|
||||
baseURL:
|
||||
process.env.SILICONFLOW_BASE_URL ||
|
||||
"https://api.siliconflow.com/v1",
|
||||
})
|
||||
model = siliconflowProvider.chat(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow`,
|
||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,12 @@
|
||||
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
|
||||
*/
|
||||
|
||||
// Default system prompt (~2700 tokens) - works with all models
|
||||
// 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.
|
||||
|
||||
When you are asked to create a diagram, you must first tell user you plan in text first. Plan the layout and structure that can avoid object overlapping or edge cross the objects.
|
||||
Then use display_diagram tool to generate the full draw.io XML for the entire diagram.
|
||||
|
||||
## Tone and style
|
||||
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
||||
- Be concise and to the point. Only use bullet points when needed for clarity.
|
||||
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting,.
|
||||
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools as means to communicate with the user during the session.
|
||||
|
||||
|
||||
## App Context
|
||||
You are an AI agent (powered by {{MODEL_NAME}}) inside a web app. The interface has:
|
||||
- **Left panel**: Draw.io diagram editor where diagrams are rendered
|
||||
@@ -61,8 +51,6 @@ Core capabilities:
|
||||
- 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
|
||||
@@ -94,11 +82,6 @@ When using edit_diagram tool:
|
||||
- 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.
|
||||
|
||||
⚠️ CRITICAL JSON ESCAPING: When outputting edit_diagram tool calls, you MUST escape ALL double quotes inside string values:
|
||||
- CORRECT: "y=\\"119\\"" (both quotes escaped)
|
||||
- WRONG: "y="119\\"" (missing backslash before first quote - causes JSON parse error!)
|
||||
- Every " inside a JSON string value needs \\" - no exceptions!
|
||||
|
||||
## Draw.io XML Structure Reference
|
||||
|
||||
Basic structure:
|
||||
@@ -136,11 +119,9 @@ 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 additions (~1800 tokens) - appended for models with 4000 token cache minimum
|
||||
// Total EXTENDED_SYSTEM_PROMPT = ~4500 tokens
|
||||
// Extended additions (~2600 tokens) - appended for models with 4000 token cache minimum
|
||||
const EXTENDED_ADDITIONS = `
|
||||
|
||||
## Extended Tool Reference
|
||||
@@ -232,11 +213,6 @@ Copy the search pattern EXACTLY from the current XML, including leading spaces,
|
||||
**BAD:** \`{"search": "<mxCell value=\\"X\\" id=\\"5\\""}\` - Reordered attributes won't match
|
||||
**GOOD:** \`{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">"}\` - Uses unique id with full context
|
||||
|
||||
### ⚠️ JSON Escaping (CRITICAL)
|
||||
Every double quote inside JSON string values MUST be escaped with backslash:
|
||||
- **CORRECT:** \`"x=\\"100\\" y=\\"200\\""\` - both quotes escaped
|
||||
- **WRONG:** \`"x=\\"100\\" y="200\\""\` - missing backslash causes JSON parse error!
|
||||
|
||||
### Error Recovery
|
||||
If edit_diagram fails with "pattern not found":
|
||||
1. **First retry**: Check attribute order - copy EXACTLY from current XML
|
||||
@@ -244,97 +220,81 @@ If edit_diagram fails with "pattern not found":
|
||||
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
|
||||
|
||||
## Common Style Properties
|
||||
|
||||
### Shape Styles
|
||||
- rounded=1, fillColor=#hex, strokeColor=#hex, strokeWidth=2
|
||||
- whiteSpace=wrap, html=1, opacity=50, shadow=1, glass=1
|
||||
|
||||
### Edge/Connector Styles
|
||||
- endArrow=classic/block/open/oval/diamond/none, startArrow=none/classic
|
||||
- curved=1, edgeStyle=orthogonalEdgeStyle, strokeWidth=2
|
||||
- dashed=1, dashPattern=3 3, flowAnimation=1
|
||||
|
||||
### Edge Routing Rules:
|
||||
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
|
||||
### Text Styles
|
||||
- fontSize=14, fontStyle=1 (1=bold, 2=italic, 4=underline, 3=bold+italic)
|
||||
- fontColor=#hex, align=center/left/right, verticalAlign=middle/top/bottom
|
||||
|
||||
**Rule 1: NEVER let multiple edges share the same path**
|
||||
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
|
||||
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
|
||||
## Common Shape Types
|
||||
|
||||
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
|
||||
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
|
||||
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
|
||||
### Basic Shapes
|
||||
- Rectangle: rounded=0;whiteSpace=wrap;html=1;
|
||||
- Rounded Rectangle: rounded=1;whiteSpace=wrap;html=1;
|
||||
- Ellipse/Circle: ellipse;whiteSpace=wrap;html=1;aspect=fixed;
|
||||
- Diamond: rhombus;whiteSpace=wrap;html=1;
|
||||
- Cylinder: shape=cylinder3;whiteSpace=wrap;html=1;
|
||||
|
||||
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
|
||||
- Every edge MUST have these 4 attributes set in the style
|
||||
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
|
||||
### Flowchart Shapes
|
||||
- Process: rounded=1;whiteSpace=wrap;html=1;
|
||||
- Decision: rhombus;whiteSpace=wrap;html=1;
|
||||
- Start/End: ellipse;whiteSpace=wrap;html=1;
|
||||
- Document: shape=document;whiteSpace=wrap;html=1;
|
||||
- Database: shape=cylinder3;whiteSpace=wrap;html=1;
|
||||
|
||||
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
|
||||
- Before creating an edge, identify ALL shapes positioned between source and target
|
||||
- If any shape is in the direct path, you MUST use waypoints to route around it
|
||||
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
|
||||
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
|
||||
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
|
||||
- NEVER draw a line that visually crosses over another shape's bounding box
|
||||
### Container Types
|
||||
- Swimlane: swimlane;whiteSpace=wrap;html=1;
|
||||
- Group Box: rounded=1;whiteSpace=wrap;html=1;container=1;collapsible=0;
|
||||
|
||||
**Rule 5: Plan layout strategically BEFORE generating XML**
|
||||
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
|
||||
- Space shapes 150-200px apart to create clear routing channels for edges
|
||||
- Mentally trace each edge: "What shapes are between source and target?"
|
||||
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
|
||||
|
||||
**Rule 6: Use multiple waypoints for complex routing**
|
||||
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
|
||||
- Each direction change needs a waypoint (corner point)
|
||||
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
|
||||
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
|
||||
|
||||
**Rule 7: Choose NATURAL connection points based on flow direction**
|
||||
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
|
||||
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
|
||||
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
|
||||
- For DIAGONAL connections: use the side closest to the target, not corners
|
||||
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
|
||||
|
||||
**Before generating XML, mentally verify:**
|
||||
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
|
||||
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
|
||||
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
|
||||
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
|
||||
|
||||
## Edge Examples
|
||||
|
||||
### Two edges between same nodes (CORRECT - no overlap):
|
||||
## Container/Group Example
|
||||
\`\`\`xml
|
||||
<mxCell id="e1" value="A to B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
<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="e2" value="B to A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
<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>
|
||||
\`\`\`
|
||||
|
||||
### Edge with single waypoint (simple detour):
|
||||
\`\`\`xml
|
||||
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;" edge="1" parent="1" source="a" target="b">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="300" y="150"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
\`\`\`
|
||||
## Example: Complete Flowchart
|
||||
|
||||
### Edge with waypoints (routing AROUND obstacles) - CRITICAL PATTERN:
|
||||
**Scenario:** Hotfix(right,bottom) → Main(center,top), but Develop(center,middle) is in between.
|
||||
**WRONG:** Direct diagonal line crosses over Develop
|
||||
**CORRECT:** Route around the OUTSIDE (go right first, then up)
|
||||
\`\`\`xml
|
||||
<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="750" y="80"/>
|
||||
<mxPoint x="750" y="150"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<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>
|
||||
\`\`\`
|
||||
This routes the edge to the RIGHT of all shapes (x=750), then enters Main from the right side.
|
||||
|
||||
**Key principle:** When connecting distant nodes diagonally, route along the PERIMETER of the diagram, not through the middle where other shapes exist.`
|
||||
`
|
||||
|
||||
// Extended system prompt = DEFAULT + EXTENDED_ADDITIONS
|
||||
export const EXTENDED_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT + EXTENDED_ADDITIONS
|
||||
|
||||
142
lib/utils.ts
142
lib/utils.ts
@@ -57,7 +57,6 @@ export function formatXML(xml: string, indent: string = " "): string {
|
||||
* Efficiently converts a potentially incomplete XML string to a legal XML string by closing any open tags properly.
|
||||
* Additionally, if an <mxCell> tag does not have an mxGeometry child (e.g. <mxCell id="3">),
|
||||
* it removes that tag from the output.
|
||||
* Also removes orphaned <mxPoint> elements that aren't inside <Array> or don't have proper 'as' attribute.
|
||||
* @param xmlString The potentially incomplete XML string
|
||||
* @returns A legal XML string with properly closed tags and removed incomplete mxCell elements.
|
||||
*/
|
||||
@@ -70,34 +69,10 @@ export function convertToLegalXml(xmlString: string): string {
|
||||
|
||||
while ((match = regex.exec(xmlString)) !== null) {
|
||||
// match[0] contains the entire matched mxCell block
|
||||
let cellContent = match[0]
|
||||
|
||||
// Remove orphaned <mxPoint> elements that are directly inside <mxGeometry>
|
||||
// without an 'as' attribute (like as="sourcePoint", as="targetPoint")
|
||||
// and not inside <Array as="points">
|
||||
// These cause "Could not add object mxPoint" errors in draw.io
|
||||
// First check if there's an <Array as="points"> - if so, keep all mxPoints inside it
|
||||
const hasArrayPoints = /<Array\s+as="points">/.test(cellContent)
|
||||
if (!hasArrayPoints) {
|
||||
// Remove mxPoint elements without 'as' attribute
|
||||
cellContent = cellContent.replace(
|
||||
/<mxPoint\b[^>]*\/>/g,
|
||||
(pointMatch) => {
|
||||
// Keep if it has an 'as' attribute
|
||||
if (/\sas=/.test(pointMatch)) {
|
||||
return pointMatch
|
||||
}
|
||||
// Remove orphaned mxPoint
|
||||
return ""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Indent each line of the matched block for readability.
|
||||
const formatted = cellContent
|
||||
const formatted = match[0]
|
||||
.split("\n")
|
||||
.map((line) => " " + line.trim())
|
||||
.filter((line) => line.trim()) // Remove empty lines from removed mxPoints
|
||||
.join("\n")
|
||||
result += formatted + "\n"
|
||||
}
|
||||
@@ -251,6 +226,7 @@ export function replaceXMLParts(
|
||||
): string {
|
||||
// Format the XML first to ensure consistent line breaks
|
||||
let result = formatXML(xmlContent)
|
||||
let lastProcessedIndex = 0
|
||||
|
||||
for (const { search, replace } of searchReplacePairs) {
|
||||
// Also format the search content for consistency
|
||||
@@ -265,10 +241,18 @@ export function replaceXMLParts(
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
// Always search from the beginning - pairs may not be in document order
|
||||
const startLineNum = 0
|
||||
// Find the line number where lastProcessedIndex falls
|
||||
let startLineNum = 0
|
||||
let currentIndex = 0
|
||||
while (
|
||||
currentIndex < lastProcessedIndex &&
|
||||
startLineNum < resultLines.length
|
||||
) {
|
||||
currentIndex += resultLines[startLineNum].length + 1 // +1 for \n
|
||||
startLineNum++
|
||||
}
|
||||
|
||||
// Try to find match using multiple strategies
|
||||
// Try to find exact match starting from lastProcessedIndex
|
||||
let matchFound = false
|
||||
let matchStartLine = -1
|
||||
let matchEndLine = -1
|
||||
@@ -413,73 +397,6 @@ export function replaceXMLParts(
|
||||
}
|
||||
}
|
||||
|
||||
// Sixth try: Match by value attribute (label text)
|
||||
// Extract value from search pattern and find elements with that value
|
||||
if (!matchFound) {
|
||||
const valueMatch = search.match(/value="([^"]*)"/)
|
||||
if (valueMatch) {
|
||||
const searchValue = valueMatch[0] // Use full match like value="text"
|
||||
for (let i = startLineNum; i < resultLines.length; i++) {
|
||||
if (resultLines[i].includes(searchValue)) {
|
||||
// Found element with matching value
|
||||
let endLine = i + 1
|
||||
const line = resultLines[i].trim()
|
||||
|
||||
if (!line.endsWith("/>")) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seventh try: Normalized whitespace match
|
||||
// Collapse all whitespace and compare
|
||||
if (!matchFound) {
|
||||
const normalizeWs = (s: string) => s.replace(/\s+/g, " ").trim()
|
||||
const normalizedSearch = normalizeWs(search)
|
||||
|
||||
for (
|
||||
let i = startLineNum;
|
||||
i <= resultLines.length - searchLines.length;
|
||||
i++
|
||||
) {
|
||||
// Build a normalized version of the candidate lines
|
||||
const candidateLines = resultLines.slice(
|
||||
i,
|
||||
i + searchLines.length,
|
||||
)
|
||||
const normalizedCandidate = normalizeWs(
|
||||
candidateLines.join(" "),
|
||||
)
|
||||
|
||||
if (normalizedCandidate === normalizedSearch) {
|
||||
matchStartLine = i
|
||||
matchEndLine = i + searchLines.length
|
||||
matchFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchFound) {
|
||||
throw new Error(
|
||||
`Search pattern not found in the diagram. The pattern may not exist in the current structure.`,
|
||||
@@ -502,6 +419,12 @@ export function replaceXMLParts(
|
||||
]
|
||||
|
||||
result = newResultLines.join("\n")
|
||||
|
||||
// Update lastProcessedIndex to the position after the replacement
|
||||
lastProcessedIndex = 0
|
||||
for (let i = 0; i < matchStartLine + replaceLines.length; i++) {
|
||||
lastProcessedIndex += newResultLines[i].length + 1
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -614,33 +537,6 @@ export function validateMxCellStructure(xml: string): string | null {
|
||||
return `Invalid XML: Found edges with invalid source/target references (${invalidConnections.slice(0, 3).join(", ")}). Edge source and target must reference existing cell IDs. Please regenerate the diagram with valid edge connections.`
|
||||
}
|
||||
|
||||
// Check for orphaned mxPoint elements (not inside <Array as="points"> and without 'as' attribute)
|
||||
// These cause "Could not add object mxPoint" errors in draw.io
|
||||
const allMxPoints = doc.querySelectorAll("mxPoint")
|
||||
const orphanedMxPoints: string[] = []
|
||||
allMxPoints.forEach((point) => {
|
||||
const hasAsAttr = point.hasAttribute("as")
|
||||
const parentIsArray =
|
||||
point.parentElement?.tagName === "Array" &&
|
||||
point.parentElement?.getAttribute("as") === "points"
|
||||
|
||||
if (!hasAsAttr && !parentIsArray) {
|
||||
// Find the parent mxCell to report which edge has the problem
|
||||
let parent = point.parentElement
|
||||
while (parent && parent.tagName !== "mxCell") {
|
||||
parent = parent.parentElement
|
||||
}
|
||||
const cellId = parent?.getAttribute("id") || "unknown"
|
||||
if (!orphanedMxPoints.includes(cellId)) {
|
||||
orphanedMxPoints.push(cellId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (orphanedMxPoints.length > 0) {
|
||||
return `Invalid XML: Found orphaned mxPoint elements in cells (${orphanedMxPoints.slice(0, 3).join(", ")}). mxPoint elements must either have an 'as' attribute (e.g., as="sourcePoint") or be inside <Array as="points">. For edge waypoints, use: <Array as="points"><mxPoint x="..." y="..."/></Array>. Please fix the mxPoint structure.`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
194
package-lock.json
generated
194
package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"@ai-sdk/deepseek": "^1.0.30",
|
||||
"@ai-sdk/google": "^2.0.0",
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@ai-sdk/react": "^2.0.107",
|
||||
"@ai-sdk/react": "^2.0.22",
|
||||
"@aws-sdk/credential-providers": "^3.943.0",
|
||||
"@langfuse/client": "^4.4.9",
|
||||
"@langfuse/otel": "^4.4.4",
|
||||
@@ -90,6 +90,23 @@
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/amazon-bedrock/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz",
|
||||
"integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"eventsource-parser": "^3.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/anthropic": {
|
||||
"version": "2.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.50.tgz",
|
||||
@@ -106,6 +123,23 @@
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz",
|
||||
"integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"eventsource-parser": "^3.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/azure": {
|
||||
"version": "2.0.69",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/azure/-/azure-2.0.69.tgz",
|
||||
@@ -157,15 +191,32 @@
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/gateway": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz",
|
||||
"integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==",
|
||||
"node_modules/@ai-sdk/deepseek/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz",
|
||||
"integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@vercel/oidc": "3.0.5"
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"eventsource-parser": "^3.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/gateway": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.7.tgz",
|
||||
"integrity": "sha512-/AI5AKi4vOK9SEb8Z1dfXkhsJ5NAfWsoJQc96B/mzn2KIrjw5occOjIwD06scuhV9xWlghCoXJT1sQD9QH/tyg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.16",
|
||||
"@vercel/oidc": "3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -249,6 +300,23 @@
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz",
|
||||
"integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"eventsource-parser": "^3.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz",
|
||||
@@ -279,9 +347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz",
|
||||
"integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==",
|
||||
"version": "3.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.16.tgz",
|
||||
"integrity": "sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
@@ -296,13 +364,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react": {
|
||||
"version": "2.0.107",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.107.tgz",
|
||||
"integrity": "sha512-rv0u+tAi2r2zJu2uSLXcC3TBgGrkQIWXRM+i6us6qcGmYQ2kOu2VYg+lxviOSGPhL9PVebvTlN5x8mf3rDqX+w==",
|
||||
"version": "2.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.22.tgz",
|
||||
"integrity": "sha512-nJt2U0ZDjpdPEIHCEWlxOixUhQyA/teQ0y9gz66mYW40OhBjSsZjcEAYhbS05mvy+NMVqzlE3sVu54DqzjR68w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"ai": "5.0.107",
|
||||
"@ai-sdk/provider-utils": "3.0.5",
|
||||
"ai": "5.0.22",
|
||||
"swr": "^2.2.5",
|
||||
"throttleit": "2.1.0"
|
||||
},
|
||||
@@ -311,7 +379,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
@@ -319,6 +387,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react/node_modules/@ai-sdk/gateway": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.11.tgz",
|
||||
"integrity": "sha512-ErwWS3sPOuWy42eE3AVxlKkTa1XjjKBEtNCOylVKMO5KNyz5qie8QVlLYbULOG56dtxX4zTKX3rQNJudplhcmQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.5.tgz",
|
||||
"integrity": "sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"eventsource-parser": "^3.0.3",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils/node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.6",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react/node_modules/ai": {
|
||||
"version": "5.0.22",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.22.tgz",
|
||||
"integrity": "sha512-RZiYhj7Ux7hrLtXkHPcxzdiSZt4NOiC69O5AkNfMCsz3twwz/KRkl9ASptosoOsg833s5yRcTSdIu5z53Sl6Pw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "1.0.11",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.5",
|
||||
"@opentelemetry/api": "1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@@ -5870,9 +5999,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/oidc": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
|
||||
"integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz",
|
||||
"integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
@@ -5920,14 +6049,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "5.0.107",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.107.tgz",
|
||||
"integrity": "sha512-laZlS9ZC/DZfSaxPgrBqI4mM+kxRvTPBBQfa74ceBFskkunZKEsaGVFNEs4cfyGa3nCCCl1WO/fjxixp4V8Zag==",
|
||||
"version": "5.0.90",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.90.tgz",
|
||||
"integrity": "sha512-bawNN10N2cXzFedbDdNUZo8KkcGp12VX1b+mCL5dfllh6WmLsIYYME7GVxsRJvHvPP7xRhuds5fn0jtLyxGnZw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "2.0.18",
|
||||
"@ai-sdk/gateway": "2.0.7",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@ai-sdk/provider-utils": "3.0.16",
|
||||
"@opentelemetry/api": "1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -10614,6 +10743,23 @@
|
||||
"zod": "^4.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/ollama-ai-provider-v2/node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "3.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz",
|
||||
"integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"eventsource-parser": "^3.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||
|
||||
@@ -19,14 +19,13 @@
|
||||
"@ai-sdk/deepseek": "^1.0.30",
|
||||
"@ai-sdk/google": "^2.0.0",
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@ai-sdk/react": "^2.0.107",
|
||||
"@ai-sdk/react": "^2.0.22",
|
||||
"@aws-sdk/credential-providers": "^3.943.0",
|
||||
"@langfuse/client": "^4.4.9",
|
||||
"@langfuse/otel": "^4.4.4",
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
"@next/third-parties": "^16.0.6",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
|
||||
Reference in New Issue
Block a user