Compare commits

..

2 Commits

Author SHA1 Message Date
dayuan.jiang
d210d0e44e 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 10:56:52 +09:00
dayuan.jiang
b5abbd8e6f 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.
2026-04-10 10:52:44 +09:00
36 changed files with 1498 additions and 2099 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,8 @@ import {
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { type DrawioTheme, isDrawioTheme } from "@/lib/drawio-themes"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
import { isIndexedDBUsable } from "@/lib/session-storage"
export default function Home() { export default function Home() {
const { const {
@@ -27,11 +27,13 @@ export default function Home() {
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const [isChatVisible, setIsChatVisible] = useState(true) const [isChatVisible, setIsChatVisible] = useState(true)
const [drawioUi, setDrawioUi] = useState<DrawioTheme>("kennedy") const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
const [darkMode, setDarkMode] = useState(false) const [darkMode, setDarkMode] = useState(false)
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const [isDrawioReady, setIsDrawioReady] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false)
const [isElectron, setIsElectron] = useState(false) const [isElectron, setIsElectron] = useState(false)
const [canPersist, setCanPersist] = useState(false)
const [canPersistChecked, setCanPersistChecked] = useState(false)
const [drawioBaseUrl, setDrawioBaseUrl] = useState( const [drawioBaseUrl, setDrawioBaseUrl] = useState(
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net", 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") const savedUi = localStorage.getItem("drawio-theme")
if (isDrawioTheme(savedUi)) { if (savedUi === "min" || savedUi === "sketch") {
setDrawioUi(savedUi) setDrawioUi(savedUi)
} }
@@ -82,6 +84,11 @@ export default function Home() {
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`) setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
} }
void (async () => {
const usable = await isIndexedDBUsable()
setCanPersist(usable)
setCanPersistChecked(true)
})()
setIsLoaded(true) setIsLoaded(true)
}, [pathname, router]) }, [pathname, router])
@@ -90,6 +97,13 @@ export default function Home() {
onDrawioLoad() onDrawioLoad()
}, [onDrawioLoad]) }, [onDrawioLoad])
const handleDrawioAutoSave = useCallback(
(data: { xml?: string }) => {
handleDiagramAutoSave(data)
},
[handleDiagramAutoSave],
)
const handleDarkModeChange = () => { const handleDarkModeChange = () => {
const newValue = !darkMode const newValue = !darkMode
setDarkMode(newValue) setDarkMode(newValue)
@@ -99,9 +113,10 @@ export default function Home() {
resetDrawioReady() resetDrawioReady()
} }
const handleDrawioUiChange = (theme: DrawioTheme) => { const handleDrawioUiChange = () => {
localStorage.setItem("drawio-theme", theme) const newUi = drawioUi === "min" ? "sketch" : "min"
setDrawioUi(theme) localStorage.setItem("drawio-theme", newUi)
setDrawioUi(newUi)
setIsDrawioReady(false) setIsDrawioReady(false)
resetDrawioReady() resetDrawioReady()
} }
@@ -172,7 +187,7 @@ export default function Home() {
}`} }`}
> >
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative"> <div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
{isLoaded && ( {isLoaded && canPersistChecked && (
<div <div
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`} className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
> >
@@ -180,19 +195,28 @@ export default function Home() {
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`} key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
ref={drawioRef} ref={drawioRef}
autosave autosave
onAutoSave={handleDiagramAutoSave} onAutoSave={handleDrawioAutoSave}
onExport={handleDiagramExport} onExport={handleDiagramExport}
onLoad={handleDrawioLoad} onLoad={handleDrawioLoad}
baseUrl={drawioBaseUrl} baseUrl={drawioBaseUrl}
configuration={
canPersist
? { confirmExit: false }
: undefined
}
urlParameters={{ urlParameters={{
ui: drawioUi, ui: drawioUi,
spin: false, spin: false,
libraries: false, libraries: false,
// Disable modified tracking only when persistence is available
...(canPersist && {
modified: false,
keepmodified: false,
}),
saveAndExit: false, saveAndExit: false,
noSaveBtn: true, noSaveBtn: true,
noExitBtn: true, noExitBtn: true,
dark: dark: darkMode,
darkMode || drawioUi === "dark",
lang: currentLang, lang: currentLang,
// Enable offline mode in Electron to disable external service calls // Enable offline mode in Electron to disable external service calls
...(isElectron && { ...(isElectron && {
@@ -240,7 +264,7 @@ export default function Home() {
isVisible={isChatVisible} isVisible={isChatVisible}
onToggleVisibility={toggleChatPanel} onToggleVisibility={toggleChatPanel}
drawioUi={drawioUi} drawioUi={drawioUi}
onDrawioUiChange={handleDrawioUiChange} onToggleDrawioUi={handleDrawioUiChange}
darkMode={darkMode} darkMode={darkMode}
onToggleDarkMode={handleDarkModeChange} onToggleDarkMode={handleDarkModeChange}
isMobile={isMobile} isMobile={isMobile}

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": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",

View File

@@ -32,7 +32,6 @@ import { useSessionManager } from "@/hooks/use-session-manager"
import { useValidateDiagram } from "@/hooks/use-validate-diagram" import { useValidateDiagram } from "@/hooks/use-validate-diagram"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"
import { findCachedResponse } from "@/lib/cached-responses" import { findCachedResponse } from "@/lib/cached-responses"
import type { DrawioTheme } from "@/lib/drawio-themes"
import { formatMessage } from "@/lib/i18n/utils" import { formatMessage } from "@/lib/i18n/utils"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { sanitizeMessages } from "@/lib/session-storage" import { sanitizeMessages } from "@/lib/session-storage"
@@ -69,8 +68,8 @@ interface ChatMessage {
interface ChatPanelProps { interface ChatPanelProps {
isVisible: boolean isVisible: boolean
onToggleVisibility: () => void onToggleVisibility: () => void
drawioUi: DrawioTheme drawioUi: "min" | "sketch"
onDrawioUiChange: (theme: DrawioTheme) => void onToggleDrawioUi: () => void
darkMode: boolean darkMode: boolean
onToggleDarkMode: () => void onToggleDarkMode: () => void
isMobile?: boolean isMobile?: boolean
@@ -111,7 +110,7 @@ export default function ChatPanel({
isVisible, isVisible,
onToggleVisibility, onToggleVisibility,
drawioUi, drawioUi,
onDrawioUiChange, onToggleDrawioUi,
darkMode, darkMode,
onToggleDarkMode, onToggleDarkMode,
isMobile = false, isMobile = false,
@@ -1443,7 +1442,7 @@ export default function ChatPanel({
open={showSettingsDialog} open={showSettingsDialog}
onOpenChange={setShowSettingsDialog} onOpenChange={setShowSettingsDialog}
drawioUi={drawioUi} drawioUi={drawioUi}
onDrawioUiChange={onDrawioUiChange} onToggleDrawioUi={onToggleDrawioUi}
darkMode={darkMode} darkMode={darkMode}
onToggleDarkMode={onToggleDarkMode} onToggleDarkMode={onToggleDarkMode}
minimalStyle={minimalStyle} minimalStyle={minimalStyle}

View File

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

View File

@@ -405,7 +405,7 @@ export function ModelConfigDialog({
</span> </span>
</div> </div>
<ScrollArea className="flex-1 px-2 min-h-0"> <ScrollArea className="flex-1 px-2">
<div className="space-y-1 pb-2"> <div className="space-y-1 pb-2">
{config.providers.length === 0 ? ( {config.providers.length === 0 ? (
<div className="px-3 py-8 text-center"> <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 { Textarea } from "@/components/ui/textarea"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"
import type { DrawioTheme } from "@/lib/drawio-themes"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
import { STORAGE_KEYS } from "@/lib/storage" import { STORAGE_KEYS } from "@/lib/storage"
@@ -64,8 +63,8 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
interface SettingsDialogProps { interface SettingsDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
drawioUi: DrawioTheme drawioUi: "min" | "sketch"
onDrawioUiChange: (theme: DrawioTheme) => void onToggleDrawioUi: () => void
darkMode: boolean darkMode: boolean
onToggleDarkMode: () => void onToggleDarkMode: () => void
minimalStyle?: boolean minimalStyle?: boolean
@@ -91,7 +90,7 @@ function SettingsContent({
open, open,
onOpenChange, onOpenChange,
drawioUi, drawioUi,
onDrawioUiChange, onToggleDrawioUi,
darkMode, darkMode,
onToggleDarkMode, onToggleDarkMode,
minimalStyle = false, minimalStyle = false,
@@ -135,11 +134,8 @@ function SettingsContent({
const [isApplyingProxy, setIsApplyingProxy] = useState(false) const [isApplyingProxy, setIsApplyingProxy] = useState(false)
useEffect(() => { useEffect(() => {
// Re-fetch config whenever the dialog opens to ensure we always show // Only fetch if not cached in localStorage
// the access code input if the server requires it. This fixes the case if (getStoredAccessCodeRequired() !== null) return
// 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")) fetch(getApiEndpoint("/api/config"))
.then((res) => { .then((res) => {
@@ -155,9 +151,10 @@ function SettingsContent({
setAccessCodeRequired(required) setAccessCodeRequired(required)
}) })
.catch(() => { .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 // Detect current language from pathname
useEffect(() => { useEffect(() => {
@@ -433,40 +430,23 @@ function SettingsContent({
{/* Draw.io Style */} {/* Draw.io Style */}
<SettingItem <SettingItem
label={dict.settings.drawioStyle} label={dict.settings.drawioStyle}
description={dict.settings.drawioStyleDescription} description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
> >
<Select <Button
value={drawioUi} id="drawio-ui"
onValueChange={(v) => variant="outline"
onDrawioUiChange(v as DrawioTheme) onClick={onToggleDrawioUi}
} className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
> >
<SelectTrigger {dict.settings.switchTo}{" "}
id="drawio-ui-select" {drawioUi === "min"
aria-label={dict.settings.drawioStyle} ? dict.settings.sketch
className="w-[120px] h-9 rounded-xl" : dict.settings.minimal}
> </Button>
<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> </SettingItem>
{/* Diagram Style */} {/* Diagram Style */}

View File

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

View File

@@ -54,31 +54,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
<tr> <tr>
<td colspan="2" valign="top" align="center"> <td colspan="2" valign="top" align="center">
<strong>アニメーションTransformerコネクタ</strong><br /> <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" /> <img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
</td> </td>
</tr> </tr>
<tr> <tr>
<td width="50%" valign="top"> <td width="50%" valign="top">
<strong>RAG技術ダイアグラム</strong><br /> <strong>GCPアーキテクチャ図</strong><br />
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p> <p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/rag_prod.svg" alt="RAGアーキテクチャ図" width="480" /> <img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
</td> </td>
<td width="50%" valign="top"> <td width="50%" valign="top">
<strong>ReactとAWSによる認証</strong><br /> <strong>AWSアーキテクチャ図</strong><br />
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p> <p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/auth.svg" alt="認証アーキテクチャ図" width="480" /> <img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
</td> </td>
</tr> </tr>
<tr> <tr>
<td width="50%" valign="top"> <td width="50%" valign="top">
<strong>オープンイノベーション</strong><br /> <strong>Azureアーキテクチャ図</strong><br />
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p> <p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/inno.svg" alt="オープンイノベーション図" width="480" /> <img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
</td> </td>
<td width="50%" valign="top"> <td width="50%" valign="top">
<strong>猫のスケッチ</strong><br /> <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" /> <img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
</td> </td>
</tr> </tr>

View File

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

View File

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

View File

@@ -60,13 +60,6 @@ export function createWindow(serverUrl: string): BrowserWindow {
mainWindow.webContents.openDevTools() 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.on("closed", () => {
mainWindow = null mainWindow = null
}) })

View File

@@ -1262,6 +1262,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "glm": case "glm":
case "qwen": case "qwen":
case "qiniu": case "qiniu":
case "kimi":
case "novita": { case "novita": {
const envVar = PROVIDER_ENV_VARS[provider] const envVar = PROVIDER_ENV_VARS[provider]
if (!envVar) { if (!envVar) {
@@ -1287,23 +1288,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
break 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: default:
throw new Error( 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`, `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`,
@@ -1373,12 +1357,10 @@ export function supportsImageInput(modelId: string): boolean {
// Qwen text models (not vision variants like qwen-vl) // Qwen text models (not vision variants like qwen-vl)
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input // Qwen3.5 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 ( if (
lowerModelId.includes("qwen") && lowerModelId.includes("qwen") &&
!hasVisionIndicator && !hasVisionIndicator &&
!lowerModelId.includes("qwen3.5") && !lowerModelId.includes("qwen3.5")
!lowerModelId.includes("qvq")
) { ) {
return false 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

@@ -102,12 +102,10 @@
"theme": "Theme", "theme": "Theme",
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.", "themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
"drawioStyle": "DrawIO Style", "drawioStyle": "DrawIO Style",
"drawioStyleDescription": "Canvas style", "drawioStyleDescription": "Canvas style:",
"themeDefault": "Default", "switchTo": "Switch to",
"themeDark": "Dark", "minimal": "Minimal",
"themeMinimal": "Minimal", "sketch": "Sketch",
"themeSketch": "Sketch",
"themeSimple": "Simple",
"diagramStyle": "Diagram Style", "diagramStyle": "Diagram Style",
"diagramStyleDescription": "Toggle between minimal and styled diagram output.", "diagramStyleDescription": "Toggle between minimal and styled diagram output.",
"sendShortcut": "Send Shortcut", "sendShortcut": "Send Shortcut",

View File

@@ -102,12 +102,10 @@
"theme": "テーマ", "theme": "テーマ",
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。", "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
"drawioStyle": "DrawIO スタイル", "drawioStyle": "DrawIO スタイル",
"drawioStyleDescription": "キャンバススタイル", "drawioStyleDescription": "キャンバススタイル",
"themeDefault": "デフォルト", "switchTo": "切り替え",
"themeDark": "ダーク", "minimal": "ミニマル",
"themeMinimal": "ミニマル", "sketch": "スケッチ",
"themeSketch": "スケッチ",
"themeSimple": "シンプル",
"diagramStyle": "ダイアグラムスタイル", "diagramStyle": "ダイアグラムスタイル",
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。", "diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
"sendShortcut": "送信ショートカット", "sendShortcut": "送信ショートカット",

View File

@@ -102,12 +102,10 @@
"theme": "主題", "theme": "主題",
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。", "themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
"drawioStyle": "DrawIO 樣式", "drawioStyle": "DrawIO 樣式",
"drawioStyleDescription": "畫布樣式", "drawioStyleDescription": "畫布樣式",
"themeDefault": "預設", "switchTo": "切換到",
"themeDark": "深色", "minimal": "簡約",
"themeMinimal": "簡約", "sketch": "草圖",
"themeSketch": "草圖",
"themeSimple": "簡單",
"diagramStyle": "圖表樣式", "diagramStyle": "圖表樣式",
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。", "diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
"sendShortcut": "傳送快捷鍵", "sendShortcut": "傳送快捷鍵",

View File

@@ -102,12 +102,10 @@
"theme": "主题", "theme": "主题",
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。", "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
"drawioStyle": "DrawIO 样式", "drawioStyle": "DrawIO 样式",
"drawioStyleDescription": "画布样式", "drawioStyleDescription": "画布样式",
"themeDefault": "默认", "switchTo": "切换到",
"themeDark": "深色", "minimal": "简约",
"themeMinimal": "简约", "sketch": "草图",
"themeSketch": "草图",
"themeSimple": "简单",
"diagramStyle": "图表样式", "diagramStyle": "图表样式",
"diagramStyleDescription": "切换简约与精致图表输出模式。", "diagramStyleDescription": "切换简约与精致图表输出模式。",
"sendShortcut": "发送快捷键", "sendShortcut": "发送快捷键",

View File

@@ -58,6 +58,33 @@ interface ChatSessionDB extends DBSchema {
// Database singleton // Database singleton
let dbPromise: Promise<IDBPDatabase<ChatSessionDB>> | null = null 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>> { async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
if (!dbPromise) { if (!dbPromise) {
@@ -88,6 +115,22 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
} }
} }
}, },
terminated() {
resetDBPromise()
},
})
dbPromise
.then((db) => {
db.onversionchange = () => {
db.close()
resetDBPromise()
}
db.onclose = () => {
resetDBPromise()
}
})
.catch(() => {
resetDBPromise()
}) })
} }
return dbPromise return dbPromise
@@ -103,11 +146,24 @@ 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 // CRUD Operations
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> { export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
if (!isIndexedDBAvailable()) return [] if (!isIndexedDBAvailable()) return []
try { try {
const db = await getDB() return await withDB(async (db) => {
const tx = db.transaction(STORE_NAME, "readonly") const tx = db.transaction(STORE_NAME, "readonly")
const index = tx.store.index("by-updated") const index = tx.store.index("by-updated")
const metadata: SessionMetadata[] = [] const metadata: SessionMetadata[] = []
@@ -122,12 +178,14 @@ export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
createdAt: s.createdAt, createdAt: s.createdAt,
updatedAt: s.updatedAt, updatedAt: s.updatedAt,
messageCount: s.messages.length, messageCount: s.messages.length,
hasDiagram: !!s.diagramXml && s.diagramXml.trim().length > 0, hasDiagram:
!!s.diagramXml && s.diagramXml.trim().length > 0,
thumbnailDataUrl: s.thumbnailDataUrl, thumbnailDataUrl: s.thumbnailDataUrl,
}) })
cursor = await cursor.continue() cursor = await cursor.continue()
} }
return metadata return metadata
})
} catch (error) { } catch (error) {
console.error("Failed to get session metadata:", error) console.error("Failed to get session metadata:", error)
return [] return []
@@ -137,8 +195,9 @@ export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
export async function getSession(id: string): Promise<ChatSession | null> { export async function getSession(id: string): Promise<ChatSession | null> {
if (!isIndexedDBAvailable()) return null if (!isIndexedDBAvailable()) return null
try { try {
const db = await getDB() return await withDB(async (db) => {
return (await db.get(STORE_NAME, id)) || null return (await db.get(STORE_NAME, id)) || null
})
} catch (error) { } catch (error) {
console.error("Failed to get session:", error) console.error("Failed to get session:", error)
return null return null
@@ -148,8 +207,9 @@ export async function getSession(id: string): Promise<ChatSession | null> {
export async function saveSession(session: ChatSession): Promise<boolean> { export async function saveSession(session: ChatSession): Promise<boolean> {
if (!isIndexedDBAvailable()) return false if (!isIndexedDBAvailable()) return false
try { try {
const db = await getDB() await withDB(async (db) => {
await db.put(STORE_NAME, session) await db.put(STORE_NAME, session)
})
return true return true
} catch (error) { } catch (error) {
// Handle quota exceeded // Handle quota exceeded
@@ -161,8 +221,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
await deleteOldestSession() await deleteOldestSession()
// Retry once // Retry once
try { try {
const db = await getDB() await withDB(async (db) => {
await db.put(STORE_NAME, session) await db.put(STORE_NAME, session)
})
return true return true
} catch (retryError) { } catch (retryError) {
console.error( console.error(
@@ -181,8 +242,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
export async function deleteSession(id: string): Promise<void> { export async function deleteSession(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return if (!isIndexedDBAvailable()) return
try { try {
const db = await getDB() await withDB(async (db) => {
await db.delete(STORE_NAME, id) await db.delete(STORE_NAME, id)
})
} catch (error) { } catch (error) {
console.error("Failed to delete session:", 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> { export async function getSessionCount(): Promise<number> {
if (!isIndexedDBAvailable()) return 0 if (!isIndexedDBAvailable()) return 0
try { try {
const db = await getDB() return await withDB(async (db) => {
return await db.count(STORE_NAME) return await db.count(STORE_NAME)
})
} catch (error) { } catch (error) {
console.error("Failed to get session count:", error) console.error("Failed to get session count:", error)
return 0 return 0
@@ -202,7 +265,7 @@ export async function getSessionCount(): Promise<number> {
export async function deleteOldestSession(): Promise<void> { export async function deleteOldestSession(): Promise<void> {
if (!isIndexedDBAvailable()) return if (!isIndexedDBAvailable()) return
try { try {
const db = await getDB() await withDB(async (db) => {
const tx = db.transaction(STORE_NAME, "readwrite") const tx = db.transaction(STORE_NAME, "readwrite")
const index = tx.store.index("by-updated") const index = tx.store.index("by-updated")
const cursor = await index.openCursor() const cursor = await index.openCursor()
@@ -210,6 +273,7 @@ export async function deleteOldestSession(): Promise<void> {
await cursor.delete() await cursor.delete()
} }
await tx.done await tx.done
})
} catch (error) { } catch (error) {
console.error("Failed to delete oldest session:", error) console.error("Failed to delete oldest session:", error)
} }

View File

@@ -57,6 +57,33 @@ export function generateDefaultTitle(prompt: string): string {
// Database singleton // Database singleton
let dbPromise: Promise<IDBPDatabase<TemplateDB>> | null = null 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>> { async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
if (!dbPromise) { if (!dbPromise) {
@@ -74,6 +101,22 @@ async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
} }
} }
}, },
terminated() {
resetDBPromise()
},
})
dbPromise
.then((db) => {
db.onversionchange = () => {
db.close()
resetDBPromise()
}
db.onclose = () => {
resetDBPromise()
}
})
.catch(() => {
resetDBPromise()
}) })
} }
return dbPromise return dbPromise
@@ -94,9 +137,10 @@ export function isIndexedDBAvailable(): boolean {
export async function getAllTemplates(): Promise<Template[]> { export async function getAllTemplates(): Promise<Template[]> {
if (!isIndexedDBAvailable()) return [] if (!isIndexedDBAvailable()) return []
try { try {
const db = await getDB() return await withDB(async (db) => {
const templates = await db.getAll(STORE_NAME) const templates = await db.getAll(STORE_NAME)
return sortTemplates(templates) return sortTemplates(templates)
})
} catch (error) { } catch (error) {
console.error("Failed to get templates:", error) console.error("Failed to get templates:", error)
return [] return []
@@ -106,8 +150,9 @@ export async function getAllTemplates(): Promise<Template[]> {
export async function getTemplate(id: string): Promise<Template | null> { export async function getTemplate(id: string): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null if (!isIndexedDBAvailable()) return null
try { try {
const db = await getDB() return await withDB(async (db) => {
return (await db.get(STORE_NAME, id)) || null return (await db.get(STORE_NAME, id)) || null
})
} catch (error) { } catch (error) {
console.error("Failed to get template:", error) console.error("Failed to get template:", error)
return null return null
@@ -137,8 +182,9 @@ export async function createTemplate(
} }
try { try {
const db = await getDB() await withDB(async (db) => {
await db.put(STORE_NAME, template) await db.put(STORE_NAME, template)
})
return template return template
} catch (error) { } catch (error) {
console.error("Failed to create template:", error) console.error("Failed to create template:", error)
@@ -152,7 +198,7 @@ export async function updateTemplate(
): Promise<Template | null> { ): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null if (!isIndexedDBAvailable()) return null
try { try {
const db = await getDB() return await withDB(async (db) => {
const existing = await db.get(STORE_NAME, id) const existing = await db.get(STORE_NAME, id)
if (!existing) return null if (!existing) return null
@@ -165,6 +211,7 @@ export async function updateTemplate(
} }
await db.put(STORE_NAME, updated) await db.put(STORE_NAME, updated)
return updated return updated
})
} catch (error) { } catch (error) {
console.error("Failed to update template:", error) console.error("Failed to update template:", error)
return null return null
@@ -174,8 +221,9 @@ export async function updateTemplate(
export async function deleteTemplate(id: string): Promise<boolean> { export async function deleteTemplate(id: string): Promise<boolean> {
if (!isIndexedDBAvailable()) return false if (!isIndexedDBAvailable()) return false
try { try {
const db = await getDB() await withDB(async (db) => {
await db.delete(STORE_NAME, id) await db.delete(STORE_NAME, id)
})
return true return true
} catch (error) { } catch (error) {
console.error("Failed to delete template:", error) console.error("Failed to delete template:", error)
@@ -189,7 +237,7 @@ export async function duplicateTemplate(
): Promise<Template | null> { ): Promise<Template | null> {
if (!isIndexedDBAvailable()) return null if (!isIndexedDBAvailable()) return null
try { try {
const db = await getDB() return await withDB(async (db) => {
const existing = await db.get(STORE_NAME, id) const existing = await db.get(STORE_NAME, id)
if (!existing) return null if (!existing) return null
@@ -207,6 +255,7 @@ export async function duplicateTemplate(
} }
await db.put(STORE_NAME, duplicate) await db.put(STORE_NAME, duplicate)
return duplicate return duplicate
})
} catch (error) { } catch (error) {
console.error("Failed to duplicate template:", error) console.error("Failed to duplicate template:", error)
return null return null
@@ -218,12 +267,13 @@ export async function duplicateTemplate(
export async function incrementClickCount(id: string): Promise<void> { export async function incrementClickCount(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return if (!isIndexedDBAvailable()) return
try { try {
const db = await getDB() await withDB(async (db) => {
const template = await db.get(STORE_NAME, id) const template = await db.get(STORE_NAME, id)
if (!template) return if (!template) return
template.clickCount += 1 template.clickCount += 1
template.updatedAt = Date.now() template.updatedAt = Date.now()
await db.put(STORE_NAME, template) await db.put(STORE_NAME, template)
})
} catch (error) { } catch (error) {
console.error("Failed to increment click count:", error) console.error("Failed to increment click count:", error)
} }
@@ -232,7 +282,7 @@ export async function incrementClickCount(id: string): Promise<void> {
export async function incrementRunCount(id: string): Promise<void> { export async function incrementRunCount(id: string): Promise<void> {
if (!isIndexedDBAvailable()) return if (!isIndexedDBAvailable()) return
try { try {
const db = await getDB() await withDB(async (db) => {
const template = await db.get(STORE_NAME, id) const template = await db.get(STORE_NAME, id)
if (!template) return if (!template) return
const now = Date.now() const now = Date.now()
@@ -240,6 +290,7 @@ export async function incrementRunCount(id: string): Promise<void> {
template.lastUsedAt = now template.lastUsedAt = now
template.updatedAt = now template.updatedAt = now
await db.put(STORE_NAME, template) await db.put(STORE_NAME, template)
})
} catch (error) { } catch (error) {
console.error("Failed to increment run count:", 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, pinned: typeof t.pinned === "boolean" ? t.pinned : false,
} }
try { try {
const db = await getDB() await withDB(async (db) => {
await db.put(STORE_NAME, newTemplate) await db.put(STORE_NAME, newTemplate)
})
existingKeys.add(key) existingKeys.add(key)
imported++ imported++
} catch (error) { } catch (error) {

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

View File

@@ -1,12 +1,12 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.2.0", "version": "0.1.17",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.2.0", "version": "0.1.17",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4", "@modelcontextprotocol/sdk": "^1.0.4",
@@ -521,9 +521,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.12.2", "version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2062,9 +2062,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "4.4.1", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-ai-drawio/mcp-server", "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", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -155,22 +155,19 @@ server.registerTool(
server.registerTool( server.registerTool(
"create_new_diagram", "create_new_diagram",
{ {
description: `Create a NEW diagram from mxGraphModel XML. ONLY use this when creating a diagram from scratch. description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
⚠️ 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. CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
When to use this tool: When to use this tool:
- Creating a new diagram from scratch (no existing diagram) - Creating a new diagram from scratch
- The user explicitly asks to "start over" or "create a new diagram" - 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): When to use edit_diagram instead:
- ANY modifications to an existing diagram - Small modifications to existing diagram
- Adding/removing/moving elements - Adding/removing individual elements
- Changing labels, colors, styles, or positions - Changing labels, colors, or positions
- Restructuring or reorganizing existing content
- Adding new elements to an existing diagram
XML FORMAT - Full mxGraphModel structure: XML FORMAT - Full mxGraphModel structure:
<mxGraphModel> <mxGraphModel>

View File

@@ -12,8 +12,7 @@
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"resolveJsonModule": true, "resolveJsonModule": true
"types": ["node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "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( await expect(
frame frame
.locator('text="Diagram"') .locator('text="Diagram"')
.or(frame.locator('[title*="Diagram"]')) .or(frame.locator('[title*="Diagram"]')),
.filter({ visible: true }) ).toBeVisible({ timeout: 10000 })
.first(),
).toBeVisible({ timeout: 30000 })
}) })
test("diagram XML is rendered in iframe after generation", async ({ test("diagram XML is rendered in iframe after generation", async ({

View File

@@ -208,13 +208,6 @@ describe("supportsImageInput", () => {
expect(supportsImageInput("qwen3-vl-flash")).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", () => { it("returns false for GLM text models", () => {
expect(supportsImageInput("glm-4")).toBe(false) expect(supportsImageInput("glm-4")).toBe(false)
expect(supportsImageInput("glm-4-plus")).toBe(false) expect(supportsImageInput("glm-4-plus")).toBe(false)
@@ -245,67 +238,6 @@ vi.mock("ollama-ai-provider-v2", () => {
return { createOllama: mockCreateOllama, ollama: mockOllama } 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", () => { describe("Ollama API key security", () => {
let createOllamaMock: ReturnType<typeof vi.fn> let createOllamaMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {} const savedEnv: Record<string, string | undefined> = {}