Compare commits

..

1 Commits

Author SHA1 Message Date
dayuan.jiang
6ed05ed24d chore: always bundle latest draw.io version in Electron builds
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:13:40 +09:00
47 changed files with 1687 additions and 2449 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](#mcp-server)
- [MCP Server (Preview)](#mcp-server-preview)
- [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>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" />
<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" />
</td>
<td width="50%" valign="top">
<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" />
<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" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<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" />
<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" />
</td>
<td width="50%" valign="top">
<strong>Cat sketch</strong><br />
<strong>Cat sketch prompt</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,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)
- **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).

View File

@@ -175,106 +175,92 @@ export default function AboutCN() {
</p>
<div className="space-y-8">
{/* ResNet50 Architecture */}
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
ResNet50模型架构动画
Transformer连接器
</h3>
<p className="text-gray-600 mb-4">
<strong>Prompt:</strong> Give me an{" "}
<strong>animated</strong> architecture diagram
of the ResNet50 model.
<strong></strong>
<strong></strong>Transformer架构图
</p>
<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>
<Image
src="/animated_connectors.svg"
alt="带动画连接器的Transformer架构"
width={480}
height={360}
className="mx-auto"
/>
</div>
{/* Diagram Grid */}
{/* Cloud Architecture 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">
RAG技术
GCP架构
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate a RAG
architecture diagram for{" "}
<strong>chat application</strong>. Use
connected diagram for data ingestion
<strong></strong> 使
<strong>GCP图标</strong>
GCP架构图
</p>
<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>
<Image
src="/gcp_demo.svg"
alt="GCP架构图"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
React和AWS认证流程
AWS架构图
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate
authentication process using React with{" "}
<strong>AWS</strong>. Use Serverless
architecture.
<strong></strong> 使
<strong>AWS图标</strong>
AWS架构图
</p>
<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>
<Image
src="/aws_demo.svg"
alt="AWS架构图"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Scrum流程
Azure架构图
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate agile
scrum workflow diagram for software
development team.
<strong></strong> 使
<strong>Azure图标</strong>
Azure架构图
</p>
<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>
<Image
src="/azure_demo.svg"
alt="Azure架构图"
width={400}
height={300}
className="mx-auto"
/>
</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>Prompt:</strong> Create
visualization of Henry Chesbrough&apos;s
Open Innovation model.
<strong></strong>{" "}
</p>
<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>
<Image
src="/cat_demo.svg"
alt="猫咪绘图"
width={240}
height={240}
className="mx-auto"
/>
</div>
</div>
</div>

View File

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

View File

@@ -182,106 +182,96 @@ export default function About() {
</p>
<div className="space-y-8">
{/* ResNet50 Architecture */}
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Animated ResNet50 Model Architecture
Animated Transformer Connectors
</h3>
<p className="text-gray-600 mb-4">
<strong>Prompt:</strong> Give me an{" "}
<strong>animated</strong> architecture diagram
of the ResNet50 model.
<strong>animated connector</strong> diagram of
transformer&apos;s architecture.
</p>
<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>
<Image
src="/animated_connectors.svg"
alt="Transformer Architecture with Animated Connectors"
width={480}
height={360}
className="mx-auto"
/>
</div>
{/* Diagram Grid */}
{/* Cloud Architecture 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">
RAG Technique Diagram
GCP Architecture Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate a RAG
architecture diagram for{" "}
<strong>chat application</strong>. Use
connected diagram for data ingestion
<strong>Prompt:</strong> Generate a GCP
architecture diagram with{" "}
<strong>GCP icons</strong>. Users connect to
a frontend hosted on an instance.
</p>
<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>
<Image
src="/gcp_demo.svg"
alt="GCP Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Authentication using React and AWS
AWS Architecture Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate
authentication process using React with{" "}
<strong>AWS</strong>. Use Serverless
architecture.
<strong>Prompt:</strong> Generate an AWS
architecture diagram with{" "}
<strong>AWS icons</strong>. Users connect to
a frontend hosted on an instance.
</p>
<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>
<Image
src="/aws_demo.svg"
alt="AWS Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Agile Scrum Process
Azure Architecture Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate agile
scrum workflow diagram for software
development team.
<strong>Prompt:</strong> Generate an Azure
architecture diagram with{" "}
<strong>Azure icons</strong>. Users connect
to a frontend hosted on an instance.
</p>
<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>
<Image
src="/azure_demo.svg"
alt="Azure Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Open Innovation
Cat Sketch
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Create
visualization of Henry Chesbrough&apos;s
Open Innovation model.
<strong>Prompt:</strong> Draw a cute cat for
me.
</p>
<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>
<Image
src="/cat_demo.svg"
alt="Cat Drawing"
width={240}
height={240}
className="mx-auto"
/>
</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,11 +27,13 @@ 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<DrawioTheme>("kennedy")
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
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",
)
@@ -54,7 +56,7 @@ export default function Home() {
}
const savedUi = localStorage.getItem("drawio-theme")
if (isDrawioTheme(savedUi)) {
if (savedUi === "min" || savedUi === "sketch") {
setDrawioUi(savedUi)
}
@@ -82,6 +84,11 @@ 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])
@@ -90,6 +97,17 @@ 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)
@@ -99,9 +117,10 @@ export default function Home() {
resetDrawioReady()
}
const handleDrawioUiChange = (theme: DrawioTheme) => {
localStorage.setItem("drawio-theme", theme)
setDrawioUi(theme)
const handleDrawioUiChange = () => {
const newUi = drawioUi === "min" ? "sketch" : "min"
localStorage.setItem("drawio-theme", newUi)
setDrawioUi(newUi)
setIsDrawioReady(false)
resetDrawioReady()
}
@@ -172,7 +191,7 @@ export default function Home() {
}`}
>
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
{isLoaded && (
{isLoaded && canPersistChecked && (
<div
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
>
@@ -180,19 +199,28 @@ export default function Home() {
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
ref={drawioRef}
autosave
onAutoSave={handleDiagramAutoSave}
onAutoSave={handleDrawioAutoSave}
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 || drawioUi === "dark",
dark: darkMode,
lang: currentLang,
// Enable offline mode in Electron to disable external service calls
...(isElectron && {
@@ -240,7 +268,7 @@ export default function Home() {
isVisible={isChatVisible}
onToggleVisibility={toggleChatPanel}
drawioUi={drawioUi}
onDrawioUiChange={handleDrawioUiChange}
onToggleDrawioUi={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 { isPrivateUrl } from "@/lib/ssrf-protection"
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
const EXTRACT_TIMEOUT_MS = 15000
@@ -28,10 +28,8 @@ export async function POST(req: Request) {
)
}
// 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)) {
// SSRF protection
if (!allowPrivateUrls && 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.14/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

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">
{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,7 +417,6 @@ 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) {
@@ -431,17 +430,8 @@ export function ChatMessageDisplay({
return
}
// 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)
}
// Single message added - smooth scroll
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
}
}, [messages])

View File

@@ -32,7 +32,6 @@ 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"
@@ -69,8 +68,8 @@ interface ChatMessage {
interface ChatPanelProps {
isVisible: boolean
onToggleVisibility: () => void
drawioUi: DrawioTheme
onDrawioUiChange: (theme: DrawioTheme) => void
drawioUi: "min" | "sketch"
onToggleDrawioUi: () => void
darkMode: boolean
onToggleDarkMode: () => void
isMobile?: boolean
@@ -111,7 +110,7 @@ export default function ChatPanel({
isVisible,
onToggleVisibility,
drawioUi,
onDrawioUiChange,
onToggleDrawioUi,
darkMode,
onToggleDarkMode,
isMobile = false,
@@ -1443,7 +1442,7 @@ export default function ChatPanel({
open={showSettingsDialog}
onOpenChange={setShowSettingsDialog}
drawioUi={drawioUi}
onDrawioUiChange={onDrawioUiChange}
onToggleDrawioUi={onToggleDrawioUi}
darkMode={darkMode}
onToggleDarkMode={onToggleDarkMode}
minimalStyle={minimalStyle}

View File

@@ -107,8 +107,8 @@ export function ChatLobby({
currentInput = "",
dict,
}: ChatLobbyProps) {
const [templatesExpanded, setTemplatesExpanded] = useState(true)
const [examplesExpanded, setExamplesExpanded] = useState(true)
const [templatesExpanded, setTemplatesExpanded] = useState(false)
const [examplesExpanded, setExamplesExpanded] = useState(false)
const [panelVisibility, setPanelVisibility] = useState(getPanelVisibility)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [sessionToDelete, setSessionToDelete] = useState<string | null>(null)
@@ -125,25 +125,19 @@ export function ChatLobby({
const hasHistory = sessions.length > 0
if (!hasHistory) {
if (!panelVisibility.myTemplates && !panelVisibility.quickExamples) {
return null
if (panelVisibility.myTemplates) {
return (
<TemplatePanel
setInput={setInput}
onSendTemplate={onSendTemplate}
currentInput={currentInput}
/>
)
}
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>
)
if (panelVisibility.quickExamples) {
return <ExamplePanel setInput={setInput} setFiles={setFiles} />
}
return null
}
// Show history + collapsible examples when there are sessions

View File

@@ -195,22 +195,7 @@ export function ToolCallCard({
{input && isExpanded && (
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
{typeof input === "object" && input.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" />
)
<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 min-h-0">
<ScrollArea className="flex-1 px-2">
<div className="space-y-1 pb-2">
{config.providers.length === 0 ? (
<div className="px-3 py-8 text-center">

View File

@@ -25,7 +25,6 @@ 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"
@@ -64,8 +63,8 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
interface SettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
drawioUi: DrawioTheme
onDrawioUiChange: (theme: DrawioTheme) => void
drawioUi: "min" | "sketch"
onToggleDrawioUi: () => void
darkMode: boolean
onToggleDarkMode: () => void
minimalStyle?: boolean
@@ -91,7 +90,7 @@ function SettingsContent({
open,
onOpenChange,
drawioUi,
onDrawioUiChange,
onToggleDrawioUi,
darkMode,
onToggleDarkMode,
minimalStyle = false,
@@ -135,11 +134,8 @@ function SettingsContent({
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
useEffect(() => {
// 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
// Only fetch if not cached in localStorage
if (getStoredAccessCodeRequired() !== null) return
fetch(getApiEndpoint("/api/config"))
.then((res) => {
@@ -155,9 +151,10 @@ function SettingsContent({
setAccessCodeRequired(required)
})
.catch(() => {
// Keep existing cached value on error
// Don't cache on error - allow retry on next mount
setAccessCodeRequired(false)
})
}, [open])
}, [])
// Detect current language from pathname
useEffect(() => {
@@ -433,40 +430,23 @@ function SettingsContent({
{/* Draw.io Style */}
<SettingItem
label={dict.settings.drawioStyle}
description={dict.settings.drawioStyleDescription}
description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
>
<Select
value={drawioUi}
onValueChange={(v) =>
onDrawioUiChange(v as DrawioTheme)
}
<Button
id="drawio-ui"
variant="outline"
onClick={onToggleDrawioUi}
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
>
<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>
{dict.settings.switchTo}{" "}
{drawioUi === "min"
? dict.settings.sketch
: dict.settings.minimal}
</Button>
</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>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
<img src="../../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<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" />
<strong>GCP架构图</strong><br />
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/gcp_demo.svg" alt="GCP架构图" width="480" />
</td>
<td width="50%" valign="top">
<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" />
<strong>AWS架构图</strong><br />
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/aws_demo.svg" alt="AWS架构图" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<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" />
<strong>Azure架构图</strong><br />
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/azure_demo.svg" alt="Azure架构图" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫咪素描</strong><br />
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
</td>
</tr>
@@ -98,7 +98,9 @@ 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

@@ -53,13 +53,6 @@ ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514
```
或者使用 Bearer 认证令牌(例如通过会下发 OAuth 风格 token 的网关时)。`ANTHROPIC_AUTH_TOKEN` 会作为 `Authorization: Bearer <token>` 头发送,而 `ANTHROPIC_API_KEY` 会作为 `x-api-key` 头发送。两者互斥,只能设置其中之一:
```bash
ANTHROPIC_AUTH_TOKEN=your_auth_token
AI_MODEL=claude-sonnet-4-5-20250514
```
可选的自定义端点:
```bash
@@ -222,7 +215,7 @@ MiniMax 支持两种 API 格式:
```bash
MINIMAX_API_KEY=your_api_key
AI_MODEL=MiniMax-M3
AI_MODEL=MiniMax-M2.7
```
可选配置:

View File

@@ -68,13 +68,6 @@ ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514
```
Or use a Bearer auth token instead of an API key (e.g. when going through a gateway that issues OAuth-style tokens). `ANTHROPIC_AUTH_TOKEN` is sent as `Authorization: Bearer <token>`, while `ANTHROPIC_API_KEY` is sent as `x-api-key`. The two are mutually exclusive — set only one:
```bash
ANTHROPIC_AUTH_TOKEN=your_auth_token
AI_MODEL=claude-sonnet-4-5-20250514
```
Optional custom endpoint:
```bash
@@ -237,7 +230,7 @@ MiniMax supports two API formats:
```bash
MINIMAX_API_KEY=your_api_key
AI_MODEL=MiniMax-M3
AI_MODEL=MiniMax-M2.7
```
Optional configuration:

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>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
<img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<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" />
<strong>GCPアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<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" />
<strong>AWSアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<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" />
<strong>Azureアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫のスケッチ</strong><br />
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
</td>
</tr>
@@ -96,7 +96,9 @@ 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

@@ -53,13 +53,6 @@ ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514
```
または、Bearer 認証トークンを使用することもできますOAuth スタイルのトークンを発行するゲートウェイ経由で利用する場合など)。`ANTHROPIC_AUTH_TOKEN``Authorization: Bearer <token>` ヘッダーで送信され、`ANTHROPIC_API_KEY``x-api-key` ヘッダーで送信されます。両者は排他的なので、いずれか一方のみを設定してください:
```bash
ANTHROPIC_AUTH_TOKEN=your_auth_token
AI_MODEL=claude-sonnet-4-5-20250514
```
任意のカスタムエンドポイント:
```bash
@@ -222,7 +215,7 @@ MiniMax は 2 つの API 形式をサポートしています:
```bash
MINIMAX_API_KEY=your_api_key
AI_MODEL=MiniMax-M3
AI_MODEL=MiniMax-M2.7
```
オプション設定:

View File

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

View File

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

View File

@@ -60,13 +60,6 @@ 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

@@ -25,8 +25,7 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# OPENAI_REASONING_SUMMARY=detailed # Optional: Override reasoning summary (none/brief/detailed)
# Anthropic (Direct) Configuration
# ANTHROPIC_API_KEY=sk-ant-... # Sent as `x-api-key` header
# ANTHROPIC_AUTH_TOKEN= # Alternative to ANTHROPIC_API_KEY; sent as `Authorization: Bearer` header (mutually exclusive)
# ANTHROPIC_API_KEY=sk-ant-...
# ANTHROPIC_BASE_URL=https://your-custom-anthropic/v1
# ANTHROPIC_THINKING_TYPE=enabled # Optional: Anthropic extended thinking (enabled)
# ANTHROPIC_THINKING_BUDGET_TOKENS=12000 # Optional: Budget for extended thinking in tokens

View File

@@ -573,15 +573,7 @@ function detectProvider(): ProviderName | null {
// Skip ollama - it doesn't require credentials
continue
}
// Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
const hasCredential =
provider === "anthropic"
? !!(
process.env.ANTHROPIC_API_KEY ||
process.env.ANTHROPIC_AUTH_TOKEN
)
: !!process.env[envVar]
if (hasCredential) {
if (process.env[envVar]) {
// Azure requires additional config (baseURL or resourceName)
if (provider === "azure") {
const hasBaseUrl = !!process.env.AZURE_BASE_URL
@@ -623,26 +615,13 @@ function validateProviderCredentials(
return
}
// Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
if (provider === "anthropic" && !customApiKeyEnv) {
const hasCredential = !!(
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
// Use custom env var name if provided, otherwise use default
const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider]
if (requiredVar && !process.env[requiredVar]) {
throw new Error(
`${requiredVar} environment variable is required for ${provider} provider. ` +
`Please set it in your .env.local file.`,
)
if (!hasCredential) {
throw new Error(
`Either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable is required for anthropic provider. ` +
`Please set one in your .env.local file.`,
)
}
} else {
// Use custom env var name if provided, otherwise use default
const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider]
if (requiredVar && !process.env[requiredVar]) {
throw new Error(
`${requiredVar} environment variable is required for ${provider} provider. ` +
`Please set it in your .env.local file.`,
)
}
}
// Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition to API key
@@ -866,16 +845,8 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
serverBaseUrl,
"https://api.anthropic.com/v1",
)
// Anthropic supports two auth methods (mutually exclusive):
// - apiKey: sends as `x-api-key` header
// - authToken: sends as `Authorization: Bearer <token>` header
// Prefer apiKey if present (including client overrides); fall back
// to ANTHROPIC_AUTH_TOKEN env var only when no apiKey is available.
const authToken = !apiKey
? process.env.ANTHROPIC_AUTH_TOKEN
: undefined
const customProvider = createAnthropic({
...(authToken ? { authToken } : { apiKey }),
apiKey,
baseURL,
headers: ANTHROPIC_BETA_HEADERS,
})
@@ -1291,6 +1262,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "glm":
case "qwen":
case "qiniu":
case "kimi":
case "novita": {
const envVar = PROVIDER_ENV_VARS[provider]
if (!envVar) {
@@ -1316,23 +1288,6 @@ 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`,
@@ -1390,12 +1345,8 @@ export function supportsImageInput(modelId: string): boolean {
return false
}
// MiniMax text models (MiniMax-M2.x series are text-only; M3 supports image input)
if (
lowerModelId.includes("minimax") &&
!hasVisionIndicator &&
!lowerModelId.includes("m3")
) {
// MiniMax text models (MiniMax-M2.x series are text-only)
if (lowerModelId.includes("minimax") && !hasVisionIndicator) {
return false
}
@@ -1405,13 +1356,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/"
// qwen3.5-plus is a vision model
if (
lowerModelId.includes("qwen") &&
!hasVisionIndicator &&
!lowerModelId.includes("qwen3.5") &&
!lowerModelId.includes("qvq")
!lowerModelId.includes("qwen3.5-plus") &&
!lowerModelId.includes("qwen3.5-flash")
) {
return false
}

View File

@@ -1,17 +0,0 @@
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,7 +76,8 @@
"creativeDescription": "Draw something fun and creative",
"cachedNote": "Examples are cached for instant response",
"mcpServer": "MCP Server",
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor"
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor",
"preview": "PREVIEW"
},
"settings": {
"title": "Settings",
@@ -102,12 +103,10 @@
"theme": "Theme",
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
"drawioStyle": "DrawIO Style",
"drawioStyleDescription": "Canvas style",
"themeDefault": "Default",
"themeDark": "Dark",
"themeMinimal": "Minimal",
"themeSketch": "Sketch",
"themeSimple": "Simple",
"drawioStyleDescription": "Canvas style:",
"switchTo": "Switch to",
"minimal": "Minimal",
"sketch": "Sketch",
"diagramStyle": "Diagram Style",
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
"sendShortcut": "Send Shortcut",

View File

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

View File

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

View File

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

View File

@@ -58,6 +58,33 @@ 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) {
@@ -88,7 +115,23 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
}
}
},
terminated() {
resetDBPromise()
},
})
dbPromise
.then((db) => {
db.onversionchange = () => {
db.close()
resetDBPromise()
}
db.onclose = () => {
resetDBPromise()
}
})
.catch(() => {
resetDBPromise()
})
}
return dbPromise
}
@@ -103,31 +146,46 @@ 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 {
const db = await getDB()
const tx = db.transaction(STORE_NAME, "readonly")
const index = tx.store.index("by-updated")
const metadata: SessionMetadata[] = []
return await withDB(async (db) => {
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 []
@@ -137,8 +195,9 @@ export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
export async function getSession(id: string): Promise<ChatSession | null> {
if (!isIndexedDBAvailable()) return null
try {
const db = await getDB()
return (await db.get(STORE_NAME, id)) || null
return await withDB(async (db) => {
return (await db.get(STORE_NAME, id)) || null
})
} catch (error) {
console.error("Failed to get session:", error)
return null
@@ -148,8 +207,9 @@ export async function getSession(id: string): Promise<ChatSession | null> {
export async function saveSession(session: ChatSession): Promise<boolean> {
if (!isIndexedDBAvailable()) return false
try {
const db = await getDB()
await db.put(STORE_NAME, session)
await withDB(async (db) => {
await db.put(STORE_NAME, session)
})
return true
} catch (error) {
// Handle quota exceeded
@@ -161,8 +221,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
await deleteOldestSession()
// Retry once
try {
const db = await getDB()
await db.put(STORE_NAME, session)
await withDB(async (db) => {
await db.put(STORE_NAME, session)
})
return true
} catch (retryError) {
console.error(
@@ -181,8 +242,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
export async function deleteSession(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
const db = await getDB()
await db.delete(STORE_NAME, id)
await withDB(async (db) => {
await db.delete(STORE_NAME, id)
})
} catch (error) {
console.error("Failed to delete session:", error)
}
@@ -191,8 +253,9 @@ export async function deleteSession(id: string): Promise<void> {
export async function getSessionCount(): Promise<number> {
if (!isIndexedDBAvailable()) return 0
try {
const db = await getDB()
return await db.count(STORE_NAME)
return await withDB(async (db) => {
return await db.count(STORE_NAME)
})
} catch (error) {
console.error("Failed to get session count:", error)
return 0
@@ -202,14 +265,15 @@ export async function getSessionCount(): Promise<number> {
export async function deleteOldestSession(): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
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
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
})
} catch (error) {
console.error("Failed to delete oldest session:", error)
}

View File

@@ -9,40 +9,17 @@
export function isPrivateUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
// 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(/^\[|\]$/g, "")
.replace(/\.$/, "")
const hostname = url.hostname.toLowerCase()
// Block localhost
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1" ||
hostname === "::"
hostname === "::1"
) {
return true
}
// Block IPv6 unique-local (fc00::/7), link-local (fe80::/10),
// and IPv4-mapped (::ffff:0:0/96) hosts.
if (hostname.includes(":")) {
if (
hostname.startsWith("fc") ||
hostname.startsWith("fd") ||
hostname.startsWith("::ffff:")
) {
return true
}
const linkLocal = hostname.match(/^fe([0-9a-f]{2}):/)
if (linkLocal) {
const high = parseInt(linkLocal[1], 16)
if (high >= 0x80 && high <= 0xbf) return true
}
}
// Block AWS/cloud metadata endpoints
if (
hostname === "169.254.169.254" ||

View File

@@ -57,6 +57,33 @@ 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) {
@@ -74,7 +101,23 @@ async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
}
}
},
terminated() {
resetDBPromise()
},
})
dbPromise
.then((db) => {
db.onversionchange = () => {
db.close()
resetDBPromise()
}
db.onclose = () => {
resetDBPromise()
}
})
.catch(() => {
resetDBPromise()
})
}
return dbPromise
}
@@ -94,9 +137,10 @@ export function isIndexedDBAvailable(): boolean {
export async function getAllTemplates(): Promise<Template[]> {
if (!isIndexedDBAvailable()) return []
try {
const db = await getDB()
const templates = await db.getAll(STORE_NAME)
return sortTemplates(templates)
return await withDB(async (db) => {
const templates = await db.getAll(STORE_NAME)
return sortTemplates(templates)
})
} catch (error) {
console.error("Failed to get templates:", error)
return []
@@ -106,8 +150,9 @@ export async function getAllTemplates(): Promise<Template[]> {
export async function getTemplate(id: string): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null
try {
const db = await getDB()
return (await db.get(STORE_NAME, id)) || null
return await withDB(async (db) => {
return (await db.get(STORE_NAME, id)) || null
})
} catch (error) {
console.error("Failed to get template:", error)
return null
@@ -137,8 +182,9 @@ export async function createTemplate(
}
try {
const db = await getDB()
await db.put(STORE_NAME, template)
await withDB(async (db) => {
await db.put(STORE_NAME, template)
})
return template
} catch (error) {
console.error("Failed to create template:", error)
@@ -152,19 +198,20 @@ export async function updateTemplate(
): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null
try {
const db = await getDB()
const existing = await db.get(STORE_NAME, id)
if (!existing) return null
return await withDB(async (db) => {
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
@@ -174,8 +221,9 @@ export async function updateTemplate(
export async function deleteTemplate(id: string): Promise<boolean> {
if (!isIndexedDBAvailable()) return false
try {
const db = await getDB()
await db.delete(STORE_NAME, id)
await withDB(async (db) => {
await db.delete(STORE_NAME, id)
})
return true
} catch (error) {
console.error("Failed to delete template:", error)
@@ -189,24 +237,25 @@ export async function duplicateTemplate(
): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null
try {
const db = await getDB()
const existing = await db.get(STORE_NAME, id)
if (!existing) return null
return await withDB(async (db) => {
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
@@ -218,12 +267,13 @@ export async function duplicateTemplate(
export async function incrementClickCount(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
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)
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)
})
} catch (error) {
console.error("Failed to increment click count:", error)
}
@@ -232,14 +282,15 @@ export async function incrementClickCount(id: string): Promise<void> {
export async function incrementRunCount(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return
try {
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)
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)
})
} catch (error) {
console.error("Failed to increment run count:", error)
}
@@ -372,8 +423,9 @@ export async function importTemplates(
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
}
try {
const db = await getDB()
await db.put(STORE_NAME, newTemplate)
await withDB(async (db) => {
await db.put(STORE_NAME, newTemplate)
})
existingKeys.add(key)
imported++
} catch (error) {

View File

@@ -190,202 +190,177 @@ export const PROVIDER_INFO: Record<
// Suggested models per provider for quick add
export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
openai: [
"gpt-5.5-pro",
"gpt-5.5",
"gpt-5.4-pro",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5-codex-mini",
"gpt-5.2-pro",
"gpt-5.2-chat-latest",
"gpt-5.2",
"gpt-5.1-codex-mini",
"gpt-5.1-codex",
"gpt-5.1-chat-latest",
"gpt-5.1",
"gpt-5-pro",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-5-codex",
"gpt-5-chat-latest",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o",
"gpt-4o-mini",
],
anthropic: [
// Claude 4.8 / 4.7 / 4.6 series (latest, dateless pinned IDs)
"claude-opus-4-8",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"claude-opus-4-7",
"claude-opus-4-6",
// Claude 4.5 series
"claude-sonnet-4-5-20250929",
"claude-opus-4-5-20251101",
// Claude 4.5 series (latest)
"claude-opus-4-5-20250514",
"claude-sonnet-4-5-20250514",
// Claude 4 series
"claude-opus-4-20250514",
"claude-sonnet-4-20250514",
// Claude 3.7 series
"claude-3-7-sonnet-20250219",
// Claude 3.5 series
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022",
// Claude 3 series
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
google: [
// Gemini 3 series
"gemini-3.1-pro",
"gemini-3.5-flash",
"gemini-3-flash",
"gemini-3.1-flash-lite",
// Gemini 2.5 series
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-2.5-flash-preview-05-20",
// Gemini 2.0 series
"gemini-2.0-flash",
"gemini-2.0-flash-exp",
"gemini-2.0-flash-lite",
// Gemini 1.5 series
"gemini-1.5-pro",
"gemini-1.5-flash",
// Legacy
"gemini-pro",
],
vertexai: [
// Gemini 3 series
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
"gemini-3-flash-preview",
"gemini-3.1-flash-lite",
// Gemini 2.5 series
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
],
azure: [
"gpt-5.5",
"gpt-5.4",
"gpt-5.1",
"gpt-5",
"gpt-5-mini",
"gpt-4.1",
"gpt-4o",
"gpt-4o-mini",
"o3",
"o4-mini",
// Gemini 2.0 series
"gemini-2.0-flash",
"gemini-2.0-flash-exp",
// Gemini 1.5 series
"gemini-1.5-pro",
"gemini-1.5-flash",
],
azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"],
bedrock: [
// Anthropic Claude
"anthropic.claude-opus-4-8",
"anthropic.claude-opus-4-7",
"anthropic.claude-sonnet-4-6",
"anthropic.claude-opus-4-6-v1",
"anthropic.claude-opus-4-5-20251101-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-opus-4-1-20250805-v1:0",
"anthropic.claude-opus-4-5-20250514-v1:0",
"anthropic.claude-sonnet-4-5-20250514-v1:0",
"anthropic.claude-opus-4-20250514-v1:0",
"anthropic.claude-sonnet-4-20250514-v1:0",
"anthropic.claude-3-7-sonnet-20250219-v1:0",
"anthropic.claude-3-5-sonnet-20241022-v2:0",
"anthropic.claude-3-5-haiku-20241022-v1:0",
"anthropic.claude-3-opus-20240229-v1:0",
"anthropic.claude-3-sonnet-20240229-v1:0",
"anthropic.claude-3-haiku-20240307-v1:0",
// Amazon Nova
"amazon.nova-2-lite-v1:0",
"amazon.nova-premier-v1:0",
"amazon.nova-pro-v1:0",
"amazon.nova-lite-v1:0",
"amazon.nova-micro-v1:0",
// Meta Llama
"meta.llama4-maverick-17b-instruct-v1:0",
"meta.llama4-scout-17b-instruct-v1:0",
"meta.llama3-3-70b-instruct-v1:0",
"meta.llama3-1-405b-instruct-v1:0",
"meta.llama3-1-70b-instruct-v1:0",
// Mistral
"mistral.mistral-large-3-675b-instruct",
"mistral.pixtral-large-2502-v1:0",
"mistral.mistral-large-2411-v1:0",
"mistral.mistral-small-2503-v1:0",
],
openrouter: [
// Anthropic
"anthropic/claude-opus-4.8",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"anthropic/claude-sonnet-4",
"anthropic/claude-opus-4",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3.5-haiku",
// OpenAI
"openai/gpt-5.5",
"openai/gpt-5.4",
"openai/gpt-5.4-mini",
"openai/gpt-4o",
"openai/gpt-4o-mini",
"openai/o1",
"openai/o3-mini",
// Google
"google/gemini-3.1-pro-preview",
"google/gemini-3.5-flash",
"google/gemini-2.5-flash-lite",
// xAI
"x-ai/grok-4.3",
"google/gemini-2.5-pro",
"google/gemini-2.5-flash",
"google/gemini-2.0-flash-exp:free",
// Meta Llama
"meta-llama/llama-4-maverick",
"meta-llama/llama-4-scout",
"meta-llama/llama-3.3-70b-instruct",
"meta-llama/llama-3.1-405b-instruct",
"meta-llama/llama-3.1-70b-instruct",
// DeepSeek
"deepseek/deepseek-v4-pro",
"deepseek/deepseek-v3.2",
"deepseek/deepseek-chat",
"deepseek/deepseek-r1",
// Qwen
"qwen/qwen3.7-max",
"qwen/qwen3-coder",
// MiniMax
"minimax/minimax-m3",
],
deepseek: [
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-chat",
"deepseek-reasoner",
"qwen/qwen-2.5-72b-instruct",
],
deepseek: ["deepseek-chat", "deepseek-reasoner", "deepseek-coder"],
siliconflow: [
// DeepSeek
"deepseek-ai/DeepSeek-V4-Pro",
"deepseek-ai/DeepSeek-V4-Flash",
"deepseek-ai/DeepSeek-V3.2",
// MiniMax
"MiniMaxAI/MiniMax-M3",
// Moonshot
"moonshotai/Kimi-K2.6",
// Z.ai
"zai-org/GLM-5",
"deepseek-ai/DeepSeek-V3",
"deepseek-ai/DeepSeek-R1",
"deepseek-ai/DeepSeek-V2.5",
// Qwen
"Qwen/Qwen3.6-35B-A3B",
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
"Qwen/Qwen3-30B-A3B-Instruct-2507",
"Qwen/Qwen3-VL-32B-Instruct",
// OpenAI open-weights
"openai/gpt-oss-120b",
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-32B-Instruct",
"Qwen/Qwen2.5-Coder-32B-Instruct",
"Qwen/Qwen2.5-7B-Instruct",
"Qwen/Qwen2-VL-72B-Instruct",
"qwen3.5-plus",
],
sglang: [
// SGLang is OpenAI-compatible, models depend on deployment
"default",
],
gateway: [
"openai/gpt-5.5",
"anthropic/claude-opus-4.7",
"google/gemini-3.1-pro-preview",
"xai/grok-4.3",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"openai/gpt-5.4-mini",
"openai/gpt-4o",
"openai/gpt-4o-mini",
"anthropic/claude-sonnet-4-5",
"anthropic/claude-3-5-sonnet",
"google/gemini-2.0-flash",
],
edgeone: ["@tx/deepseek-ai/deepseek-v32"],
doubao: [
// ByteDance Doubao models (Volcengine Ark IDs use dash form)
"doubao-seed-2-0-pro-260215",
"doubao-seed-2-0-lite-260428",
"doubao-seed-2-0-mini-260428",
"doubao-seed-1-8-251228",
"doubao-seed-1-6-251015",
"doubao-seed-1-6-flash-250828",
"doubao-seed-1-6-vision-250815",
"doubao-1-5-pro-32k-250115",
"doubao-1-5-lite-32k-250115",
// ByteDance Doubao models
"doubao-1.5-thinking-pro-250415",
"doubao-1.5-thinking-pro-m-250428",
"doubao-1.5-pro-32k-250115",
"doubao-1.5-pro-256k-250115",
"doubao-pro-32k-241215",
"doubao-pro-256k-241215",
],
modelscope: [
// DeepSeek
"deepseek-ai/DeepSeek-V4-Pro",
"deepseek-ai/DeepSeek-V3.2",
"deepseek-ai/DeepSeek-R1-0528",
"deepseek-ai/DeepSeek-R1",
// Qwen
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-32B-Instruct",
"Qwen/Qwen3-235B-A22B-Instruct-2507",
"Qwen/Qwen3-VL-235B-A22B-Instruct",
"Qwen/Qwen3-Coder-30B-A3B-Instruct",
"Qwen/Qwen3-32B",
"Qwen/Qwen2.5-72B-Instruct",
"qwen3.5-plus",
// DeepSeek
"deepseek-ai/DeepSeek-R1-0528",
"deepseek-ai/DeepSeek-V3.2",
],
minimax: [
// MiniMax models (Anthropic-compatible API)
"MiniMax-M3",
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
],
novita: [
// Novita AI models (OpenAI-compatible API)
"minimax/minimax-m3",
"deepseek/deepseek-v4-pro",
"zai-org/glm-5.1",
"moonshotai/kimi-k2.6",
"deepseek/deepseek-v4-flash",
"moonshotai/kimi-k2.5",
"zai-org/glm-5",
"minimax/minimax-m2.5",
],
}

2571
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.16",
"version": "0.4.14",
"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": "^2.0.0",
"@openrouter/ai-sdk-provider": "^1.5.4",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.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": "^3.0.0",
"ollama-ai-provider-v2": "^2.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.13",
"@biomejs/biome": "2.4.10",
"@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.28.0",
"esbuild": "^0.27.2",
"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.2.0",
"version": "0.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@next-ai-drawio/mcp-server",
"version": "0.2.0",
"version": "0.1.17",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
@@ -521,9 +521,9 @@
}
},
"node_modules/@types/node": {
"version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2062,9 +2062,9 @@
}
},
"node_modules/zod": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -1,6 +1,6 @@
{
"name": "@next-ai-drawio/mcp-server",
"version": "0.2.0",
"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",

View File

@@ -155,22 +155,19 @@ server.registerTool(
server.registerTool(
"create_new_diagram",
{
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.
description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
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 (no existing diagram)
- The user explicitly asks to "start over" or "create a new diagram"
- Creating a new diagram from scratch
- Replacing the current diagram with a completely different one
- Major structural changes that require regenerating the diagram
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
When to use edit_diagram instead:
- Small modifications to existing diagram
- Adding/removing individual elements
- Changing labels, colors, or positions
XML FORMAT - Full mxGraphModel structure:
<mxGraphModel>

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -35,10 +35,8 @@ test.describe("Iframe Interaction", () => {
await expect(
frame
.locator('text="Diagram"')
.or(frame.locator('[title*="Diagram"]'))
.filter({ visible: true })
.first(),
).toBeVisible({ timeout: 30000 })
.or(frame.locator('[title*="Diagram"]')),
).toBeVisible({ timeout: 10000 })
})
test("diagram XML is rendered in iframe after generation", async ({

View File

@@ -180,14 +180,11 @@ describe("supportsImageInput", () => {
expect(supportsImageInput("moonshot-v1-128k")).toBe(false)
})
it("returns false for MiniMax M2 text models", () => {
it("returns false for MiniMax text models", () => {
expect(supportsImageInput("MiniMax-M2.7")).toBe(false)
expect(supportsImageInput("MiniMax-M2.7-highspeed")).toBe(false)
expect(supportsImageInput("MiniMax-M2.5")).toBe(false)
expect(supportsImageInput("MiniMax-M2")).toBe(false)
})
it("returns true for MiniMax M3 (supports image input)", () => {
expect(supportsImageInput("MiniMax-M3")).toBe(true)
expect(supportsImageInput("MiniMax-M2.5-highspeed")).toBe(false)
})
it("returns false for DeepSeek text models", () => {
@@ -203,21 +200,12 @@ 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)
@@ -248,67 +236,6 @@ 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> = {}

View File

@@ -1,21 +0,0 @@
import { describe, expect, it } from "vitest"
import { isPrivateUrl } from "@/lib/ssrf-protection"
describe("isPrivateUrl", () => {
it("blocks private IPv6 URLs", () => {
expect(isPrivateUrl("http://[::1]/")).toBe(true)
expect(isPrivateUrl("http://[0:0:0:0:0:0:0:1]/")).toBe(true)
expect(isPrivateUrl("http://[::]/")).toBe(true)
expect(isPrivateUrl("http://[::ffff:127.0.0.1]/")).toBe(true)
expect(isPrivateUrl("http://[fc00::1]/")).toBe(true)
expect(isPrivateUrl("http://[fd12:3456:789a::1]/")).toBe(true)
expect(isPrivateUrl("http://[fe80::1]/")).toBe(true)
expect(isPrivateUrl("http://[fe9f::1]/")).toBe(true)
expect(isPrivateUrl("http://[febf::1]/")).toBe(true)
})
it("allows public URLs", () => {
expect(isPrivateUrl("https://example.com/article")).toBe(false)
expect(isPrivateUrl("https://fc00.example.com/article")).toBe(false)
})
})