From 41c410c2ba019eed7e5d2034e0db09b817eb0a7a Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:04:38 +0900 Subject: [PATCH 01/63] fix: bind MCP server HTTP to 127.0.0.1 only (#787) The embedded HTTP sidecar was using server.listen(port) without a host argument, which defaults to 0.0.0.0 (all interfaces). This exposed the server to the local network. Now explicitly binds to 127.0.0.1. Also excludes release/ from tsconfig to fix pre-existing TS errors. Bumps @next-ai-drawio/mcp-server to 0.1.18. --- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/http-server.ts | 2 +- tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 2690aaf5..ae9360b1 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.17", + "version": "0.1.18", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "type": "module", "main": "dist/index.js", diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index d3f868df..d2f16d20 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -155,7 +155,7 @@ export function startHttpServer(port = 6002): Promise { } }) - server.listen(port, () => { + server.listen(port, "127.0.0.1", () => { serverPort = port log.info(`HTTP server running on http://localhost:${port}`) resolve(port) diff --git a/tsconfig.json b/tsconfig.json index 8a4a94c0..e5f90e41 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,7 @@ "packages", "electron", "electron-standalone", - "dist-electron" + "dist-electron", + "release" ] } From 31819f413cc4b329a1cb81e5fccd0cd98c1fd665 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:14:20 +0900 Subject: [PATCH 02/63] fix: add 10MB body size limit to MCP HTTP endpoints (#791) All three POST handlers (/api/state, /api/restore, /api/history-svg) now use a shared readBody() helper that enforces a 10MB limit and returns 413 if exceeded, preventing memory exhaustion from oversized requests. Bumps @next-ai-drawio/mcp-server to 0.1.19. --- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/http-server.ts | 41 ++++++++++++++++---------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index ae9360b1..0026b2bd 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.18", + "version": "0.1.19", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "type": "module", "main": "dist/index.js", diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index d2f16d20..c75a48f8 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -4,6 +4,29 @@ */ import http from "node:http" + +const MAX_BODY_BYTES = 10 * 1024 * 1024 // 10 MiB + +function readBody( + req: http.IncomingMessage, + res: http.ServerResponse, + cb: (body: string) => void, +): void { + let body = "" + let size = 0 + req.on("data", (chunk: Buffer) => { + size += chunk.length + if (size > MAX_BODY_BYTES) { + res.writeHead(413, { "Content-Type": "application/json" }) + res.end(JSON.stringify({ error: "Payload too large" })) + req.destroy() + return + } + body += chunk + }) + req.on("end", () => cb(body)) +} + import { addHistory, clearHistory, @@ -266,11 +289,7 @@ function handleStateApi( }), ) } else if (req.method === "POST") { - let body = "" - req.on("data", (chunk) => { - body += chunk - }) - req.on("end", () => { + readBody(req, res, (body) => { try { const data = JSON.parse(body) const { sessionId } = data @@ -347,11 +366,7 @@ function handleRestoreApi( return } - let body = "" - req.on("data", (chunk) => { - body += chunk - }) - req.on("end", () => { + readBody(req, res, (body) => { try { const { sessionId, index } = JSON.parse(body) if (!sessionId || index === undefined) { @@ -393,11 +408,7 @@ function handleHistorySvgApi( return } - let body = "" - req.on("data", (chunk) => { - body += chunk - }) - req.on("end", () => { + readBody(req, res, (body) => { try { const { sessionId, svg } = JSON.parse(body) if (!sessionId || !svg) { From 1f31692701eec24f8511922cc47e0959cd526eae Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:14:50 +0900 Subject: [PATCH 03/63] chore: always bundle latest draw.io version in Electron builds (#792) Remove pinned v29.3.5 tag so the build always clones the latest draw.io. This adds the Animated GIF export and other new features to the Electron app. Closes #770 --- .github/workflows/electron-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index 54bf7050..9e8ef191 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -37,7 +37,7 @@ jobs: - name: Download draw.io static files for offline use run: | rm -rf public/drawio - git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio + git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio mkdir -p public/drawio cp -r /tmp/drawio/src/main/webapp/* public/drawio/ rm -rf public/drawio/WEB-INF @@ -70,7 +70,7 @@ jobs: shell: bash run: | rm -rf public/drawio - git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio + git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio mkdir -p public/drawio cp -r /tmp/drawio/src/main/webapp/* public/drawio/ rm -rf public/drawio/WEB-INF From b9fdf9538c38f7d34d4f7137d9ef6e0dcd3badae Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:17:38 +0900 Subject: [PATCH 04/63] chore: remove MCP preview labels (#790) MCP server is no longer in preview. Remove "(Preview)" headings from READMEs, the purple PREVIEW badge from the UI, and the preview i18n keys. --- README.md | 6 ++---- components/chat-example-panel.tsx | 3 --- docs/cn/README_CN.md | 6 ++---- docs/ja/README_JA.md | 6 ++---- lib/i18n/dictionaries/en.json | 3 +-- lib/i18n/dictionaries/ja.json | 3 +-- lib/i18n/dictionaries/zh-Hant.json | 3 +-- lib/i18n/dictionaries/zh.json | 3 +-- 8 files changed, 10 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1073662d..72839c28 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1 - [Table of Contents](#table-of-contents) - [Examples](#examples) - [Features](#features) - - [MCP Server (Preview)](#mcp-server-preview) + - [MCP Server](#mcp-server) - [Claude Code CLI](#claude-code-cli) - [Getting Started](#getting-started) - [Try it Online](#try-it-online) @@ -99,9 +99,7 @@ Here are some example prompts and their generated diagrams: - **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure) - **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization -## MCP Server (Preview) - -> **Preview Feature**: This feature is experimental and may not be stable. +## MCP Server Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol). diff --git a/components/chat-example-panel.tsx b/components/chat-example-panel.tsx index a74f42cc..4e721292 100644 --- a/components/chat-example-panel.tsx +++ b/components/chat-example-panel.tsx @@ -141,9 +141,6 @@ export default function ExamplePanel({ {dict.examples.mcpServer} - - {dict.examples.preview} -

{dict.examples.mcpDescription} diff --git a/docs/cn/README_CN.md b/docs/cn/README_CN.md index 2af8ab5c..028af1c5 100644 --- a/docs/cn/README_CN.md +++ b/docs/cn/README_CN.md @@ -30,7 +30,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 - [目录](#目录) - [示例](#示例) - [功能特性](#功能特性) - - [MCP服务器(预览)](#mcp服务器预览) + - [MCP服务器](#mcp服务器) - [Claude Code CLI](#claude-code-cli) - [快速开始](#快速开始) - [在线试用](#在线试用) @@ -98,9 +98,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 - **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure) - **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果 -## MCP服务器(预览) - -> **预览功能**:此功能为实验性功能,可能不稳定。 +## MCP服务器 通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。 diff --git a/docs/ja/README_JA.md b/docs/ja/README_JA.md index dbf211b4..50b35b6b 100644 --- a/docs/ja/README_JA.md +++ b/docs/ja/README_JA.md @@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 - [目次](#目次) - [例](#例) - [機能](#機能) - - [MCPサーバー(プレビュー)](#mcpサーバープレビュー) + - [MCPサーバー](#mcpサーバー) - [Claude Code CLI](#claude-code-cli) - [はじめに](#はじめに) - [オンラインで試す](#オンラインで試す) @@ -96,9 +96,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 - **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure) - **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成 -## MCPサーバー(プレビュー) - -> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。 +## MCPサーバー MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。 diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 0c665700..2e005025 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -76,8 +76,7 @@ "creativeDescription": "Draw something fun and creative", "cachedNote": "Examples are cached for instant response", "mcpServer": "MCP Server", - "mcpDescription": "Use in Claude Desktop, VS Code & Cursor", - "preview": "PREVIEW" + "mcpDescription": "Use in Claude Desktop, VS Code & Cursor" }, "settings": { "title": "Settings", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 925a26fa..189da7f7 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -76,8 +76,7 @@ "creativeDescription": "楽しくてクリエイティブなものを描く", "cachedNote": "例はキャッシュされ、即座に応答します", "mcpServer": "MCP サーバー", - "mcpDescription": "Claude Desktop、VS Code、Cursor で使用", - "preview": "プレビュー" + "mcpDescription": "Claude Desktop、VS Code、Cursor で使用" }, "settings": { "title": "設定", diff --git a/lib/i18n/dictionaries/zh-Hant.json b/lib/i18n/dictionaries/zh-Hant.json index dd8f2d85..396459d1 100644 --- a/lib/i18n/dictionaries/zh-Hant.json +++ b/lib/i18n/dictionaries/zh-Hant.json @@ -76,8 +76,7 @@ "creativeDescription": "繪製有趣且富有創意的內容", "cachedNote": "範例已快取,可即時回應", "mcpServer": "MCP 伺服器", - "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用", - "preview": "預覽" + "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用" }, "settings": { "title": "設定", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 57a03d47..2f3c266d 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -76,8 +76,7 @@ "creativeDescription": "绘制有趣且富有创意的内容", "cachedNote": "示例已缓存,可即时响应", "mcpServer": "MCP 服务器", - "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用", - "preview": "预览" + "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用" }, "settings": { "title": "设置", From 43ddb7a999192380acb2327638912346802f856c Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 10 Apr 2026 09:23:39 +0800 Subject: [PATCH 05/63] fix: allow Qwen3.5 models to use image input (fixes #799) (#800) Qwen3.5 models deployed via vLLM natively support image input, but the supportsImageInput() check was incorrectly blocking them. The function only exempted qwen3.5-plus and qwen3.5-flash variants, missing the base qwen3.5 model name. Simplify the exception to cover all qwen3.5 variants with a single substring check on "qwen3.5", since it is a common prefix of all three. Co-authored-by: octo-patch --- lib/ai-providers.ts | 5 ++--- tests/unit/ai-providers.test.ts | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index b355a548..38ce84bc 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -1356,12 +1356,11 @@ export function supportsImageInput(modelId: string): boolean { } // Qwen text models (not vision variants like qwen-vl) - // qwen3.5-plus is a vision model + // Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input if ( lowerModelId.includes("qwen") && !hasVisionIndicator && - !lowerModelId.includes("qwen3.5-plus") && - !lowerModelId.includes("qwen3.5-flash") + !lowerModelId.includes("qwen3.5") ) { return false } diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts index da786dc5..5b74bab4 100644 --- a/tests/unit/ai-providers.test.ts +++ b/tests/unit/ai-providers.test.ts @@ -200,6 +200,8 @@ describe("supportsImageInput", () => { it("returns true for Qwen vision models", () => { expect(supportsImageInput("qwen-vl")).toBe(true) + expect(supportsImageInput("Qwen3.5")).toBe(true) + expect(supportsImageInput("qwen3.5")).toBe(true) expect(supportsImageInput("qwen3.5-plus")).toBe(true) expect(supportsImageInput("qwen3.5-flash")).toBe(true) expect(supportsImageInput("qwen3-vl-plus")).toBe(true) From 622aa8683d4d3194945cf6c43828e7af7d4bd62f Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 10 Apr 2026 09:30:05 +0800 Subject: [PATCH 06/63] fix: remove redundant status(modified:false) call to restore undo/redo (fixes #779) (#780) --- app/[lang]/page.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index 0170bf34..e44bd6ca 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -100,12 +100,8 @@ export default function Home() { const handleDrawioAutoSave = useCallback( (data: { xml?: string }) => { handleDiagramAutoSave(data) - // Only suppress modified state when persistence is available - if (canPersist) { - drawioRef.current?.status({ message: "", modified: false }) - } }, - [canPersist, drawioRef, handleDiagramAutoSave], + [handleDiagramAutoSave], ) const handleDarkModeChange = () => { From ccd9c1f48ea47ef77a2c2e9ff127a37c5b59e760 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:12:50 +0900 Subject: [PATCH 07/63] fix: reduce CPU usage during large XML streaming (#801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: reduce CPU usage during large XML streaming Skip Prism syntax highlighting while tool call is streaming — use plain

 during streaming and only run Prism once at completion. Also throttle
scrollIntoView to once per 150ms to avoid layout thrashing.

Profiled with Playwright + CDP: for 200-cell diagrams the longest
browser task dropped from 6.7s to ~450ms and total long-task time
fell from ~10.9s to ~1.1s.

* fix: add trailing edge to scroll throttle

Ensures the chat scrolls to the bottom after streaming ends, even if
the last messages update arrives during the 150ms throttle window.
---
 components/chat-message-display.tsx | 14 ++++++++++++--
 components/chat/ToolCallCard.tsx    | 17 ++++++++++++++++-
 2 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx
index 69d5f860..ada6ae57 100644
--- a/components/chat-message-display.tsx
+++ b/components/chat-message-display.tsx
@@ -417,6 +417,7 @@ export function ChatMessageDisplay({
 
     // Track previous message count to detect bulk loads vs streaming
     const prevMessageCountRef = useRef(0)
+    const scrollThrottleRef = useRef | null>(null)
 
     useEffect(() => {
         if (messagesEndRef.current && messages.length > 0) {
@@ -430,8 +431,17 @@ export function ChatMessageDisplay({
                 return
             }
 
-            // Single message added - smooth scroll
-            messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
+            // Throttle scroll during streaming to avoid layout thrashing
+            // Leading + trailing: scroll immediately, then once more after cooldown
+            if (!scrollThrottleRef.current) {
+                messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
+                scrollThrottleRef.current = setTimeout(() => {
+                    scrollThrottleRef.current = null
+                    messagesEndRef.current?.scrollIntoView({
+                        behavior: "smooth",
+                    })
+                }, 150)
+            }
         }
     }, [messages])
 
diff --git a/components/chat/ToolCallCard.tsx b/components/chat/ToolCallCard.tsx
index 392c60ae..ca0a4c17 100644
--- a/components/chat/ToolCallCard.tsx
+++ b/components/chat/ToolCallCard.tsx
@@ -195,7 +195,22 @@ export function ToolCallCard({
             {input && isExpanded && (
                 
{typeof input === "object" && input.xml ? ( - + state === "input-streaming" || + state === "input-available" ? ( +
+                                {input.xml}
+                            
+ ) : ( + + ) ) : typeof input === "object" && input.operations && Array.isArray(input.operations) ? ( From 49bfb51b10fad406eeb088c11d7c7e70e53f3ae5 Mon Sep 17 00:00:00 2001 From: Khanh Thanh <100078354+khanhthanhdev@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:11:34 +0700 Subject: [PATCH 08/63] feat: Update some tool demos in english about page (#598) * feat: update about page examples with new diagrams Replace old cloud architecture and cat examples with ResNet50, RAG, Authentication, Agile Scrum, and Open Innovation diagrams. Update all three language versions (EN, CN, JA) for consistency. * fix: correct ResNet50 prompt text and keep prompts in English Update the ResNet50 example prompt to match the actual diagram. Keep all prompt texts in English across CN and JA about pages since prompts are what users type into the AI. * docs: update README examples to match about page Replace GCP, AWS, Azure examples with ResNet50, RAG, Auth, Agile Scrum, and Open Innovation. Keep Animated Transformer and Cat Sketch. Keep prompts in English across all versions. * docs: remove ResNet50 and Agile Scrum examples from READMEs --------- Co-authored-by: dayuan.jiang --- README.md | 20 +++--- app/[lang]/about/cn/page.tsx | 124 +++++++++++++++++++--------------- app/[lang]/about/ja/page.tsx | 125 ++++++++++++++++++---------------- app/[lang]/about/page.tsx | 126 +++++++++++++++++++---------------- docs/cn/README_CN.md | 22 +++--- docs/ja/README_JA.md | 22 +++--- public/agile_scrum.svg | 1 + public/auth.svg | 1 + public/inno.svg | 1 + public/rag_prod.svg | 1 + public/resnet50.svg | 1 + 11 files changed, 243 insertions(+), 201 deletions(-) create mode 100644 public/agile_scrum.svg create mode 100644 public/auth.svg create mode 100644 public/inno.svg create mode 100644 public/rag_prod.svg create mode 100644 public/resnet50.svg diff --git a/README.md b/README.md index 72839c28..699b2bc6 100644 --- a/README.md +++ b/README.md @@ -63,24 +63,24 @@ Here are some example prompts and their generated diagrams: - GCP architecture diagram
-

Prompt: Generate a GCP architecture diagram with **GCP icons**. In this diagram, users connect to a frontend hosted on an instance.

- GCP Architecture Diagram + RAG Technique Diagram
+

Prompt: Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion

+ RAG Architecture Diagram - AWS architecture diagram
-

Prompt: Generate a AWS architecture diagram with **AWS icons**. In this diagram, users connect to a frontend hosted on an instance.

- AWS Architecture Diagram + Authentication using React and AWS
+

Prompt: Generate authentication process using React with **AWS**. Use Serverless architecture.

+ Authentication Architecture Diagram - Azure architecture diagram
-

Prompt: Generate a Azure architecture diagram with **Azure icons**. In this diagram, users connect to a frontend hosted on an instance.

- Azure Architecture Diagram + Open Innovation
+

Prompt: Create visualization of Henry Chesbrough's Open Innovation model.

+ Open Innovation Diagram - Cat sketch prompt
+ Cat sketch

Prompt: Draw a cute cat for me.

Cat Drawing diff --git a/app/[lang]/about/cn/page.tsx b/app/[lang]/about/cn/page.tsx index 4dcac80e..bacd18e9 100644 --- a/app/[lang]/about/cn/page.tsx +++ b/app/[lang]/about/cn/page.tsx @@ -175,92 +175,106 @@ export default function AboutCN() {

- {/* Animated Transformer */} + {/* ResNet50 Architecture */}

- 动画Transformer连接器 + ResNet50模型架构动画

- 提示词: 给我一个带有 - 动画连接器的Transformer架构图。 + Prompt: Give me an{" "} + animated architecture diagram + of the ResNet50 model.

- 带动画连接器的Transformer架构 +
+ ResNet50模型架构图 +
- {/* Cloud Architecture Grid */} + {/* Diagram Grid */}

- GCP架构图 + RAG技术图

- 提示词: 使用 - GCP图标 - 生成一个GCP架构图。用户连接到托管在实例上的前端。 + Prompt: Generate a RAG + architecture diagram for{" "} + chat application. Use + connected diagram for data ingestion

- GCP架构图 +
+ RAG架构图 +

- AWS架构图 + React和AWS认证流程

- 提示词: 使用 - AWS图标 - 生成一个AWS架构图。用户连接到托管在实例上的前端。 + Prompt: Generate + authentication process using React with{" "} + AWS. Use Serverless + architecture.

- AWS架构图 +
+ 认证架构图 +

- Azure架构图 + 敏捷Scrum流程

- 提示词: 使用 - Azure图标 - 生成一个Azure架构图。用户连接到托管在实例上的前端。 + Prompt: Generate agile + scrum workflow diagram for software + development team.

- Azure架构图 +
+ 敏捷Scrum流程图 +

- 猫咪素描 + 开放式创新

- 提示词:{" "} - 给我画一只可爱的猫。 + Prompt: Create + visualization of Henry Chesbrough's + Open Innovation model.

- 猫咪绘图 +
+ 开放式创新图 +
diff --git a/app/[lang]/about/ja/page.tsx b/app/[lang]/about/ja/page.tsx index 8b4a79c2..22f53fc0 100644 --- a/app/[lang]/about/ja/page.tsx +++ b/app/[lang]/about/ja/page.tsx @@ -168,93 +168,106 @@ export default function AboutJA() {

- {/* Animated Transformer */} + {/* ResNet50 Architecture */}

- アニメーションTransformerコネクタ + ResNet50モデルアーキテクチャアニメーション

- プロンプト:{" "} - アニメーションコネクタ - 付きのTransformerアーキテクチャ図を作成してください。 + Prompt: Give me an{" "} + animated architecture diagram + of the ResNet50 model.

- アニメーションコネクタ付きTransformerアーキテクチャ +
+ ResNet50モデルアーキテクチャ図 +
- {/* Cloud Architecture Grid */} + {/* Diagram Grid */}

- GCPアーキテクチャ図 + RAG技術ダイアグラム

- プロンプト:{" "} - GCPアイコン - を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。 + Prompt: Generate a RAG + architecture diagram for{" "} + chat application. Use + connected diagram for data ingestion

- GCPアーキテクチャ図 +
+ RAGアーキテクチャ図 +

- AWSアーキテクチャ図 + ReactとAWSによる認証

- プロンプト:{" "} - AWSアイコン - を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。 + Prompt: Generate + authentication process using React with{" "} + AWS. Use Serverless + architecture.

- AWSアーキテクチャ図 +
+ 認証アーキテクチャ図 +

- Azureアーキテクチャ図 + アジャイルスクラムプロセス

- プロンプト:{" "} - Azureアイコン - を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。 + Prompt: Generate agile + scrum workflow diagram for software + development team.

- Azureアーキテクチャ図 +
+ アジャイルスクラム図 +

- 猫のスケッチ + オープンイノベーション

- プロンプト:{" "} - かわいい猫を描いてください。 + Prompt: Create + visualization of Henry Chesbrough's + Open Innovation model.

- 猫の絵 +
+ オープンイノベーション図 +
diff --git a/app/[lang]/about/page.tsx b/app/[lang]/about/page.tsx index d2e1bf00..85a46ef5 100644 --- a/app/[lang]/about/page.tsx +++ b/app/[lang]/about/page.tsx @@ -182,96 +182,106 @@ export default function About() {

- {/* Animated Transformer */} + {/* ResNet50 Architecture */}

- Animated Transformer Connectors + Animated ResNet50 Model Architecture

Prompt: Give me an{" "} - animated connector diagram of - transformer's architecture. + animated architecture diagram + of the ResNet50 model.

- Transformer Architecture with Animated Connectors +
+ Architecture diagram for ResNet50 model +
- {/* Cloud Architecture Grid */} + {/* Diagram Grid */}

- GCP Architecture Diagram + RAG Technique Diagram

- Prompt: Generate a GCP - architecture diagram with{" "} - GCP icons. Users connect to - a frontend hosted on an instance. + Prompt: Generate a RAG + architecture diagram for{" "} + chat application. Use + connected diagram for data ingestion

- GCP Architecture Diagram +
+ RAG Architecture Diagram +

- AWS Architecture Diagram + Authentication using React and AWS

- Prompt: Generate an AWS - architecture diagram with{" "} - AWS icons. Users connect to - a frontend hosted on an instance. + Prompt: Generate + authentication process using React with{" "} + AWS. Use Serverless + architecture.

- AWS Architecture Diagram +
+ Authentication Architecture Diagram +

- Azure Architecture Diagram + Agile Scrum Process

- Prompt: Generate an Azure - architecture diagram with{" "} - Azure icons. Users connect - to a frontend hosted on an instance. + Prompt: Generate agile + scrum workflow diagram for software + development team.

- Azure Architecture Diagram +
+ Agile Scrum Diagram +

- Cat Sketch + Open Innovation

- Prompt: Draw a cute cat for - me. + Prompt: Create + visualization of Henry Chesbrough's + Open Innovation model.

- Cat Drawing +
+ Open Innovation Diagram +
diff --git a/docs/cn/README_CN.md b/docs/cn/README_CN.md index 028af1c5..969208ee 100644 --- a/docs/cn/README_CN.md +++ b/docs/cn/README_CN.md @@ -56,31 +56,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 动画Transformer连接器
-

提示词: 给我一个带有**动画连接器**的Transformer架构图。

+

Prompt: Give me a **animated connector** diagram of transformer's architecture.

带动画连接器的Transformer架构 - GCP架构图
-

提示词: 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。

- GCP架构图 + RAG技术图
+

Prompt: Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion

+ RAG架构图 - AWS架构图
-

提示词: 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。

- AWS架构图 + React和AWS认证流程
+

Prompt: Generate authentication process using React with **AWS**. Use Serverless architecture.

+ 认证架构图 - Azure架构图
-

提示词: 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。

- Azure架构图 + 开放式创新
+

Prompt: Create visualization of Henry Chesbrough's Open Innovation model.

+ 开放式创新图 猫咪素描
-

提示词: 给我画一只可爱的猫。

+

Prompt: Draw a cute cat for me.

猫咪绘图 diff --git a/docs/ja/README_JA.md b/docs/ja/README_JA.md index 50b35b6b..a8720cf4 100644 --- a/docs/ja/README_JA.md +++ b/docs/ja/README_JA.md @@ -54,31 +54,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 アニメーションTransformerコネクタ
-

プロンプト: **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。

+

Prompt: Give me a **animated connector** diagram of transformer's architecture.

アニメーションコネクタ付きTransformerアーキテクチャ - GCPアーキテクチャ図
-

プロンプト: **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。

- GCPアーキテクチャ図 + RAG技術ダイアグラム
+

Prompt: Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion

+ RAGアーキテクチャ図 - AWSアーキテクチャ図
-

プロンプト: **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。

- AWSアーキテクチャ図 + ReactとAWSによる認証
+

Prompt: Generate authentication process using React with **AWS**. Use Serverless architecture.

+ 認証アーキテクチャ図 - Azureアーキテクチャ図
-

プロンプト: **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。

- Azureアーキテクチャ図 + オープンイノベーション
+

Prompt: Create visualization of Henry Chesbrough's Open Innovation model.

+ オープンイノベーション図 猫のスケッチ
-

プロンプト: かわいい猫を描いてください。

+

Prompt: Draw a cute cat for me.

猫の絵 diff --git a/public/agile_scrum.svg b/public/agile_scrum.svg new file mode 100644 index 00000000..b2761128 --- /dev/null +++ b/public/agile_scrum.svg @@ -0,0 +1 @@ +
Product Backlog
Product Backlog
Backlog Refinement
Backlog Refinement
Sprint Planning
Sprint Planning
Sprint Backlog
Sprint Backlog
Sprint Execution (1-4 Weeks)
Sprint Execution (1-4 Weeks)
Daily Standup
Daily Standup
Development
Development
Testing / QA
Testing / QA
Bug?
Bug?
Done
Done
Sprint Review
Sprint Review
Sprint Retrospective
Sprint Retrospective
Prioritized
Prioritized
Selected
Selected
Start Work
Start Work
Yes (Fix)
Yes (Fix)
No
No
Sync (24h)
Sync (24h)
Next Cycle
Next Cycle
Text is not SVG - cannot display
\ No newline at end of file diff --git a/public/auth.svg b/public/auth.svg new file mode 100644 index 00000000..28073380 --- /dev/null +++ b/public/auth.svg @@ -0,0 +1 @@ +
AWS Cloud
AWS Cloud
Amazon Cognito
Amazon C...
Amazon API Gateway
Amazon A...
AWS Lambda
AWS Lamb...
Amazon DynamoDB
Amazon D...
Return Data (JSON)
Return Data (JSON)
User
User
React Client
React Cl...
Stripe API
Stripe API
Status?
Status?
Initiates Login
Initiates Login
Authenticates
Authenticates
Authenticated Request
(JWT)
Authenticated Request...
Validate JWT
Validate JWT
Forwards Request
Forwards Request
Query Subscription
Query Subscription
Active
Active
Fetch User Data
Fetch User Data
Inactive
Inactive
401 Unauthorized
401 Unauthorized
402 Payment Required
402 Payment Required
Text is not SVG - cannot display
\ No newline at end of file diff --git a/public/inno.svg b/public/inno.svg new file mode 100644 index 00000000..bd179fd3 --- /dev/null +++ b/public/inno.svg @@ -0,0 +1 @@ +
OUTER RING
(External Environment)
OUTER RING...
INNER CORE
(The Firm / Internal R&D)
INNER CORE...
Internal R&D
Activities
Internal R&D...
IP Management
(Creation & Transfer)
IP Management...
Universities &
Research Inst.
Universities &...
Startups &
Inventors
Startups &...
Competitors
(Co-opetition)
Competitors...
Current Market
(Customers)
Current Market...
New Markets
(Spin-offs)
New Markets...
External Tech Acquisition
External T...
Licensing In
Licensing...
Joint Ventures
Joint Vent...
Product Launch
Product La...
Divest / Spin-off
Divest / S...
Market Feedback
Market Fee...
Absorptive Capacity
Absorptive Capacity
Strategic Alignment
Strategic Alignment
Licensing Out
Licensing...
Risk!
Risk!
Permeable
Boundary
Permeable...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/public/rag_prod.svg b/public/rag_prod.svg new file mode 100644 index 00000000..bda0871d --- /dev/null +++ b/public/rag_prod.svg @@ -0,0 +1 @@ +
DATA INGESTION PIPELINE (Offline/Async)
DATA INGESTION PIPELINE (Offline/Async)
Data Sources
(PDFs, SQL, APIs)
Data Sources...
Data Loader /
Crawler
Data Loader /...
Cleaner /
Preprocessor
Cleaner /...
Chunker
(500 tokens)
Chunker...
Embedding Model
(e.g., Ada-002)
Embedding Model...
User Interface
(Chat Client)
User Interface...
Chat Backend
(Orchestrator)
Chat Backend...
Query Embedding
Model
Query Embedding...
Vector Database
(Pinecone/Weaviate)
Vector Database...
Prompt Engineering
(Context + History)
Prompt Engineering...
LLM
(GPT-4 / Claude)
LLM...
Post-Processing
(Safety/Format)
Post-Processing...
1. Query
1. Query
2. Vector Search
2. Vector Search
3. Retrieve Context (k=5)
3. Retrieve Context (k=5)
History
History
4. Generated Prompt
4. Generated Prompt
5. Response
5. Response
6. Final Answer
6. Final Answer
Upsert Vectors
Upsert Vectors
RETRIEVAL
RETRIEVAL
GENERATION
GENERATION
Hybrid Search
Hybrid Search
Text is not SVG - cannot display
\ No newline at end of file diff --git a/public/resnet50.svg b/public/resnet50.svg new file mode 100644 index 00000000..02bbed25 --- /dev/null +++ b/public/resnet50.svg @@ -0,0 +1 @@ +
Input Image
224x224x3
Input Image...
Initial Conv
7x7, 64, stride 2
Output: 112x112
Initial Conv...
Max Pooling
3x3, stride 2
Output: 56x56
Max Pooling...
Stage 1 (conv2_x)
Output: 56x56, 256 filters
Stage 1 (conv2_x)...
Bottleneck Block x3
[1x1, 64]
[3x3, 64]
[1x1, 256]
Bottleneck Block x3...
Stage 2 (conv3_x)
Output: 28x28, 512 filters
Stage 2 (conv3_x)...
Bottleneck Block x4
[1x1, 128]
[3x3, 128]
[1x1, 512]
*First block stride 2
Bottleneck Block x4...
Stage 3 (conv4_x)
Output: 14x14, 1024 filters
Stage 3 (conv4_x)...
Bottleneck Block x6
[1x1, 256]
[3x3, 256]
[1x1, 1024]
*First block stride 2
Bottleneck Block x6...
Stage 4 (conv5_x)
Output: 7x7, 2048 filters
Stage 4 (conv5_x)...
Bottleneck Block x3
[1x1, 512]
[3x3, 512]
[1x1, 2048]
*First block stride 2
Bottleneck Block x3...
Global Avg Pool
1x1, 2048
Global Avg Pool...
FC / Dense
1000 units
(Softmax)
FC / Dense...
Detailed Bottleneck Architecture
Detailed Bottleneck Architecture
Block Input
Block Input
1x1 Conv
BN + ReLU
(Reduce Dim)
1x1 Conv...
3x3 Conv
BN + ReLU
3x3 Conv...
1x1 Conv
BN
(Restore Dim)
1x1 Conv...
Shortcut Connection
(Identity or Projection)
Shortcut Connection...
ReLU
ReLU

ResNet50 Architecture

ResNet50 Architecture
  • Bottleneck Design: Uses 1x1 convs to reduce/restore dimensions, making deep networks efficient.
  • Downsampling: Occurs at the first block of Stages 2, 3, and 4 (stride=2).
  • Shortcuts: Projection shortcuts (1x1 conv) used when dimensions change; Identity otherwise.
Bottleneck Design: Uses 1x1 convs to reduce/restore dimensi...
Detailed View
Detailed View
Output
Output
Text is not SVG - cannot display
\ No newline at end of file From 0cd1260172a27a59fb4a88af2cd7c38c78068cf8 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:33:56 +0900 Subject: [PATCH 09/63] fix: show all enabled panels on chat lobby instead of only templates (#802) --- components/chat/ChatLobby.tsx | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/components/chat/ChatLobby.tsx b/components/chat/ChatLobby.tsx index f67281d0..28f98ec8 100644 --- a/components/chat/ChatLobby.tsx +++ b/components/chat/ChatLobby.tsx @@ -107,8 +107,8 @@ export function ChatLobby({ currentInput = "", dict, }: ChatLobbyProps) { - const [templatesExpanded, setTemplatesExpanded] = useState(false) - const [examplesExpanded, setExamplesExpanded] = useState(false) + const [templatesExpanded, setTemplatesExpanded] = useState(true) + const [examplesExpanded, setExamplesExpanded] = useState(true) const [panelVisibility, setPanelVisibility] = useState(getPanelVisibility) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [sessionToDelete, setSessionToDelete] = useState(null) @@ -125,19 +125,25 @@ export function ChatLobby({ const hasHistory = sessions.length > 0 if (!hasHistory) { - if (panelVisibility.myTemplates) { - return ( - - ) + if (!panelVisibility.myTemplates && !panelVisibility.quickExamples) { + return null } - if (panelVisibility.quickExamples) { - return - } - return null + return ( +
+ {panelVisibility.myTemplates && ( + + )} + {panelVisibility.quickExamples && ( +
+ +
+ )} +
+ ) } // Show history + collapsible examples when there are sessions From c77af86011779593c8d03b72df9774852a73b25a Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:18:24 +0900 Subject: [PATCH 10/63] fix: improve create_new_diagram tool description to prevent misuse and bump to v0.2.0 (#803) --- packages/mcp-server/package-lock.json | 4 ++-- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/index.ts | 19 +++++++++++-------- packages/mcp-server/tsconfig.json | 3 ++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json index e6afbe5e..40737cec 100644 --- a/packages/mcp-server/package-lock.json +++ b/packages/mcp-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.17", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.17", + "version": "0.2.0", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 0026b2bd..706b4587 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@next-ai-drawio/mcp-server", - "version": "0.1.19", + "version": "0.2.0", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "type": "module", "main": "dist/index.js", diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 8ed0565a..563c4b87 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -155,19 +155,22 @@ server.registerTool( server.registerTool( "create_new_diagram", { - description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely. + description: `Create a NEW diagram from mxGraphModel XML. ONLY use this when creating a diagram from scratch. + +⚠️ DO NOT use this tool to modify an existing diagram — it will DESTROY all existing content and user changes. Use edit_diagram instead for ANY modifications to an existing diagram. CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml. When to use this tool: -- Creating a new diagram from scratch -- Replacing the current diagram with a completely different one -- Major structural changes that require regenerating the diagram +- Creating a new diagram from scratch (no existing diagram) +- The user explicitly asks to "start over" or "create a new diagram" -When to use edit_diagram instead: -- Small modifications to existing diagram -- Adding/removing individual elements -- Changing labels, colors, or positions +When to use edit_diagram instead (ALWAYS prefer edit_diagram if a diagram already exists): +- ANY modifications to an existing diagram +- Adding/removing/moving elements +- Changing labels, colors, styles, or positions +- Restructuring or reorganizing existing content +- Adding new elements to an existing diagram XML FORMAT - Full mxGraphModel structure: diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index 386ed98a..d3d60a3c 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -12,7 +12,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] From eadc2c2629ec2b816f232eb4e0f24c407b3509fc Mon Sep 17 00:00:00 2001 From: Zhichang Yu Date: Mon, 13 Apr 2026 11:18:50 +0800 Subject: [PATCH 11/63] feat: add rpm target to electron-builder for Linux distribution (#806) Added rpm target (x64 + arm64) alongside existing deb and AppImage targets. No CI changes required since electron-builder handles both deb and rpm generation under the --linux flag. Co-authored-by: Claude Opus 4.6 --- electron/electron-builder.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electron/electron-builder.yml b/electron/electron-builder.yml index 6c78889a..49d076d8 100644 --- a/electron/electron-builder.yml +++ b/electron/electron-builder.yml @@ -95,6 +95,10 @@ linux: arch: - x64 - arm64 + - target: rpm + arch: + - x64 + - arm64 # Publish configuration (optional) publish: From 171174378c1550dbb852232f5eee5d99469a47ed Mon Sep 17 00:00:00 2001 From: Octopus Date: Mon, 13 Apr 2026 19:05:57 +0800 Subject: [PATCH 12/63] fix: allow QvQ (Qwen Visual QA) models to use image input (#808) QvQ models (e.g. qvq-72b-preview, qvq-max) are visual reasoning models from the Qwen family that support image input. When accessed via providers that prefix model names with 'qwen/' (e.g., OpenRouter), these models contain 'qwen' in their ID but lack the 'vl' or 'vision' indicator. This caused supportsImageInput() to incorrectly return false for model IDs like 'qwen/qvq-72b-preview', blocking image uploads for vision-capable models. Add 'qvq' as an explicit exception in the Qwen text-model check so that QvQ models are correctly allowed to receive image input regardless of the provider prefix. Co-authored-by: octo-patch --- lib/ai-providers.ts | 4 +++- tests/unit/ai-providers.test.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 38ce84bc..5c0aa7e3 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -1357,10 +1357,12 @@ export function supportsImageInput(modelId: string): boolean { // Qwen text models (not vision variants like qwen-vl) // Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input + // QvQ (Qwen Visual QA) models are vision models — exclude them even when prefixed with "qwen/" if ( lowerModelId.includes("qwen") && !hasVisionIndicator && - !lowerModelId.includes("qwen3.5") + !lowerModelId.includes("qwen3.5") && + !lowerModelId.includes("qvq") ) { return false } diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts index 5b74bab4..92b3320a 100644 --- a/tests/unit/ai-providers.test.ts +++ b/tests/unit/ai-providers.test.ts @@ -208,6 +208,13 @@ describe("supportsImageInput", () => { expect(supportsImageInput("qwen3-vl-flash")).toBe(true) }) + it("returns true for QvQ (Qwen Visual QA) models including OpenRouter-prefixed names", () => { + expect(supportsImageInput("qvq-72b-preview")).toBe(true) + expect(supportsImageInput("qvq-max")).toBe(true) + expect(supportsImageInput("qwen/qvq-72b-preview")).toBe(true) + expect(supportsImageInput("qwen/qvq-max")).toBe(true) + }) + it("returns false for GLM text models", () => { expect(supportsImageInput("glm-4")).toBe(false) expect(supportsImageInput("glm-4-plus")).toBe(false) From d4454beb9ace3ea63139653dc6d2ff2e40d68682 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:57:40 +0900 Subject: [PATCH 13/63] chore: bump version to 0.4.15 (#810) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 965e11fb..c3c5468c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-ai-draw-io", - "version": "0.4.14", + "version": "0.4.15", "license": "Apache-2.0", "private": true, "main": "dist-electron/main/index.js", From a8d27088efc2cf84f0c4e3a6a799dc8c191b55ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:57:27 +0000 Subject: [PATCH 14/63] chore(deps): update dependency @xmldom/xmldom to v0.9.10 [security] (#821) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 556ed882..ba21c662 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "next-ai-draw-io", - "version": "0.4.14", + "version": "0.4.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "next-ai-draw-io", - "version": "0.4.14", + "version": "0.4.15", "license": "Apache-2.0", "dependencies": { "@ai-sdk/amazon-bedrock": "^4.0.1", @@ -50,8 +50,6 @@ "cmdk": "^1.1.1", "idb": "^8.0.3", "jsonrepair": "^3.13.1", - "lightningcss": "^1.32.0", - "lightningcss-linux-x64-gnu": "^1.32.0", "lucide-react": "^0.577.0", "motion": "^12.23.25", "nanoid": "^5.0.0", @@ -9675,9 +9673,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.9.tgz", - "integrity": "sha512-qycIHAucxy/LXAYIjmLmtQ8q9GPnMbnjG1KXhWm9o5sCr6pOYDATkMPiTNa6/v8eELyqOQ2FsEqeoFYmgv/gJg==", + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", "license": "MIT", "engines": { "node": ">=14.6" From 73eefc7aa6263f991a67d60ce0e27d7e15df02e7 Mon Sep 17 00:00:00 2001 From: LaaraibAhmed <90887574+LaaraibAhmed@users.noreply.github.com> Date: Thu, 7 May 2026 14:14:46 +0100 Subject: [PATCH 15/63] fix: CSS/UI issue in model-config-dialog (#818) Co-authored-by: Laaraib Ahmed --- components/model-config-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/model-config-dialog.tsx b/components/model-config-dialog.tsx index 5f82ef76..d3817613 100644 --- a/components/model-config-dialog.tsx +++ b/components/model-config-dialog.tsx @@ -405,7 +405,7 @@ export function ModelConfigDialog({
- +
{config.providers.length === 0 ? (
From f965f3fa2e8e0e05d2a80031a96acd1f11a76174 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Thu, 7 May 2026 21:43:37 +0800 Subject: [PATCH 16/63] chore: align biome schema version with CLI latest (#832) Bumps biome.json $schema from 2.4.4 to 2.4.14 so the repo schema matches the version the 'Auto Format' workflow installs via @biomejs/biome@latest. Also applies the one auto-fix the newer version produces (export ordering in electron/electron.d.ts). Fixes the spurious 'This PR has formatting issues' CI failure that was blocking fork PRs unrelated to formatting. --- biome.json | 2 +- electron/electron.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.json b/biome.json index 19644545..2c4409fa 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/electron/electron.d.ts b/electron/electron.d.ts index ae49580e..e268b100 100644 --- a/electron/electron.d.ts +++ b/electron/electron.d.ts @@ -101,8 +101,8 @@ declare global { } export type { - ConfigPreset, ApplyPresetResult, + ConfigPreset, ProxyConfig, SetProxyResult, SetUserLocaleResult, From 5c8ae4d6d7afdbe3a8c8c987e89200e424e6f66d Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 15 May 2026 09:52:51 +0800 Subject: [PATCH 17/63] fix: always re-fetch access code config when settings dialog opens (#816) When ACCESS_CODE_LIST is configured on the server, the settings dialog was not showing the access code input field in two cases: 1. Stale localStorage cache: if a user had previously visited without ACCESS_CODE_LIST enabled, the cached value of accessCodeRequired=false would be used indefinitely, hiding the password input. 2. Race condition on first visit: the dialog could open triggered by an auth error before the async fetch to /api/config completed, showing a blank settings dialog with no access code field. Fix by re-fetching /api/config whenever the dialog opens (on open change) instead of only once on mount with a cache guard. The cached value in localStorage is still updated on success, keeping the fast initial render intact while ensuring the dialog always reflects the server configuration. Fixes #811 Co-authored-by: octo-patch --- components/settings-dialog.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index c0656510..75064b1e 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -134,8 +134,11 @@ function SettingsContent({ const [isApplyingProxy, setIsApplyingProxy] = useState(false) useEffect(() => { - // Only fetch if not cached in localStorage - if (getStoredAccessCodeRequired() !== null) return + // Re-fetch config whenever the dialog opens to ensure we always show + // the access code input if the server requires it. This fixes the case + // where a stale localStorage cache (from before ACCESS_CODE_LIST was + // configured) would hide the access code input. + if (!open) return fetch(getApiEndpoint("/api/config")) .then((res) => { @@ -151,10 +154,9 @@ function SettingsContent({ setAccessCodeRequired(required) }) .catch(() => { - // Don't cache on error - allow retry on next mount - setAccessCodeRequired(false) + // Keep existing cached value on error }) - }, []) + }, [open]) // Detect current language from pathname useEffect(() => { From c60e3930a38e580d80bec0f436d0f4df97564eba Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 15 May 2026 13:02:26 +0800 Subject: [PATCH 18/63] fix: use createDeepSeek for kimi provider to handle reasoning_content in multi-turn conversations (fixes #824) (#825) Kimi thinking models (e.g. kimi-k2.6) return reasoning_content in their responses. The previous createOpenAI-based implementation silently ignored this field, so reasoning was never captured or replayed in subsequent turns. Switching to createDeepSeek (which natively understands reasoning_content) ensures that reasoning context is preserved across conversation turns, resolving the "cannot interact a second time" error with Kimi k2.6. This mirrors the existing doubao provider pattern, which already uses createDeepSeek for kimi-based models routed through Doubao. Co-authored-by: octo-patch --- lib/ai-providers.ts | 18 +++++++++- tests/unit/ai-providers.test.ts | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 5c0aa7e3..bc689e39 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -1262,7 +1262,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { case "glm": case "qwen": case "qiniu": - case "kimi": case "novita": { const envVar = PROVIDER_ENV_VARS[provider] if (!envVar) { @@ -1288,6 +1287,23 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { break } + case "kimi": { + const apiKey = resolveApiKey(overrides, "KIMI_API_KEY") + const baseURL = resolveBaseURL( + overrides?.apiKey, + overrides?.baseUrl, + resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"), + PROVIDER_INFO["kimi"]?.defaultBaseUrl, + ) + // Use createDeepSeek to properly handle reasoning_content for Kimi + // thinking models (e.g., kimi-k2.6). Kimi's API uses the same + // reasoning_content field as DeepSeek, so this provider correctly + // captures and replays reasoning in multi-turn conversations. + const customProvider = createDeepSeek({ apiKey, baseURL }) + model = customProvider(modelId) + break + } + default: throw new Error( `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`, diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts index 92b3320a..338a179d 100644 --- a/tests/unit/ai-providers.test.ts +++ b/tests/unit/ai-providers.test.ts @@ -245,6 +245,67 @@ vi.mock("ollama-ai-provider-v2", () => { return { createOllama: mockCreateOllama, ollama: mockOllama } }) +vi.mock("@ai-sdk/deepseek", () => { + const mockModel = { modelId: "test-model" } + const mockProviderFn = vi.fn(() => mockModel) + const mockCreateDeepSeek = vi.fn(() => mockProviderFn) + const mockDeepseek = vi.fn(() => mockModel) + return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek } +}) + +describe("Kimi provider uses createDeepSeek for reasoning_content support", () => { + let createDeepSeekMock: ReturnType + const savedEnv: Record = {} + + beforeEach(async () => { + savedEnv.KIMI_API_KEY = process.env.KIMI_API_KEY + savedEnv.KIMI_BASE_URL = process.env.KIMI_BASE_URL + delete process.env.KIMI_BASE_URL + + const mod = await import("@ai-sdk/deepseek") + createDeepSeekMock = mod.createDeepSeek as ReturnType + createDeepSeekMock.mockClear() + }) + + afterEach(() => { + process.env.KIMI_API_KEY = savedEnv.KIMI_API_KEY + process.env.KIMI_BASE_URL = savedEnv.KIMI_BASE_URL + }) + + it("uses createDeepSeek with Kimi default base URL for reasoning_content support", () => { + process.env.KIMI_API_KEY = "test-kimi-key" + + getAIModel({ + provider: "kimi", + apiKey: "test-kimi-key", + modelId: "moonshot-v1-8k", + }) + + expect(createDeepSeekMock).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.moonshot.cn/v1", + }), + ) + }) + + it("uses custom base URL when provided for kimi provider", () => { + process.env.KIMI_API_KEY = "test-kimi-key" + + getAIModel({ + provider: "kimi", + apiKey: "test-kimi-key", + baseUrl: "https://custom-kimi-endpoint.com/v1", + modelId: "kimi-k2.6", + }) + + expect(createDeepSeekMock).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://custom-kimi-endpoint.com/v1", + }), + ) + }) +}) + describe("Ollama API key security", () => { let createOllamaMock: ReturnType const savedEnv: Record = {} From 4e223b623757131b9371571ca43f14c227029665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=9C=E5=AD=90?= Date: Fri, 15 May 2026 22:14:20 +0800 Subject: [PATCH 19/63] feat: add all Draw.io themes to settings panel (#835) * feat: add all Draw.io themes to settings panel Add all available Draw.io themes (kennedy, atlas, dark, min, sketch, simple) to the settings panel dropdown. Previously only min and sketch were available as a toggle button. Changes: - Replace the Draw.io style toggle button with a dropdown selector - Expand theme type from "min" | "sketch" to include all 6 themes - Update localStorage validation to accept all themes - Update handler from toggle to direct theme selection Closes #499 * fix: localize theme labels, tighten DrawioTheme typing, sync dark param - Move DRAWIO_THEMES + DrawioTheme to lib/drawio-themes.ts; reuse in page.tsx, chat-panel.tsx and settings-dialog.tsx instead of `string` - Localize theme dropdown labels (Dark/Minimal/Sketch/Simple) in en/zh/ja/zh-Hant; keep proper-noun themes (Kennedy/Atlas) as-is - Drop trailing colon from drawioStyleDescription and remove dead switchTo/minimal/sketch keys in all 4 dictionaries - Auto-sync drawio dark URL param when ui="dark" is selected - Add aria-label to drawio-style SelectTrigger * fix: use kennedy as default theme and label it "Default" --------- Co-authored-by: dayuan.jiang --- app/[lang]/page.tsx | 17 +++++----- components/chat-panel.tsx | 9 ++--- components/settings-dialog.tsx | 54 ++++++++++++++++++++---------- lib/drawio-themes.ts | 17 ++++++++++ lib/i18n/dictionaries/en.json | 10 +++--- lib/i18n/dictionaries/ja.json | 10 +++--- lib/i18n/dictionaries/zh-Hant.json | 10 +++--- lib/i18n/dictionaries/zh.json | 10 +++--- 8 files changed, 91 insertions(+), 46 deletions(-) create mode 100644 lib/drawio-themes.ts diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index e44bd6ca..9d6acc8b 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -10,6 +10,7 @@ import { ResizablePanelGroup, } from "@/components/ui/resizable" import { useDiagram } from "@/contexts/diagram-context" +import { type DrawioTheme, isDrawioTheme } from "@/lib/drawio-themes" import { i18n, type Locale } from "@/lib/i18n/config" import { isIndexedDBUsable } from "@/lib/session-storage" @@ -27,7 +28,7 @@ export default function Home() { const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale const [isMobile, setIsMobile] = useState(false) const [isChatVisible, setIsChatVisible] = useState(true) - const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min") + const [drawioUi, setDrawioUi] = useState("kennedy") const [darkMode, setDarkMode] = useState(false) const [isLoaded, setIsLoaded] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false) @@ -56,7 +57,7 @@ export default function Home() { } const savedUi = localStorage.getItem("drawio-theme") - if (savedUi === "min" || savedUi === "sketch") { + if (isDrawioTheme(savedUi)) { setDrawioUi(savedUi) } @@ -113,10 +114,9 @@ export default function Home() { resetDrawioReady() } - const handleDrawioUiChange = () => { - const newUi = drawioUi === "min" ? "sketch" : "min" - localStorage.setItem("drawio-theme", newUi) - setDrawioUi(newUi) + const handleDrawioUiChange = (theme: DrawioTheme) => { + localStorage.setItem("drawio-theme", theme) + setDrawioUi(theme) setIsDrawioReady(false) resetDrawioReady() } @@ -216,7 +216,8 @@ export default function Home() { saveAndExit: false, noSaveBtn: true, noExitBtn: true, - dark: darkMode, + dark: + darkMode || drawioUi === "dark", lang: currentLang, // Enable offline mode in Electron to disable external service calls ...(isElectron && { @@ -264,7 +265,7 @@ export default function Home() { isVisible={isChatVisible} onToggleVisibility={toggleChatPanel} drawioUi={drawioUi} - onToggleDrawioUi={handleDrawioUiChange} + onDrawioUiChange={handleDrawioUiChange} darkMode={darkMode} onToggleDarkMode={handleDarkModeChange} isMobile={isMobile} diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index bd763e17..8d533ce4 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -32,6 +32,7 @@ import { useSessionManager } from "@/hooks/use-session-manager" import { useValidateDiagram } from "@/hooks/use-validate-diagram" import { getApiEndpoint } from "@/lib/base-path" import { findCachedResponse } from "@/lib/cached-responses" +import type { DrawioTheme } from "@/lib/drawio-themes" import { formatMessage } from "@/lib/i18n/utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { sanitizeMessages } from "@/lib/session-storage" @@ -68,8 +69,8 @@ interface ChatMessage { interface ChatPanelProps { isVisible: boolean onToggleVisibility: () => void - drawioUi: "min" | "sketch" - onToggleDrawioUi: () => void + drawioUi: DrawioTheme + onDrawioUiChange: (theme: DrawioTheme) => void darkMode: boolean onToggleDarkMode: () => void isMobile?: boolean @@ -110,7 +111,7 @@ export default function ChatPanel({ isVisible, onToggleVisibility, drawioUi, - onToggleDrawioUi, + onDrawioUiChange, darkMode, onToggleDarkMode, isMobile = false, @@ -1442,7 +1443,7 @@ export default function ChatPanel({ open={showSettingsDialog} onOpenChange={setShowSettingsDialog} drawioUi={drawioUi} - onToggleDrawioUi={onToggleDrawioUi} + onDrawioUiChange={onDrawioUiChange} darkMode={darkMode} onToggleDarkMode={onToggleDarkMode} minimalStyle={minimalStyle} diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 75064b1e..d3dccc44 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -25,6 +25,7 @@ import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { useDictionary } from "@/hooks/use-dictionary" import { getApiEndpoint } from "@/lib/base-path" +import type { DrawioTheme } from "@/lib/drawio-themes" import { i18n, type Locale } from "@/lib/i18n/config" import { STORAGE_KEYS } from "@/lib/storage" @@ -63,8 +64,8 @@ const LANGUAGE_LABELS: Record = { interface SettingsDialogProps { open: boolean onOpenChange: (open: boolean) => void - drawioUi: "min" | "sketch" - onToggleDrawioUi: () => void + drawioUi: DrawioTheme + onDrawioUiChange: (theme: DrawioTheme) => void darkMode: boolean onToggleDarkMode: () => void minimalStyle?: boolean @@ -90,7 +91,7 @@ function SettingsContent({ open, onOpenChange, drawioUi, - onToggleDrawioUi, + onDrawioUiChange, darkMode, onToggleDarkMode, minimalStyle = false, @@ -432,23 +433,40 @@ function SettingsContent({ {/* Draw.io Style */} - + + + + + + {dict.settings.themeDefault} + + Atlas + + {dict.settings.themeDark} + + + {dict.settings.themeMinimal} + + + {dict.settings.themeSketch} + + + {dict.settings.themeSimple} + + + {/* Diagram Style */} diff --git a/lib/drawio-themes.ts b/lib/drawio-themes.ts new file mode 100644 index 00000000..7eac1a65 --- /dev/null +++ b/lib/drawio-themes.ts @@ -0,0 +1,17 @@ +export const DRAWIO_THEMES = [ + "kennedy", + "atlas", + "dark", + "min", + "sketch", + "simple", +] as const + +export type DrawioTheme = (typeof DRAWIO_THEMES)[number] + +export function isDrawioTheme(value: unknown): value is DrawioTheme { + return ( + typeof value === "string" && + (DRAWIO_THEMES as readonly string[]).includes(value) + ) +} diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 2e005025..607981af 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -102,10 +102,12 @@ "theme": "Theme", "themeDescription": "Dark/Light mode for interface and DrawIO canvas.", "drawioStyle": "DrawIO Style", - "drawioStyleDescription": "Canvas style:", - "switchTo": "Switch to", - "minimal": "Minimal", - "sketch": "Sketch", + "drawioStyleDescription": "Canvas style", + "themeDefault": "Default", + "themeDark": "Dark", + "themeMinimal": "Minimal", + "themeSketch": "Sketch", + "themeSimple": "Simple", "diagramStyle": "Diagram Style", "diagramStyleDescription": "Toggle between minimal and styled diagram output.", "sendShortcut": "Send Shortcut", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 189da7f7..7992b84c 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -102,10 +102,12 @@ "theme": "テーマ", "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。", "drawioStyle": "DrawIO スタイル", - "drawioStyleDescription": "キャンバススタイル:", - "switchTo": "切り替え", - "minimal": "ミニマル", - "sketch": "スケッチ", + "drawioStyleDescription": "キャンバススタイル", + "themeDefault": "デフォルト", + "themeDark": "ダーク", + "themeMinimal": "ミニマル", + "themeSketch": "スケッチ", + "themeSimple": "シンプル", "diagramStyle": "ダイアグラムスタイル", "diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。", "sendShortcut": "送信ショートカット", diff --git a/lib/i18n/dictionaries/zh-Hant.json b/lib/i18n/dictionaries/zh-Hant.json index 396459d1..8c3d9c3d 100644 --- a/lib/i18n/dictionaries/zh-Hant.json +++ b/lib/i18n/dictionaries/zh-Hant.json @@ -102,10 +102,12 @@ "theme": "主題", "themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。", "drawioStyle": "DrawIO 樣式", - "drawioStyleDescription": "畫布樣式:", - "switchTo": "切換到", - "minimal": "簡約", - "sketch": "草圖", + "drawioStyleDescription": "畫布樣式", + "themeDefault": "預設", + "themeDark": "深色", + "themeMinimal": "簡約", + "themeSketch": "草圖", + "themeSimple": "簡單", "diagramStyle": "圖表樣式", "diagramStyleDescription": "切換簡約與精緻圖表輸出模式。", "sendShortcut": "傳送快捷鍵", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 2f3c266d..0ee9f819 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -102,10 +102,12 @@ "theme": "主题", "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。", "drawioStyle": "DrawIO 样式", - "drawioStyleDescription": "画布样式:", - "switchTo": "切换到", - "minimal": "简约", - "sketch": "草图", + "drawioStyleDescription": "画布样式", + "themeDefault": "默认", + "themeDark": "深色", + "themeMinimal": "简约", + "themeSketch": "草图", + "themeSimple": "简单", "diagramStyle": "图表样式", "diagramStyleDescription": "切换简约与精致图表输出模式。", "sendShortcut": "发送快捷键", From bb65a8c07a58d35115d3c091ccfc3b97749a3517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=9C=E5=AD=90?= Date: Tue, 19 May 2026 08:52:02 +0800 Subject: [PATCH 20/63] fix(e2e): resolve iframe toolbar strict mode violation (#837) * fix(e2e): resolve strict mode violation in iframe test Use .first() with [title*="Diagram"] selector to avoid matching multiple elements. Fixes CI failure in E2E Tests job. * fix(e2e): use .first() to resolve strict mode violation * style: fix biome formatting in iframe test * style: fix biome formatting in iframe test * fix(e2e): use .or().first() to handle both text and title selectors * fix(e2e): increase timeout for draw.io toolbar visibility check * fix(e2e): filter visible elements to avoid selecting hidden toolbar --- tests/e2e/iframe-interaction.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e/iframe-interaction.spec.ts b/tests/e2e/iframe-interaction.spec.ts index 97ff24b6..6ac10a48 100644 --- a/tests/e2e/iframe-interaction.spec.ts +++ b/tests/e2e/iframe-interaction.spec.ts @@ -35,8 +35,10 @@ test.describe("Iframe Interaction", () => { await expect( frame .locator('text="Diagram"') - .or(frame.locator('[title*="Diagram"]')), - ).toBeVisible({ timeout: 10000 }) + .or(frame.locator('[title*="Diagram"]')) + .filter({ visible: true }) + .first(), + ).toBeVisible({ timeout: 30000 }) }) test("diagram XML is rendered in iframe after generation", async ({ From 5406778dd68d481a67f99fd7db6ccc8b8f0ccfbd Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Wed, 20 May 2026 23:33:05 +0900 Subject: [PATCH 21/63] fix(electron): override draw.io iframe beforeunload to allow window close (fixes #815) (#840) The draw.io iframe registers a window.onbeforeunload handler that returns a non-empty string whenever its internal editor.modified flag is true. After the user edits text in a shape, that flag is set and never cleared. Per Electron BrowserWindow docs, returning a non-void value from any beforeunload handler in the page tree silently cancels the window close without showing a dialog. This is what caused the X button (and Cmd+Q) to do nothing for users who had typed in a shape. Calling event.preventDefault() in will-prevent-unload tells Electron to ignore the iframe's beforeunload return value and proceed with the close. The host app already persists diagrams via autosave + visibilitychange, so the prompt was unnecessary. Verified by reproducing the bug, applying the fix, and re-testing. --- electron/main/window-manager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/electron/main/window-manager.ts b/electron/main/window-manager.ts index 025f02c4..7b515715 100644 --- a/electron/main/window-manager.ts +++ b/electron/main/window-manager.ts @@ -60,6 +60,13 @@ export function createWindow(serverUrl: string): BrowserWindow { mainWindow.webContents.openDevTools() } + // Override the draw.io iframe's beforeunload handler so the window can + // close after the user edits text in a shape (fixes #815). Diagrams are + // already persisted via autosave, so the prompt is unnecessary. + mainWindow.webContents.on("will-prevent-unload", (event) => { + event.preventDefault() + }) + mainWindow.on("closed", () => { mainWindow = null }) From 2f2d75961d2c8e914d568c89a63cbaa08f4baf20 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Thu, 21 May 2026 08:37:14 +0900 Subject: [PATCH 22/63] chore: remove dead code from previous #815 fix attempts (#841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #840 fixed issue #815 in the Electron main process via will-prevent-unload + preventDefault. The renderer-side workarounds introduced by previous fix attempts (#642, #648) are no longer needed and never had effect for their stated purpose. Removed: - configuration={ confirmExit: false } in DrawIoEmbed confirmExit is not a recognized draw.io config key (zero matches in jgraph/drawio source). This was always dead code. - modified=0 / keepmodified=0 URL parameters Per drawio source (app.min.js:14898), these only suppress the post-save modified-flag clearing — they do not prevent edits from setting editor.modified=true. They were ineffective for blocking beforeunload prompts and actually prevented draw.io from clearing its modified flag after save. - canPersist / canPersistChecked state and isIndexedDBUsable() probe Their only purpose was gating the dead config above. Removing them also removes a startup delay before the iframe renders. - handleDrawioAutoSave wrapper After PR #780 stripped its body, it was a pure passthrough useCallback. Now passes handleDiagramAutoSave directly to onAutoSave. - withDB / isClosingError / resetDBPromise / onversionchange / onclose / terminated handlers in lib/session-storage.ts and lib/template-storage.ts PR #648 added these to recover from 'IDBDatabase: connection is closing' errors that PR #642's first land caused via db.close() on the shared singleton. That bug was already fixed in c5de1a1 (re-land of #642), three minutes before PR #648 commits started. The retry handlers defend against multi-tab / version-change scenarios that cannot occur in this single-instance Electron app (requestSingleInstanceLock). template-storage.ts copied the same pattern when introduced by #773. Verified: - npx tsc --noEmit passes - Manual test in dev mode: session save/load works, template create works, diagram-only persistence works. --- app/[lang]/page.tsx | 29 +------- lib/session-storage.ts | 140 ++++++++++------------------------- lib/template-storage.ts | 160 ++++++++++++++-------------------------- 3 files changed, 94 insertions(+), 235 deletions(-) diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index 9d6acc8b..b44c9fef 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -12,7 +12,6 @@ import { import { useDiagram } from "@/contexts/diagram-context" import { type DrawioTheme, isDrawioTheme } from "@/lib/drawio-themes" import { i18n, type Locale } from "@/lib/i18n/config" -import { isIndexedDBUsable } from "@/lib/session-storage" export default function Home() { const { @@ -33,8 +32,6 @@ export default function Home() { const [isLoaded, setIsLoaded] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false) const [isElectron, setIsElectron] = useState(false) - const [canPersist, setCanPersist] = useState(false) - const [canPersistChecked, setCanPersistChecked] = useState(false) const [drawioBaseUrl, setDrawioBaseUrl] = useState( process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net", ) @@ -85,11 +82,6 @@ export default function Home() { setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`) } - void (async () => { - const usable = await isIndexedDBUsable() - setCanPersist(usable) - setCanPersistChecked(true) - })() setIsLoaded(true) }, [pathname, router]) @@ -98,13 +90,6 @@ export default function Home() { onDrawioLoad() }, [onDrawioLoad]) - const handleDrawioAutoSave = useCallback( - (data: { xml?: string }) => { - handleDiagramAutoSave(data) - }, - [handleDiagramAutoSave], - ) - const handleDarkModeChange = () => { const newValue = !darkMode setDarkMode(newValue) @@ -187,7 +172,7 @@ export default function Home() { }`} >
- {isLoaded && canPersistChecked && ( + {isLoaded && (
@@ -195,24 +180,14 @@ export default function Home() { key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`} ref={drawioRef} autosave - onAutoSave={handleDrawioAutoSave} + onAutoSave={handleDiagramAutoSave} onExport={handleDiagramExport} onLoad={handleDrawioLoad} baseUrl={drawioBaseUrl} - configuration={ - canPersist - ? { confirmExit: false } - : undefined - } urlParameters={{ ui: drawioUi, spin: false, libraries: false, - // Disable modified tracking only when persistence is available - ...(canPersist && { - modified: false, - keepmodified: false, - }), saveAndExit: false, noSaveBtn: true, noExitBtn: true, diff --git a/lib/session-storage.ts b/lib/session-storage.ts index 4a51d668..8450375c 100644 --- a/lib/session-storage.ts +++ b/lib/session-storage.ts @@ -58,33 +58,6 @@ interface ChatSessionDB extends DBSchema { // Database singleton let dbPromise: Promise> | null = null -const resetDBPromise = () => { - dbPromise = null -} - -const isClosingError = (error: unknown): boolean => { - return ( - error instanceof DOMException && - error.name === "InvalidStateError" && - /closing/i.test(error.message) - ) -} - -const withDB = async ( - action: (db: IDBPDatabase) => Promise, -): Promise => { - try { - const db = await getDB() - return await action(db) - } catch (error) { - if (isClosingError(error)) { - resetDBPromise() - const db = await getDB() - return await action(db) - } - throw error - } -} async function getDB(): Promise> { if (!dbPromise) { @@ -115,23 +88,7 @@ async function getDB(): Promise> { } } }, - terminated() { - resetDBPromise() - }, }) - dbPromise - .then((db) => { - db.onversionchange = () => { - db.close() - resetDBPromise() - } - db.onclose = () => { - resetDBPromise() - } - }) - .catch(() => { - resetDBPromise() - }) } return dbPromise } @@ -146,46 +103,31 @@ export function isIndexedDBAvailable(): boolean { } } -// Check if IndexedDB is actually usable (not just present). -// Note: Do NOT close the db here - getDB() returns a shared singleton connection -// that other code depends on. -export async function isIndexedDBUsable(): Promise { - if (!isIndexedDBAvailable()) return false - try { - await getDB() - return true - } catch { - return false - } -} - // CRUD Operations export async function getAllSessionMetadata(): Promise { if (!isIndexedDBAvailable()) return [] try { - return await withDB(async (db) => { - const tx = db.transaction(STORE_NAME, "readonly") - const index = tx.store.index("by-updated") - const metadata: SessionMetadata[] = [] + const db = await getDB() + const tx = db.transaction(STORE_NAME, "readonly") + const index = tx.store.index("by-updated") + const metadata: SessionMetadata[] = [] - // Use cursor to read only metadata fields (avoids loading full messages/XML) - let cursor = await index.openCursor(null, "prev") // newest first - while (cursor) { - const s = cursor.value - metadata.push({ - id: s.id, - title: s.title, - createdAt: s.createdAt, - updatedAt: s.updatedAt, - messageCount: s.messages.length, - hasDiagram: - !!s.diagramXml && s.diagramXml.trim().length > 0, - thumbnailDataUrl: s.thumbnailDataUrl, - }) - cursor = await cursor.continue() - } - return metadata - }) + // Use cursor to read only metadata fields (avoids loading full messages/XML) + let cursor = await index.openCursor(null, "prev") // newest first + while (cursor) { + const s = cursor.value + metadata.push({ + id: s.id, + title: s.title, + createdAt: s.createdAt, + updatedAt: s.updatedAt, + messageCount: s.messages.length, + hasDiagram: !!s.diagramXml && s.diagramXml.trim().length > 0, + thumbnailDataUrl: s.thumbnailDataUrl, + }) + cursor = await cursor.continue() + } + return metadata } catch (error) { console.error("Failed to get session metadata:", error) return [] @@ -195,9 +137,8 @@ export async function getAllSessionMetadata(): Promise { export async function getSession(id: string): Promise { if (!isIndexedDBAvailable()) return null try { - return await withDB(async (db) => { - return (await db.get(STORE_NAME, id)) || null - }) + const db = await getDB() + return (await db.get(STORE_NAME, id)) || null } catch (error) { console.error("Failed to get session:", error) return null @@ -207,9 +148,8 @@ export async function getSession(id: string): Promise { export async function saveSession(session: ChatSession): Promise { if (!isIndexedDBAvailable()) return false try { - await withDB(async (db) => { - await db.put(STORE_NAME, session) - }) + const db = await getDB() + await db.put(STORE_NAME, session) return true } catch (error) { // Handle quota exceeded @@ -221,9 +161,8 @@ export async function saveSession(session: ChatSession): Promise { await deleteOldestSession() // Retry once try { - await withDB(async (db) => { - await db.put(STORE_NAME, session) - }) + const db = await getDB() + await db.put(STORE_NAME, session) return true } catch (retryError) { console.error( @@ -242,9 +181,8 @@ export async function saveSession(session: ChatSession): Promise { export async function deleteSession(id: string): Promise { if (!isIndexedDBAvailable()) return try { - await withDB(async (db) => { - await db.delete(STORE_NAME, id) - }) + const db = await getDB() + await db.delete(STORE_NAME, id) } catch (error) { console.error("Failed to delete session:", error) } @@ -253,9 +191,8 @@ export async function deleteSession(id: string): Promise { export async function getSessionCount(): Promise { if (!isIndexedDBAvailable()) return 0 try { - return await withDB(async (db) => { - return await db.count(STORE_NAME) - }) + const db = await getDB() + return await db.count(STORE_NAME) } catch (error) { console.error("Failed to get session count:", error) return 0 @@ -265,15 +202,14 @@ export async function getSessionCount(): Promise { export async function deleteOldestSession(): Promise { if (!isIndexedDBAvailable()) return try { - await withDB(async (db) => { - const tx = db.transaction(STORE_NAME, "readwrite") - const index = tx.store.index("by-updated") - const cursor = await index.openCursor() - if (cursor) { - await cursor.delete() - } - await tx.done - }) + const db = await getDB() + const tx = db.transaction(STORE_NAME, "readwrite") + const index = tx.store.index("by-updated") + const cursor = await index.openCursor() + if (cursor) { + await cursor.delete() + } + await tx.done } catch (error) { console.error("Failed to delete oldest session:", error) } diff --git a/lib/template-storage.ts b/lib/template-storage.ts index 65c533af..8b0e4516 100644 --- a/lib/template-storage.ts +++ b/lib/template-storage.ts @@ -57,33 +57,6 @@ export function generateDefaultTitle(prompt: string): string { // Database singleton let dbPromise: Promise> | null = null -const resetDBPromise = () => { - dbPromise = null -} - -const isClosingError = (error: unknown): boolean => { - return ( - error instanceof DOMException && - error.name === "InvalidStateError" && - /closing/i.test(error.message) - ) -} - -const withDB = async ( - action: (db: IDBPDatabase) => Promise, -): Promise => { - try { - const db = await getDB() - return await action(db) - } catch (error) { - if (isClosingError(error)) { - resetDBPromise() - const db = await getDB() - return await action(db) - } - throw error - } -} async function getDB(): Promise> { if (!dbPromise) { @@ -101,23 +74,7 @@ async function getDB(): Promise> { } } }, - terminated() { - resetDBPromise() - }, }) - dbPromise - .then((db) => { - db.onversionchange = () => { - db.close() - resetDBPromise() - } - db.onclose = () => { - resetDBPromise() - } - }) - .catch(() => { - resetDBPromise() - }) } return dbPromise } @@ -137,10 +94,9 @@ export function isIndexedDBAvailable(): boolean { export async function getAllTemplates(): Promise { if (!isIndexedDBAvailable()) return [] try { - return await withDB(async (db) => { - const templates = await db.getAll(STORE_NAME) - return sortTemplates(templates) - }) + const db = await getDB() + const templates = await db.getAll(STORE_NAME) + return sortTemplates(templates) } catch (error) { console.error("Failed to get templates:", error) return [] @@ -150,9 +106,8 @@ export async function getAllTemplates(): Promise { export async function getTemplate(id: string): Promise