Compare commits

..

1 Commits

Author SHA1 Message Date
dayuan.jiang
834abc2b2d fix: zoom reset on drag and IndexedDB version conflict
- Fix zoom resetting when dragging items (#775): removed useEffect that
  called load() on every autosave-triggered chartXML change, which reset
  the viewport. Moved diagram restore logic to onDrawioLoad where it
  only fires on remount.

- Fix IndexedDB VersionError: template-storage.ts shared the same DB
  name as session-storage.ts but at version 2, causing session storage
  to fail with "requested version (1) < existing version (2)". Give
  templates their own DB ("next-ai-drawio-templates").
2026-04-03 16:37:16 +09:00
17 changed files with 54 additions and 75 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: Download draw.io static files for offline use - name: Download draw.io static files for offline use
run: | run: |
rm -rf public/drawio rm -rf public/drawio
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/ cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF rm -rf public/drawio/WEB-INF
@@ -70,7 +70,7 @@ jobs:
shell: bash shell: bash
run: | run: |
rm -rf public/drawio rm -rf public/drawio
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/ cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF rm -rf public/drawio/WEB-INF

View File

@@ -31,7 +31,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Examples](#examples) - [Examples](#examples)
- [Features](#features) - [Features](#features)
- [MCP Server](#mcp-server) - [MCP Server (Preview)](#mcp-server-preview)
- [Claude Code CLI](#claude-code-cli) - [Claude Code CLI](#claude-code-cli)
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Try it Online](#try-it-online) - [Try it Online](#try-it-online)
@@ -99,7 +99,9 @@ Here are some example prompts and their generated diagrams:
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure) - **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 - **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
## MCP Server ## MCP Server (Preview)
> **Preview Feature**: This feature is experimental and may not be stable.
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol). Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).

View File

@@ -100,8 +100,12 @@ export default function Home() {
const handleDrawioAutoSave = useCallback( const handleDrawioAutoSave = useCallback(
(data: { xml?: string }) => { (data: { xml?: string }) => {
handleDiagramAutoSave(data) handleDiagramAutoSave(data)
// Only suppress modified state when persistence is available
if (canPersist) {
drawioRef.current?.status({ message: "", modified: false })
}
}, },
[handleDiagramAutoSave], [canPersist, drawioRef, handleDiagramAutoSave],
) )
const handleDarkModeChange = () => { const handleDarkModeChange = () => {

View File

@@ -141,6 +141,9 @@ export default function ExamplePanel({
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors"> <span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
{dict.examples.mcpServer} {dict.examples.mcpServer}
</span> </span>
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
{dict.examples.preview}
</span>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{dict.examples.mcpDescription} {dict.examples.mcpDescription}

View File

@@ -417,7 +417,6 @@ export function ChatMessageDisplay({
// Track previous message count to detect bulk loads vs streaming // Track previous message count to detect bulk loads vs streaming
const prevMessageCountRef = useRef(0) const prevMessageCountRef = useRef(0)
const scrollThrottleRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
if (messagesEndRef.current && messages.length > 0) { if (messagesEndRef.current && messages.length > 0) {
@@ -431,17 +430,8 @@ export function ChatMessageDisplay({
return return
} }
// Throttle scroll during streaming to avoid layout thrashing // Single message added - smooth scroll
// Leading + trailing: scroll immediately, then once more after cooldown messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
if (!scrollThrottleRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
scrollThrottleRef.current = setTimeout(() => {
scrollThrottleRef.current = null
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
})
}, 150)
}
} }
}, [messages]) }, [messages])

View File

@@ -195,22 +195,7 @@ export function ToolCallCard({
{input && isExpanded && ( {input && isExpanded && (
<div className="px-4 py-3 border-t border-border/40 bg-muted/20"> <div className="px-4 py-3 border-t border-border/40 bg-muted/20">
{typeof input === "object" && input.xml ? ( {typeof input === "object" && input.xml ? (
state === "input-streaming" || <CodeBlock code={input.xml} language="xml" />
state === "input-available" ? (
<pre
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all whitespace-pre-wrap"
style={{
fontFamily:
"var(--font-mono), ui-monospace, monospace",
margin: 0,
padding: 0,
}}
>
{input.xml}
</pre>
) : (
<CodeBlock code={input.xml} language="xml" />
)
) : typeof input === "object" && ) : typeof input === "object" &&
input.operations && input.operations &&
Array.isArray(input.operations) ? ( Array.isArray(input.operations) ? (

View File

@@ -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) - [Claude Code CLI](#claude-code-cli)
- [快速开始](#快速开始) - [快速开始](#快速开始)
- [在线试用](#在线试用) - [在线试用](#在线试用)
@@ -98,7 +98,9 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- **云架构图支持**专门支持生成云架构图AWS、GCP、Azure - **云架构图支持**专门支持生成云架构图AWS、GCP、Azure
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果 - **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
## MCP服务器 ## MCP服务器(预览)
> **预览功能**:此功能为实验性功能,可能不稳定。
通过MCP模型上下文协议在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。 通过MCP模型上下文协议在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。

View File

@@ -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) - [Claude Code CLI](#claude-code-cli)
- [はじめに](#はじめに) - [はじめに](#はじめに)
- [オンラインで試す](#オンラインで試す) - [オンラインで試す](#オンラインで試す)
@@ -96,7 +96,9 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- **クラウドアーキテクチャダイアグラムサポート**クラウドアーキテクチャダイアグラムの生成を専門的にサポートAWS、GCP、Azure - **クラウドアーキテクチャダイアグラムサポート**クラウドアーキテクチャダイアグラムの生成を専門的にサポートAWS、GCP、Azure
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成 - **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
## MCPサーバー ## MCPサーバー(プレビュー)
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
MCPModel Context Protocolを介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。 MCPModel Context Protocolを介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。

View File

@@ -1356,11 +1356,12 @@ export function supportsImageInput(modelId: string): boolean {
} }
// Qwen text models (not vision variants like qwen-vl) // Qwen text models (not vision variants like qwen-vl)
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input // qwen3.5-plus is a vision model
if ( if (
lowerModelId.includes("qwen") && lowerModelId.includes("qwen") &&
!hasVisionIndicator && !hasVisionIndicator &&
!lowerModelId.includes("qwen3.5") !lowerModelId.includes("qwen3.5-plus") &&
!lowerModelId.includes("qwen3.5-flash")
) { ) {
return false return false
} }

View File

@@ -76,7 +76,8 @@
"creativeDescription": "Draw something fun and creative", "creativeDescription": "Draw something fun and creative",
"cachedNote": "Examples are cached for instant response", "cachedNote": "Examples are cached for instant response",
"mcpServer": "MCP Server", "mcpServer": "MCP Server",
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor" "mcpDescription": "Use in Claude Desktop, VS Code & Cursor",
"preview": "PREVIEW"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",

View File

@@ -76,7 +76,8 @@
"creativeDescription": "楽しくてクリエイティブなものを描く", "creativeDescription": "楽しくてクリエイティブなものを描く",
"cachedNote": "例はキャッシュされ、即座に応答します", "cachedNote": "例はキャッシュされ、即座に応答します",
"mcpServer": "MCP サーバー", "mcpServer": "MCP サーバー",
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用" "mcpDescription": "Claude Desktop、VS Code、Cursor で使用",
"preview": "プレビュー"
}, },
"settings": { "settings": {
"title": "設定", "title": "設定",

View File

@@ -76,7 +76,8 @@
"creativeDescription": "繪製有趣且富有創意的內容", "creativeDescription": "繪製有趣且富有創意的內容",
"cachedNote": "範例已快取,可即時回應", "cachedNote": "範例已快取,可即時回應",
"mcpServer": "MCP 伺服器", "mcpServer": "MCP 伺服器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用" "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
"preview": "預覽"
}, },
"settings": { "settings": {
"title": "設定", "title": "設定",

View File

@@ -76,7 +76,8 @@
"creativeDescription": "绘制有趣且富有创意的内容", "creativeDescription": "绘制有趣且富有创意的内容",
"cachedNote": "示例已缓存,可即时响应", "cachedNote": "示例已缓存,可即时响应",
"mcpServer": "MCP 服务器", "mcpServer": "MCP 服务器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用" "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
"preview": "预览"
}, },
"settings": { "settings": {
"title": "设置", "title": "设置",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.1.19", "version": "0.1.17",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -4,29 +4,6 @@
*/ */
import http from "node:http" 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 { import {
addHistory, addHistory,
clearHistory, clearHistory,
@@ -178,7 +155,7 @@ export function startHttpServer(port = 6002): Promise<number> {
} }
}) })
server.listen(port, "127.0.0.1", () => { server.listen(port, () => {
serverPort = port serverPort = port
log.info(`HTTP server running on http://localhost:${port}`) log.info(`HTTP server running on http://localhost:${port}`)
resolve(port) resolve(port)
@@ -289,7 +266,11 @@ function handleStateApi(
}), }),
) )
} else if (req.method === "POST") { } else if (req.method === "POST") {
readBody(req, res, (body) => { let body = ""
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
try { try {
const data = JSON.parse(body) const data = JSON.parse(body)
const { sessionId } = data const { sessionId } = data
@@ -366,7 +347,11 @@ function handleRestoreApi(
return return
} }
readBody(req, res, (body) => { let body = ""
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
try { try {
const { sessionId, index } = JSON.parse(body) const { sessionId, index } = JSON.parse(body)
if (!sessionId || index === undefined) { if (!sessionId || index === undefined) {
@@ -408,7 +393,11 @@ function handleHistorySvgApi(
return return
} }
readBody(req, res, (body) => { let body = ""
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
try { try {
const { sessionId, svg } = JSON.parse(body) const { sessionId, svg } = JSON.parse(body)
if (!sessionId || !svg) { if (!sessionId || !svg) {

View File

@@ -200,8 +200,6 @@ describe("supportsImageInput", () => {
it("returns true for Qwen vision models", () => { it("returns true for Qwen vision models", () => {
expect(supportsImageInput("qwen-vl")).toBe(true) 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-plus")).toBe(true)
expect(supportsImageInput("qwen3.5-flash")).toBe(true) expect(supportsImageInput("qwen3.5-flash")).toBe(true)
expect(supportsImageInput("qwen3-vl-plus")).toBe(true) expect(supportsImageInput("qwen3-vl-plus")).toBe(true)

View File

@@ -35,7 +35,6 @@
"packages", "packages",
"electron", "electron",
"electron-standalone", "electron-standalone",
"dist-electron", "dist-electron"
"release"
] ]
} }