Compare commits

..

26 Commits

Author SHA1 Message Date
dayuan.jiang
9656fc0cfe fix(parse-url): block SSRF via private/internal URLs
/api/parse-url accepted any URL the user submitted, fetched it via
@extractus/article-extractor, and returned the body as Markdown. With
ALLOW_PRIVATE_URLS unset (the default after #600) the SSRF guard
short-circuited entirely, so an unauthenticated POST could probe
container ports, read AWS IMDS / GCP metadata, and reach same-VPC
internal services.

- parse-url now always rejects private URLs regardless of
  ALLOW_PRIVATE_URLS. The flag's only legitimate use case is local
  LLM provider baseUrl overrides (validate-model, chat); article
  extraction has no business fetching internal hosts. Local LLM
  setups (Ollama, LM Studio, etc.) are unaffected.
- Strip a trailing dot from the hostname before equality checks so
  the FQDN form "localhost." (which still resolves to 127.0.0.1) is
  caught by the existing string match.

Known follow-ups (not addressed here):
- DNS rebinding: hostnames are matched as strings; a public domain
  resolving to 127.0.0.1 (e.g. localtest.me) is not caught.
- HTTP redirects: @extractus/article-extractor uses cross-fetch with
  default redirect: "follow" and exposes no hook, so a public URL
  302-ing to an internal host still leaks.
2026-05-21 23:50:30 +09:00
Dayuan Jiang
1115b2d2cd chore: bump version to 0.4.16 (#843) 2026-05-21 09:28:55 +09:00
renovate[bot]
08afb6dd34 chore(deps): update dependency next to v16.2.6 [security] (#842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-21 09:05:06 +09:00
renovate[bot]
c703159e00 fix(deps): update core framework packages (major) (#724)
* fix(deps): update core framework packages

* style: auto-format with Biome

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-21 09:01:42 +09:00
renovate[bot]
ee75408136 chore(deps): update dependency electron to v39.8.5 [security] (#789)
* chore(deps): update dependency electron to v39.8.5 [security]

* style: auto-format with Biome

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-21 09:01:03 +09:00
renovate[bot]
2ed1a64ae3 fix(deps): update minor and patch dependencies (#829)
* fix(deps): update minor and patch dependencies

* style: auto-format with Biome

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-21 08:59:18 +09:00
Dayuan Jiang
2f2d75961d chore: remove dead code from previous #815 fix attempts (#841)
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.
2026-05-21 08:37:14 +09:00
Dayuan Jiang
5406778dd6 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.
2026-05-20 23:33:05 +09:00
果子
bb65a8c07a 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
2026-05-19 09:52:02 +09:00
果子
4e223b6237 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 <jdy.toh@gmail.com>
2026-05-15 23:14:20 +09:00
Octopus
c60e3930a3 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 <octo-patch@github.com>
2026-05-15 14:02:26 +09:00
Octopus
5c8ae4d6d7 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 <octo-patch@github.com>
2026-05-15 10:52:51 +09:00
Dayuan Jiang
f965f3fa2e 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.
2026-05-07 22:43:37 +09:00
LaaraibAhmed
73eefc7aa6 fix: CSS/UI issue in model-config-dialog (#818)
Co-authored-by: Laaraib Ahmed <laaraibahmed@Laaraibs-MacBook-Pro.local>
2026-05-07 22:14:46 +09:00
renovate[bot]
a8d27088ef chore(deps): update dependency @xmldom/xmldom to v0.9.10 [security] (#821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 02:57:27 +00:00
Dayuan Jiang
d4454beb9a chore: bump version to 0.4.15 (#810) 2026-04-14 23:57:40 +09:00
Octopus
171174378c 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 <octo-patch@github.com>
2026-04-13 20:05:57 +09:00
Zhichang Yu
eadc2c2629 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 <noreply@anthropic.com>
2026-04-13 12:18:50 +09:00
Dayuan Jiang
c77af86011 fix: improve create_new_diagram tool description to prevent misuse and bump to v0.2.0 (#803) 2026-04-10 17:18:24 +09:00
Dayuan Jiang
0cd1260172 fix: show all enabled panels on chat lobby instead of only templates (#802) 2026-04-10 14:33:56 +09:00
Khanh Thanh
49bfb51b10 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 <jdy.toh@gmail.com>
2026-04-10 13:11:34 +09:00
Dayuan Jiang
ccd9c1f48e fix: reduce CPU usage during large XML streaming (#801)
* fix: reduce CPU usage during large XML streaming

Skip Prism syntax highlighting while tool call is streaming — use plain
<pre> 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.
2026-04-10 11:12:50 +09:00
Octopus
622aa8683d fix: remove redundant status(modified:false) call to restore undo/redo (fixes #779) (#780) 2026-04-10 10:30:05 +09:00
Octopus
43ddb7a999 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 <octo-patch@github.com>
2026-04-10 10:23:39 +09:00
Dayuan Jiang
b9fdf9538c 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.
2026-04-06 10:17:38 +09:00
Dayuan Jiang
1f31692701 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
2026-04-06 10:14:50 +09:00
41 changed files with 2149 additions and 1535 deletions

View File

@@ -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)
@@ -63,24 +63,24 @@ Here are some example prompts and their generated diagrams:
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCP architecture diagram</strong><br />
<p><strong>Prompt:</strong> Generate a GCP architecture diagram with **GCP icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
<img src="./public/gcp_demo.svg" alt="GCP Architecture Diagram" width="480" />
<strong>RAG Technique Diagram</strong><br />
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
<img src="./public/rag_prod.svg" alt="RAG Architecture Diagram" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWS architecture diagram</strong><br />
<p><strong>Prompt:</strong> Generate a AWS architecture diagram with **AWS icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
<img src="./public/aws_demo.svg" alt="AWS Architecture Diagram" width="480" />
<strong>Authentication using React and AWS</strong><br />
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
<img src="./public/auth.svg" alt="Authentication Architecture Diagram" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azure architecture diagram</strong><br />
<p><strong>Prompt:</strong> Generate a Azure architecture diagram with **Azure icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
<img src="./public/azure_demo.svg" alt="Azure Architecture Diagram" width="480" />
<strong>Open Innovation</strong><br />
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
<img src="./public/inno.svg" alt="Open Innovation Diagram" width="480" />
</td>
<td width="50%" valign="top">
<strong>Cat sketch prompt</strong><br />
<strong>Cat sketch</strong><br />
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
<img src="./public/cat_demo.svg" alt="Cat Drawing" width="240" />
</td>
@@ -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).

View File

@@ -175,92 +175,106 @@ export default function AboutCN() {
</p>
<div className="space-y-8">
{/* Animated Transformer */}
{/* ResNet50 Architecture */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Transformer连接器
ResNet50模型架构动画
</h3>
<p className="text-gray-600 mb-4">
<strong></strong>
<strong></strong>Transformer架构图
<strong>Prompt:</strong> Give me an{" "}
<strong>animated</strong> architecture diagram
of the ResNet50 model.
</p>
<Image
src="/animated_connectors.svg"
alt="带动画连接器的Transformer架构"
width={480}
height={360}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
<Image
src="/resnet50.svg"
alt="ResNet50模型架构图"
width={480}
height={360}
className="mx-auto"
/>
</div>
</div>
{/* Cloud Architecture Grid */}
{/* Diagram Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
GCP架构
RAG技术
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使
<strong>GCP图标</strong>
GCP架构图
<strong>Prompt:</strong> Generate a RAG
architecture diagram for{" "}
<strong>chat application</strong>. Use
connected diagram for data ingestion
</p>
<Image
src="/gcp_demo.svg"
alt="GCP架构图"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/rag_prod.svg"
alt="RAG架构图"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
AWS架构图
React和AWS认证流程
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使
<strong>AWS图标</strong>
AWS架构图
<strong>Prompt:</strong> Generate
authentication process using React with{" "}
<strong>AWS</strong>. Use Serverless
architecture.
</p>
<Image
src="/aws_demo.svg"
alt="AWS架构图"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/auth.svg"
alt="认证架构图"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Azure架构图
Scrum流程
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使
<strong>Azure图标</strong>
Azure架构图
<strong>Prompt:</strong> Generate agile
scrum workflow diagram for software
development team.
</p>
<Image
src="/azure_demo.svg"
alt="Azure架构图"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/agile_scrum.svg"
alt="敏捷Scrum流程图"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>Prompt:</strong> Create
visualization of Henry Chesbrough&apos;s
Open Innovation model.
</p>
<Image
src="/cat_demo.svg"
alt="猫咪绘图"
width={240}
height={240}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/inno.svg"
alt="开放式创新图"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
</div>
</div>

View File

@@ -168,93 +168,106 @@ export default function AboutJA() {
</p>
<div className="space-y-8">
{/* Animated Transformer */}
{/* ResNet50 Architecture */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Transformerコネクタ
ResNet50モデルアーキテクチャアニメーション
</h3>
<p className="text-gray-600 mb-4">
<strong></strong>{" "}
<strong></strong>
Transformerアーキテクチャ図を作成してください
<strong>Prompt:</strong> Give me an{" "}
<strong>animated</strong> architecture diagram
of the ResNet50 model.
</p>
<Image
src="/animated_connectors.svg"
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
width={480}
height={360}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
<Image
src="/resnet50.svg"
alt="ResNet50モデルアーキテクチャ図"
width={480}
height={360}
className="mx-auto"
/>
</div>
</div>
{/* Cloud Architecture Grid */}
{/* Diagram Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
GCPアーキテクチャ図
RAG技術ダイアグラム
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>GCPアイコン</strong>
使GCPアーキテクチャ図を生成してください
<strong>Prompt:</strong> Generate a RAG
architecture diagram for{" "}
<strong>chat application</strong>. Use
connected diagram for data ingestion
</p>
<Image
src="/gcp_demo.svg"
alt="GCPアーキテクチャ図"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/rag_prod.svg"
alt="RAGアーキテクチャ図"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
AWSアーキテクチャ図
ReactとAWSによる認証
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>AWSアイコン</strong>
使AWSアーキテクチャ図を生成してください
<strong>Prompt:</strong> Generate
authentication process using React with{" "}
<strong>AWS</strong>. Use Serverless
architecture.
</p>
<Image
src="/aws_demo.svg"
alt="AWSアーキテクチャ図"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/auth.svg"
alt="認証アーキテクチャ図"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Azureアーキテクチャ図
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>Azureアイコン</strong>
使Azureアーキテクチャ図を生成してください
<strong>Prompt:</strong> Generate agile
scrum workflow diagram for software
development team.
</p>
<Image
src="/azure_demo.svg"
alt="Azureアーキテクチャ図"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/agile_scrum.svg"
alt="アジャイルスクラム図"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>Prompt:</strong> Create
visualization of Henry Chesbrough&apos;s
Open Innovation model.
</p>
<Image
src="/cat_demo.svg"
alt="猫の絵"
width={240}
height={240}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/inno.svg"
alt="オープンイノベーション図"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
</div>
</div>

View File

@@ -182,96 +182,106 @@ export default function About() {
</p>
<div className="space-y-8">
{/* Animated Transformer */}
{/* ResNet50 Architecture */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Animated Transformer Connectors
Animated ResNet50 Model Architecture
</h3>
<p className="text-gray-600 mb-4">
<strong>Prompt:</strong> Give me an{" "}
<strong>animated connector</strong> diagram of
transformer&apos;s architecture.
<strong>animated</strong> architecture diagram
of the ResNet50 model.
</p>
<Image
src="/animated_connectors.svg"
alt="Transformer Architecture with Animated Connectors"
width={480}
height={360}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
<Image
src="/resnet50.svg"
alt="Architecture diagram for ResNet50 model"
width={480}
height={360}
className="mx-auto"
/>
</div>
</div>
{/* Cloud Architecture Grid */}
{/* Diagram Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
GCP Architecture Diagram
RAG Technique Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate a GCP
architecture diagram with{" "}
<strong>GCP icons</strong>. Users connect to
a frontend hosted on an instance.
<strong>Prompt:</strong> Generate a RAG
architecture diagram for{" "}
<strong>chat application</strong>. Use
connected diagram for data ingestion
</p>
<Image
src="/gcp_demo.svg"
alt="GCP Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/rag_prod.svg"
alt="RAG Architecture Diagram"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
AWS Architecture Diagram
Authentication using React and AWS
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate an AWS
architecture diagram with{" "}
<strong>AWS icons</strong>. Users connect to
a frontend hosted on an instance.
<strong>Prompt:</strong> Generate
authentication process using React with{" "}
<strong>AWS</strong>. Use Serverless
architecture.
</p>
<Image
src="/aws_demo.svg"
alt="AWS Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/auth.svg"
alt="Authentication Architecture Diagram"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Azure Architecture Diagram
Agile Scrum Process
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate an Azure
architecture diagram with{" "}
<strong>Azure icons</strong>. Users connect
to a frontend hosted on an instance.
<strong>Prompt:</strong> Generate agile
scrum workflow diagram for software
development team.
</p>
<Image
src="/azure_demo.svg"
alt="Azure Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/agile_scrum.svg"
alt="Agile Scrum Diagram"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Cat Sketch
Open Innovation
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Draw a cute cat for
me.
<strong>Prompt:</strong> Create
visualization of Henry Chesbrough&apos;s
Open Innovation model.
</p>
<Image
src="/cat_demo.svg"
alt="Cat Drawing"
width={240}
height={240}
className="mx-auto"
/>
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
<Image
src="/inno.svg"
alt="Open Innovation Diagram"
width={480}
height={360}
className="max-w-full max-h-full object-contain"
/>
</div>
</div>
</div>
</div>

View File

@@ -10,8 +10,8 @@ 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"
export default function Home() {
const {
@@ -27,13 +27,11 @@ 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<DrawioTheme>("kennedy")
const [darkMode, setDarkMode] = useState(false)
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",
)
@@ -56,7 +54,7 @@ export default function Home() {
}
const savedUi = localStorage.getItem("drawio-theme")
if (savedUi === "min" || savedUi === "sketch") {
if (isDrawioTheme(savedUi)) {
setDrawioUi(savedUi)
}
@@ -84,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])
@@ -97,17 +90,6 @@ export default function Home() {
onDrawioLoad()
}, [onDrawioLoad])
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],
)
const handleDarkModeChange = () => {
const newValue = !darkMode
setDarkMode(newValue)
@@ -117,10 +99,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()
}
@@ -191,7 +172,7 @@ export default function Home() {
}`}
>
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
{isLoaded && canPersistChecked && (
{isLoaded && (
<div
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
>
@@ -199,28 +180,19 @@ 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,
dark: darkMode,
dark:
darkMode || drawioUi === "dark",
lang: currentLang,
// Enable offline mode in Electron to disable external service calls
...(isElectron && {
@@ -268,7 +240,7 @@ export default function Home() {
isVisible={isChatVisible}
onToggleVisibility={toggleChatPanel}
drawioUi={drawioUi}
onToggleDrawioUi={handleDrawioUiChange}
onDrawioUiChange={handleDrawioUiChange}
darkMode={darkMode}
onToggleDarkMode={handleDarkModeChange}
isMobile={isMobile}

View File

@@ -1,7 +1,7 @@
import { extract } from "@extractus/article-extractor"
import { NextResponse } from "next/server"
import TurndownService from "turndown"
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
import { isPrivateUrl } from "@/lib/ssrf-protection"
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
const EXTRACT_TIMEOUT_MS = 15000
@@ -28,8 +28,10 @@ export async function POST(req: Request) {
)
}
// SSRF protection
if (!allowPrivateUrls && isPrivateUrl(url)) {
// SSRF protection: parse-url has no use case for fetching internal
// hosts, so private URLs are always rejected. ALLOW_PRIVATE_URLS only
// governs LLM provider baseUrl overrides (validate-model, chat).
if (isPrivateUrl(url)) {
return NextResponse.json(
{ error: "Cannot access private/internal URLs" },
{ status: 400 },

View File

@@ -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",

View File

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

View File

@@ -417,6 +417,7 @@ export function ChatMessageDisplay({
// Track previous message count to detect bulk loads vs streaming
const prevMessageCountRef = useRef(0)
const scrollThrottleRef = useRef<ReturnType<typeof setTimeout> | 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])

View File

@@ -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}

View File

@@ -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<string | null>(null)
@@ -125,19 +125,25 @@ export function ChatLobby({
const hasHistory = sessions.length > 0
if (!hasHistory) {
if (panelVisibility.myTemplates) {
return (
<TemplatePanel
setInput={setInput}
onSendTemplate={onSendTemplate}
currentInput={currentInput}
/>
)
if (!panelVisibility.myTemplates && !panelVisibility.quickExamples) {
return null
}
if (panelVisibility.quickExamples) {
return <ExamplePanel setInput={setInput} setFiles={setFiles} />
}
return null
return (
<div className="animate-fade-in">
{panelVisibility.myTemplates && (
<TemplatePanel
setInput={setInput}
onSendTemplate={onSendTemplate}
currentInput={currentInput}
/>
)}
{panelVisibility.quickExamples && (
<div className={panelVisibility.myTemplates ? "mt-6" : ""}>
<ExamplePanel setInput={setInput} setFiles={setFiles} />
</div>
)}
</div>
)
}
// Show history + collapsible examples when there are sessions

View File

@@ -195,7 +195,22 @@ export function ToolCallCard({
{input && isExpanded && (
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
{typeof input === "object" && input.xml ? (
<CodeBlock code={input.xml} language="xml" />
state === "input-streaming" ||
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" &&
input.operations &&
Array.isArray(input.operations) ? (

View File

@@ -405,7 +405,7 @@ export function ModelConfigDialog({
</span>
</div>
<ScrollArea className="flex-1 px-2">
<ScrollArea className="flex-1 px-2 min-h-0">
<div className="space-y-1 pb-2">
{config.providers.length === 0 ? (
<div className="px-3 py-8 text-center">

View File

@@ -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<Locale, string> = {
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,
@@ -134,8 +135,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 +155,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(() => {
@@ -430,23 +433,40 @@ function SettingsContent({
{/* Draw.io Style */}
<SettingItem
label={dict.settings.drawioStyle}
description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
description={dict.settings.drawioStyleDescription}
>
<Button
id="drawio-ui"
variant="outline"
onClick={onToggleDrawioUi}
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
<Select
value={drawioUi}
onValueChange={(v) =>
onDrawioUiChange(v as DrawioTheme)
}
>
{dict.settings.switchTo}{" "}
{drawioUi === "min"
? dict.settings.sketch
: dict.settings.minimal}
</Button>
<SelectTrigger
id="drawio-ui-select"
aria-label={dict.settings.drawioStyle}
className="w-[120px] h-9 rounded-xl"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="kennedy">
{dict.settings.themeDefault}
</SelectItem>
<SelectItem value="atlas">Atlas</SelectItem>
<SelectItem value="dark">
{dict.settings.themeDark}
</SelectItem>
<SelectItem value="min">
{dict.settings.themeMinimal}
</SelectItem>
<SelectItem value="sketch">
{dict.settings.themeSketch}
</SelectItem>
<SelectItem value="simple">
{dict.settings.themeSimple}
</SelectItem>
</SelectContent>
</Select>
</SettingItem>
{/* Diagram Style */}

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)
- [快速开始](#快速开始)
- [在线试用](#在线试用)
@@ -56,31 +56,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
<tr>
<td colspan="2" valign="top" align="center">
<strong>动画Transformer连接器</strong><br />
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
<img src="../../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCP架构图</strong><br />
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/gcp_demo.svg" alt="GCP架构图" width="480" />
<strong>RAG技术图</strong><br />
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
<img src="../../public/rag_prod.svg" alt="RAG架构图" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWS架构图</strong><br />
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/aws_demo.svg" alt="AWS架构图" width="480" />
<strong>React和AWS认证流程</strong><br />
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
<img src="../../public/auth.svg" alt="认证架构图" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azure架构图</strong><br />
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/azure_demo.svg" alt="Azure架构图" width="480" />
<strong>开放式创新</strong><br />
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
<img src="../../public/inno.svg" alt="开放式创新图" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫咪素描</strong><br />
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
</td>
</tr>
@@ -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。

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)
- [はじめに](#はじめに)
- [オンラインで試す](#オンラインで試す)
@@ -54,31 +54,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
<tr>
<td colspan="2" valign="top" align="center">
<strong>アニメーションTransformerコネクタ</strong><br />
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
<img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCPアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
<strong>RAG技術ダイアグラム</strong><br />
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
<img src="../../public/rag_prod.svg" alt="RAGアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWSアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
<strong>ReactとAWSによる認証</strong><br />
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
<img src="../../public/auth.svg" alt="認証アーキテクチャ図" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azureアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
<strong>オープンイノベーション</strong><br />
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
<img src="../../public/inno.svg" alt="オープンイノベーション図" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫のスケッチ</strong><br />
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
</td>
</tr>
@@ -96,9 +96,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- **クラウドアーキテクチャダイアグラムサポート**クラウドアーキテクチャダイアグラムの生成を専門的にサポートAWS、GCP、Azure
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
## MCPサーバー(プレビュー)
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
## MCPサーバー
MCPModel Context Protocolを介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。

View File

@@ -95,6 +95,10 @@ linux:
arch:
- x64
- arm64
- target: rpm
arch:
- x64
- arm64
# Publish configuration (optional)
publish:

View File

@@ -101,8 +101,8 @@ declare global {
}
export type {
ConfigPreset,
ApplyPresetResult,
ConfigPreset,
ProxyConfig,
SetProxyResult,
SetUserLocaleResult,

View File

@@ -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
})

View File

@@ -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`,
@@ -1356,12 +1372,13 @@ 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
// QvQ (Qwen Visual QA) models are vision models — exclude them even when prefixed with "qwen/"
if (
lowerModelId.includes("qwen") &&
!hasVisionIndicator &&
!lowerModelId.includes("qwen3.5-plus") &&
!lowerModelId.includes("qwen3.5-flash")
!lowerModelId.includes("qwen3.5") &&
!lowerModelId.includes("qvq")
) {
return false
}

17
lib/drawio-themes.ts Normal file
View File

@@ -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)
)
}

View File

@@ -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",
@@ -103,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",

View File

@@ -76,8 +76,7 @@
"creativeDescription": "楽しくてクリエイティブなものを描く",
"cachedNote": "例はキャッシュされ、即座に応答します",
"mcpServer": "MCP サーバー",
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用",
"preview": "プレビュー"
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用"
},
"settings": {
"title": "設定",
@@ -103,10 +102,12 @@
"theme": "テーマ",
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
"drawioStyle": "DrawIO スタイル",
"drawioStyleDescription": "キャンバススタイル",
"switchTo": "切り替え",
"minimal": "ミニマル",
"sketch": "スケッチ",
"drawioStyleDescription": "キャンバススタイル",
"themeDefault": "デフォルト",
"themeDark": "ダーク",
"themeMinimal": "ミニマル",
"themeSketch": "スケッチ",
"themeSimple": "シンプル",
"diagramStyle": "ダイアグラムスタイル",
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
"sendShortcut": "送信ショートカット",

View File

@@ -76,8 +76,7 @@
"creativeDescription": "繪製有趣且富有創意的內容",
"cachedNote": "範例已快取,可即時回應",
"mcpServer": "MCP 伺服器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
"preview": "預覽"
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
},
"settings": {
"title": "設定",
@@ -103,10 +102,12 @@
"theme": "主題",
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
"drawioStyle": "DrawIO 樣式",
"drawioStyleDescription": "畫布樣式",
"switchTo": "切換到",
"minimal": "簡約",
"sketch": "草圖",
"drawioStyleDescription": "畫布樣式",
"themeDefault": "預設",
"themeDark": "深色",
"themeMinimal": "簡約",
"themeSketch": "草圖",
"themeSimple": "簡單",
"diagramStyle": "圖表樣式",
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
"sendShortcut": "傳送快捷鍵",

View File

@@ -76,8 +76,7 @@
"creativeDescription": "绘制有趣且富有创意的内容",
"cachedNote": "示例已缓存,可即时响应",
"mcpServer": "MCP 服务器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
"preview": "预览"
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
},
"settings": {
"title": "设置",
@@ -103,10 +102,12 @@
"theme": "主题",
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
"drawioStyle": "DrawIO 样式",
"drawioStyleDescription": "画布样式",
"switchTo": "切换到",
"minimal": "简约",
"sketch": "草图",
"drawioStyleDescription": "画布样式",
"themeDefault": "默认",
"themeDark": "深色",
"themeMinimal": "简约",
"themeSketch": "草图",
"themeSimple": "简单",
"diagramStyle": "图表样式",
"diagramStyleDescription": "切换简约与精致图表输出模式。",
"sendShortcut": "发送快捷键",

View File

@@ -58,33 +58,6 @@ interface ChatSessionDB extends DBSchema {
// Database singleton
let dbPromise: Promise<IDBPDatabase<ChatSessionDB>> | 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 <T>(
action: (db: IDBPDatabase<ChatSessionDB>) => Promise<T>,
): Promise<T> => {
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<IDBPDatabase<ChatSessionDB>> {
if (!dbPromise) {
@@ -115,23 +88,7 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
}
}
},
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<boolean> {
if (!isIndexedDBAvailable()) return false
try {
await getDB()
return true
} catch {
return false
}
}
// CRUD Operations
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
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<SessionMetadata[]> {
export async function getSession(id: string): Promise<ChatSession | null> {
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<ChatSession | null> {
export async function saveSession(session: ChatSession): Promise<boolean> {
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<boolean> {
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<boolean> {
export async function deleteSession(id: string): Promise<void> {
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<void> {
export async function getSessionCount(): Promise<number> {
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<number> {
export async function deleteOldestSession(): Promise<void> {
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)
}

View File

@@ -9,7 +9,9 @@
export function isPrivateUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
const hostname = url.hostname.toLowerCase()
// Strip a trailing dot so FQDN forms like "localhost." (which still
// resolve to 127.0.0.1) cannot bypass the equality checks below.
const hostname = url.hostname.toLowerCase().replace(/\.$/, "")
// Block localhost
if (

View File

@@ -57,33 +57,6 @@ export function generateDefaultTitle(prompt: string): string {
// Database singleton
let dbPromise: Promise<IDBPDatabase<TemplateDB>> | 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 <T>(
action: (db: IDBPDatabase<TemplateDB>) => Promise<T>,
): Promise<T> => {
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<IDBPDatabase<TemplateDB>> {
if (!dbPromise) {
@@ -101,23 +74,7 @@ async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
}
}
},
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<Template[]> {
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<Template[]> {
export async function getTemplate(id: string): Promise<Template | null> {
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 template:", error)
return null
@@ -182,9 +137,8 @@ export async function createTemplate(
}
try {
await withDB(async (db) => {
await db.put(STORE_NAME, template)
})
const db = await getDB()
await db.put(STORE_NAME, template)
return template
} catch (error) {
console.error("Failed to create template:", error)
@@ -198,20 +152,19 @@ export async function updateTemplate(
): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null
try {
return await withDB(async (db) => {
const existing = await db.get(STORE_NAME, id)
if (!existing) return null
const db = await getDB()
const existing = await db.get(STORE_NAME, id)
if (!existing) return null
const updated: Template = {
...existing,
...updates,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: Date.now(),
}
await db.put(STORE_NAME, updated)
return updated
})
const updated: Template = {
...existing,
...updates,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: Date.now(),
}
await db.put(STORE_NAME, updated)
return updated
} catch (error) {
console.error("Failed to update template:", error)
return null
@@ -221,9 +174,8 @@ export async function updateTemplate(
export async function deleteTemplate(id: string): Promise<boolean> {
if (!isIndexedDBAvailable()) return false
try {
await withDB(async (db) => {
await db.delete(STORE_NAME, id)
})
const db = await getDB()
await db.delete(STORE_NAME, id)
return true
} catch (error) {
console.error("Failed to delete template:", error)
@@ -237,25 +189,24 @@ export async function duplicateTemplate(
): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null
try {
return await withDB(async (db) => {
const existing = await db.get(STORE_NAME, id)
if (!existing) return null
const db = await getDB()
const existing = await db.get(STORE_NAME, id)
if (!existing) return null
const now = Date.now()
const duplicate: Template = {
...existing,
id: nanoid(),
title: `${existing.title} ${copySuffix}`,
createdAt: now,
updatedAt: now,
clickCount: 0,
runCount: 0,
lastUsedAt: 0,
pinned: false,
}
await db.put(STORE_NAME, duplicate)
return duplicate
})
const now = Date.now()
const duplicate: Template = {
...existing,
id: nanoid(),
title: `${existing.title} ${copySuffix}`,
createdAt: now,
updatedAt: now,
clickCount: 0,
runCount: 0,
lastUsedAt: 0,
pinned: false,
}
await db.put(STORE_NAME, duplicate)
return duplicate
} catch (error) {
console.error("Failed to duplicate template:", error)
return null
@@ -267,13 +218,12 @@ export async function duplicateTemplate(
export async function incrementClickCount(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
await withDB(async (db) => {
const template = await db.get(STORE_NAME, id)
if (!template) return
template.clickCount += 1
template.updatedAt = Date.now()
await db.put(STORE_NAME, template)
})
const db = await getDB()
const template = await db.get(STORE_NAME, id)
if (!template) return
template.clickCount += 1
template.updatedAt = Date.now()
await db.put(STORE_NAME, template)
} catch (error) {
console.error("Failed to increment click count:", error)
}
@@ -282,15 +232,14 @@ export async function incrementClickCount(id: string): Promise<void> {
export async function incrementRunCount(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
await withDB(async (db) => {
const template = await db.get(STORE_NAME, id)
if (!template) return
const now = Date.now()
template.runCount += 1
template.lastUsedAt = now
template.updatedAt = now
await db.put(STORE_NAME, template)
})
const db = await getDB()
const template = await db.get(STORE_NAME, id)
if (!template) return
const now = Date.now()
template.runCount += 1
template.lastUsedAt = now
template.updatedAt = now
await db.put(STORE_NAME, template)
} catch (error) {
console.error("Failed to increment run count:", error)
}
@@ -423,9 +372,8 @@ export async function importTemplates(
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
}
try {
await withDB(async (db) => {
await db.put(STORE_NAME, newTemplate)
})
const db = await getDB()
await db.put(STORE_NAME, newTemplate)
existingKeys.add(key)
imported++
} catch (error) {

2478
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "next-ai-draw-io",
"version": "0.4.14",
"version": "0.4.16",
"license": "Apache-2.0",
"private": true,
"main": "dist-electron/main/index.js",
@@ -49,9 +49,9 @@
"@langfuse/tracing": "^4.4.9",
"@next/third-parties": "^16.0.6",
"@opennextjs/cloudflare": "^1.17.1",
"@openrouter/ai-sdk-provider": "^1.5.4",
"@openrouter/ai-sdk-provider": "^2.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
"@opentelemetry/sdk-trace-node": "^2.2.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-collapsible": "^1.1.12",
@@ -77,7 +77,7 @@
"nanoid": "^5.0.0",
"negotiator": "^1.0.0",
"next": "^16.0.7",
"ollama-ai-provider-v2": "^2.0.0",
"ollama-ai-provider-v2": "^3.0.0",
"pako": "^2.1.0",
"prism-react-renderer": "^2.4.1",
"react": "^19.1.2",
@@ -108,7 +108,7 @@
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.4.10",
"@biomejs/biome": "2.4.13",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
@@ -127,7 +127,7 @@
"cross-env": "^10.1.0",
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"esbuild": "^0.27.2",
"esbuild": "^0.28.0",
"eslint": "9.39.4",
"eslint-config-next": "16.1.6",
"husky": "^9.1.7",

View File

@@ -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",
@@ -521,9 +521,9 @@
}
},
"node_modules/@types/node": {
"version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2062,9 +2062,9 @@
}
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -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",

View File

@@ -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:
<mxGraphModel>

View File

@@ -12,7 +12,8 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]

1
public/agile_scrum.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

1
public/auth.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

1
public/inno.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

1
public/rag_prod.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

1
public/resnet50.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -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 ({

View File

@@ -200,12 +200,21 @@ 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)
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)
@@ -236,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<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {}
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<typeof vi.fn>
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<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {}