mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
Compare commits
22 Commits
fix/stream
...
fix/parse-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9656fc0cfe | ||
|
|
1115b2d2cd | ||
|
|
08afb6dd34 | ||
|
|
c703159e00 | ||
|
|
ee75408136 | ||
|
|
2ed1a64ae3 | ||
|
|
2f2d75961d | ||
|
|
5406778dd6 | ||
|
|
bb65a8c07a | ||
|
|
4e223b6237 | ||
|
|
c60e3930a3 | ||
|
|
5c8ae4d6d7 | ||
|
|
f965f3fa2e | ||
|
|
73eefc7aa6 | ||
|
|
a8d27088ef | ||
|
|
d4454beb9a | ||
|
|
171174378c | ||
|
|
eadc2c2629 | ||
|
|
c77af86011 | ||
|
|
0cd1260172 | ||
|
|
49bfb51b10 | ||
|
|
ccd9c1f48e |
20
README.md
20
README.md
@@ -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>GCP architecture diagram</strong><br />
|
<strong>RAG Technique 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>
|
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
|
||||||
<img src="./public/gcp_demo.svg" alt="GCP Architecture Diagram" width="480" />
|
<img src="./public/rag_prod.svg" alt="RAG Architecture Diagram" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>AWS architecture diagram</strong><br />
|
<strong>Authentication using React and AWS</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>
|
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
|
||||||
<img src="./public/aws_demo.svg" alt="AWS Architecture Diagram" width="480" />
|
<img src="./public/auth.svg" alt="Authentication Architecture Diagram" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Azure architecture diagram</strong><br />
|
<strong>Open Innovation</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>
|
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
|
||||||
<img src="./public/azure_demo.svg" alt="Azure Architecture Diagram" width="480" />
|
<img src="./public/inno.svg" alt="Open Innovation Diagram" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Cat sketch prompt</strong><br />
|
<strong>Cat sketch</strong><br />
|
||||||
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
<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>
|
||||||
|
|||||||
@@ -175,92 +175,106 @@ export default function AboutCN() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* ResNet50 Architecture */}
|
||||||
<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">
|
||||||
动画Transformer连接器
|
ResNet50模型架构动画
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>提示词:</strong> 给我一个带有
|
<strong>Prompt:</strong> Give me an{" "}
|
||||||
<strong>动画连接器</strong>的Transformer架构图。
|
<strong>animated</strong> architecture diagram
|
||||||
|
of the ResNet50 model.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
|
||||||
src="/animated_connectors.svg"
|
<Image
|
||||||
alt="带动画连接器的Transformer架构"
|
src="/resnet50.svg"
|
||||||
width={480}
|
alt="ResNet50模型架构图"
|
||||||
height={360}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Diagram 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">
|
||||||
GCP架构图
|
RAG技术图
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用
|
<strong>Prompt:</strong> Generate a RAG
|
||||||
<strong>GCP图标</strong>
|
architecture diagram for{" "}
|
||||||
生成一个GCP架构图。用户连接到托管在实例上的前端。
|
<strong>chat application</strong>. Use
|
||||||
|
connected diagram for data ingestion
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/gcp_demo.svg"
|
<Image
|
||||||
alt="GCP架构图"
|
src="/rag_prod.svg"
|
||||||
width={400}
|
alt="RAG架构图"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
AWS架构图
|
React和AWS认证流程
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用
|
<strong>Prompt:</strong> Generate
|
||||||
<strong>AWS图标</strong>
|
authentication process using React with{" "}
|
||||||
生成一个AWS架构图。用户连接到托管在实例上的前端。
|
<strong>AWS</strong>. Use Serverless
|
||||||
|
architecture.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/aws_demo.svg"
|
<Image
|
||||||
alt="AWS架构图"
|
src="/auth.svg"
|
||||||
width={400}
|
alt="认证架构图"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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架构图
|
敏捷Scrum流程
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用
|
<strong>Prompt:</strong> Generate agile
|
||||||
<strong>Azure图标</strong>
|
scrum workflow diagram for software
|
||||||
生成一个Azure架构图。用户连接到托管在实例上的前端。
|
development team.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/azure_demo.svg"
|
<Image
|
||||||
alt="Azure架构图"
|
src="/agile_scrum.svg"
|
||||||
width={400}
|
alt="敏捷Scrum流程图"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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>提示词:</strong>{" "}
|
<strong>Prompt:</strong> Create
|
||||||
给我画一只可爱的猫。
|
visualization of Henry Chesbrough's
|
||||||
|
Open Innovation model.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/cat_demo.svg"
|
<Image
|
||||||
alt="猫咪绘图"
|
src="/inno.svg"
|
||||||
width={240}
|
alt="开放式创新图"
|
||||||
height={240}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -168,93 +168,106 @@ export default function AboutJA() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* ResNet50 Architecture */}
|
||||||
<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">
|
||||||
アニメーションTransformerコネクタ
|
ResNet50モデルアーキテクチャアニメーション
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>Prompt:</strong> Give me an{" "}
|
||||||
<strong>アニメーションコネクタ</strong>
|
<strong>animated</strong> architecture diagram
|
||||||
付きのTransformerアーキテクチャ図を作成してください。
|
of the ResNet50 model.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
|
||||||
src="/animated_connectors.svg"
|
<Image
|
||||||
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
|
src="/resnet50.svg"
|
||||||
width={480}
|
alt="ResNet50モデルアーキテクチャ図"
|
||||||
height={360}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Diagram 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">
|
||||||
GCPアーキテクチャ図
|
RAG技術ダイアグラム
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>Prompt:</strong> Generate a RAG
|
||||||
<strong>GCPアイコン</strong>
|
architecture diagram for{" "}
|
||||||
を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
<strong>chat application</strong>. Use
|
||||||
|
connected diagram for data ingestion
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/gcp_demo.svg"
|
<Image
|
||||||
alt="GCPアーキテクチャ図"
|
src="/rag_prod.svg"
|
||||||
width={400}
|
alt="RAGアーキテクチャ図"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
AWSアーキテクチャ図
|
ReactとAWSによる認証
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>Prompt:</strong> Generate
|
||||||
<strong>AWSアイコン</strong>
|
authentication process using React with{" "}
|
||||||
を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
<strong>AWS</strong>. Use Serverless
|
||||||
|
architecture.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/aws_demo.svg"
|
<Image
|
||||||
alt="AWSアーキテクチャ図"
|
src="/auth.svg"
|
||||||
width={400}
|
alt="認証アーキテクチャ図"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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>プロンプト:</strong>{" "}
|
<strong>Prompt:</strong> Generate agile
|
||||||
<strong>Azureアイコン</strong>
|
scrum workflow diagram for software
|
||||||
を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
development team.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/azure_demo.svg"
|
<Image
|
||||||
alt="Azureアーキテクチャ図"
|
src="/agile_scrum.svg"
|
||||||
width={400}
|
alt="アジャイルスクラム図"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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>プロンプト:</strong>{" "}
|
<strong>Prompt:</strong> Create
|
||||||
かわいい猫を描いてください。
|
visualization of Henry Chesbrough's
|
||||||
|
Open Innovation model.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/cat_demo.svg"
|
<Image
|
||||||
alt="猫の絵"
|
src="/inno.svg"
|
||||||
width={240}
|
alt="オープンイノベーション図"
|
||||||
height={240}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -182,96 +182,106 @@ export default function About() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* ResNet50 Architecture */}
|
||||||
<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 Transformer Connectors
|
Animated ResNet50 Model Architecture
|
||||||
</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 connector</strong> diagram of
|
<strong>animated</strong> architecture diagram
|
||||||
transformer's architecture.
|
of the ResNet50 model.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
|
||||||
src="/animated_connectors.svg"
|
<Image
|
||||||
alt="Transformer Architecture with Animated Connectors"
|
src="/resnet50.svg"
|
||||||
width={480}
|
alt="Architecture diagram for ResNet50 model"
|
||||||
height={360}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Diagram 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">
|
||||||
GCP Architecture Diagram
|
RAG Technique 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 GCP
|
<strong>Prompt:</strong> Generate a RAG
|
||||||
architecture diagram with{" "}
|
architecture diagram for{" "}
|
||||||
<strong>GCP icons</strong>. Users connect to
|
<strong>chat application</strong>. Use
|
||||||
a frontend hosted on an instance.
|
connected diagram for data ingestion
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/gcp_demo.svg"
|
<Image
|
||||||
alt="GCP Architecture Diagram"
|
src="/rag_prod.svg"
|
||||||
width={400}
|
alt="RAG Architecture Diagram"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
AWS Architecture Diagram
|
Authentication using React and 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 an AWS
|
<strong>Prompt:</strong> Generate
|
||||||
architecture diagram with{" "}
|
authentication process using React with{" "}
|
||||||
<strong>AWS icons</strong>. Users connect to
|
<strong>AWS</strong>. Use Serverless
|
||||||
a frontend hosted on an instance.
|
architecture.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/aws_demo.svg"
|
<Image
|
||||||
alt="AWS Architecture Diagram"
|
src="/auth.svg"
|
||||||
width={400}
|
alt="Authentication Architecture Diagram"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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 Architecture Diagram
|
Agile Scrum Process
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate an Azure
|
<strong>Prompt:</strong> Generate agile
|
||||||
architecture diagram with{" "}
|
scrum workflow diagram for software
|
||||||
<strong>Azure icons</strong>. Users connect
|
development team.
|
||||||
to a frontend hosted on an instance.
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/azure_demo.svg"
|
<Image
|
||||||
alt="Azure Architecture Diagram"
|
src="/agile_scrum.svg"
|
||||||
width={400}
|
alt="Agile Scrum Diagram"
|
||||||
height={300}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
Cat Sketch
|
Open Innovation
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Draw a cute cat for
|
<strong>Prompt:</strong> Create
|
||||||
me.
|
visualization of Henry Chesbrough's
|
||||||
|
Open Innovation model.
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||||
src="/cat_demo.svg"
|
<Image
|
||||||
alt="Cat Drawing"
|
src="/inno.svg"
|
||||||
width={240}
|
alt="Open Innovation Diagram"
|
||||||
height={240}
|
width={480}
|
||||||
className="mx-auto"
|
height={360}
|
||||||
/>
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,13 +27,11 @@ 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<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<DrawioTheme>("kennedy")
|
||||||
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",
|
||||||
)
|
)
|
||||||
@@ -56,7 +54,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const savedUi = localStorage.getItem("drawio-theme")
|
const savedUi = localStorage.getItem("drawio-theme")
|
||||||
if (savedUi === "min" || savedUi === "sketch") {
|
if (isDrawioTheme(savedUi)) {
|
||||||
setDrawioUi(savedUi)
|
setDrawioUi(savedUi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +82,6 @@ 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])
|
||||||
|
|
||||||
@@ -97,13 +90,6 @@ 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)
|
||||||
@@ -113,10 +99,9 @@ export default function Home() {
|
|||||||
resetDrawioReady()
|
resetDrawioReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrawioUiChange = () => {
|
const handleDrawioUiChange = (theme: DrawioTheme) => {
|
||||||
const newUi = drawioUi === "min" ? "sketch" : "min"
|
localStorage.setItem("drawio-theme", theme)
|
||||||
localStorage.setItem("drawio-theme", newUi)
|
setDrawioUi(theme)
|
||||||
setDrawioUi(newUi)
|
|
||||||
setIsDrawioReady(false)
|
setIsDrawioReady(false)
|
||||||
resetDrawioReady()
|
resetDrawioReady()
|
||||||
}
|
}
|
||||||
@@ -187,7 +172,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 && canPersistChecked && (
|
{isLoaded && (
|
||||||
<div
|
<div
|
||||||
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
|
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
|
||||||
>
|
>
|
||||||
@@ -195,28 +180,19 @@ export default function Home() {
|
|||||||
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
|
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
autosave
|
autosave
|
||||||
onAutoSave={handleDrawioAutoSave}
|
onAutoSave={handleDiagramAutoSave}
|
||||||
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: darkMode,
|
dark:
|
||||||
|
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 && {
|
||||||
@@ -264,7 +240,7 @@ export default function Home() {
|
|||||||
isVisible={isChatVisible}
|
isVisible={isChatVisible}
|
||||||
onToggleVisibility={toggleChatPanel}
|
onToggleVisibility={toggleChatPanel}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={handleDrawioUiChange}
|
onDrawioUiChange={handleDrawioUiChange}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={handleDarkModeChange}
|
onToggleDarkMode={handleDarkModeChange}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { extract } from "@extractus/article-extractor"
|
import { extract } from "@extractus/article-extractor"
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import TurndownService from "turndown"
|
import TurndownService from "turndown"
|
||||||
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
|
import { isPrivateUrl } from "@/lib/ssrf-protection"
|
||||||
|
|
||||||
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
||||||
const EXTRACT_TIMEOUT_MS = 15000
|
const EXTRACT_TIMEOUT_MS = 15000
|
||||||
@@ -28,8 +28,10 @@ export async function POST(req: Request) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSRF protection
|
// SSRF protection: parse-url has no use case for fetching internal
|
||||||
if (!allowPrivateUrls && isPrivateUrl(url)) {
|
// hosts, so private URLs are always rejected. ALLOW_PRIVATE_URLS only
|
||||||
|
// governs LLM provider baseUrl overrides (validate-model, chat).
|
||||||
|
if (isPrivateUrl(url)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Cannot access private/internal URLs" },
|
{ error: "Cannot access private/internal URLs" },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ export function ChatMessageDisplay({
|
|||||||
|
|
||||||
// Track previous message count to detect bulk loads vs streaming
|
// Track previous message count to detect bulk loads vs streaming
|
||||||
const prevMessageCountRef = useRef(0)
|
const prevMessageCountRef = useRef(0)
|
||||||
|
const scrollThrottleRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current && messages.length > 0) {
|
if (messagesEndRef.current && messages.length > 0) {
|
||||||
@@ -430,8 +431,17 @@ export function ChatMessageDisplay({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single message added - smooth scroll
|
// Throttle scroll during streaming to avoid layout thrashing
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
// Leading + trailing: scroll immediately, then once more after cooldown
|
||||||
|
if (!scrollThrottleRef.current) {
|
||||||
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
|
scrollThrottleRef.current = setTimeout(() => {
|
||||||
|
scrollThrottleRef.current = null
|
||||||
|
messagesEndRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}, 150)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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"
|
||||||
@@ -68,8 +69,8 @@ interface ChatMessage {
|
|||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
onToggleVisibility: () => void
|
onToggleVisibility: () => void
|
||||||
drawioUi: "min" | "sketch"
|
drawioUi: DrawioTheme
|
||||||
onToggleDrawioUi: () => void
|
onDrawioUiChange: (theme: DrawioTheme) => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
onToggleDarkMode: () => void
|
onToggleDarkMode: () => void
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
@@ -110,7 +111,7 @@ export default function ChatPanel({
|
|||||||
isVisible,
|
isVisible,
|
||||||
onToggleVisibility,
|
onToggleVisibility,
|
||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onDrawioUiChange,
|
||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
@@ -1442,7 +1443,7 @@ export default function ChatPanel({
|
|||||||
open={showSettingsDialog}
|
open={showSettingsDialog}
|
||||||
onOpenChange={setShowSettingsDialog}
|
onOpenChange={setShowSettingsDialog}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={onToggleDrawioUi}
|
onDrawioUiChange={onDrawioUiChange}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={onToggleDarkMode}
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
minimalStyle={minimalStyle}
|
minimalStyle={minimalStyle}
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ export function ChatLobby({
|
|||||||
currentInput = "",
|
currentInput = "",
|
||||||
dict,
|
dict,
|
||||||
}: ChatLobbyProps) {
|
}: ChatLobbyProps) {
|
||||||
const [templatesExpanded, setTemplatesExpanded] = useState(false)
|
const [templatesExpanded, setTemplatesExpanded] = useState(true)
|
||||||
const [examplesExpanded, setExamplesExpanded] = useState(false)
|
const [examplesExpanded, setExamplesExpanded] = useState(true)
|
||||||
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,19 +125,25 @@ export function ChatLobby({
|
|||||||
const hasHistory = sessions.length > 0
|
const hasHistory = sessions.length > 0
|
||||||
|
|
||||||
if (!hasHistory) {
|
if (!hasHistory) {
|
||||||
if (panelVisibility.myTemplates) {
|
if (!panelVisibility.myTemplates && !panelVisibility.quickExamples) {
|
||||||
return (
|
return null
|
||||||
<TemplatePanel
|
|
||||||
setInput={setInput}
|
|
||||||
onSendTemplate={onSendTemplate}
|
|
||||||
currentInput={currentInput}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (panelVisibility.quickExamples) {
|
return (
|
||||||
return <ExamplePanel setInput={setInput} setFiles={setFiles} />
|
<div className="animate-fade-in">
|
||||||
}
|
{panelVisibility.myTemplates && (
|
||||||
return null
|
<TemplatePanel
|
||||||
|
setInput={setInput}
|
||||||
|
onSendTemplate={onSendTemplate}
|
||||||
|
currentInput={currentInput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{panelVisibility.quickExamples && (
|
||||||
|
<div className={panelVisibility.myTemplates ? "mt-6" : ""}>
|
||||||
|
<ExamplePanel setInput={setInput} setFiles={setFiles} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show history + collapsible examples when there are sessions
|
// Show history + collapsible examples when there are sessions
|
||||||
|
|||||||
@@ -195,7 +195,22 @@ export function ToolCallCard({
|
|||||||
{input && isExpanded && (
|
{input && isExpanded && (
|
||||||
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
||||||
{typeof input === "object" && input.xml ? (
|
{typeof input === "object" && input.xml ? (
|
||||||
<CodeBlock code={input.xml} language="xml" />
|
state === "input-streaming" ||
|
||||||
|
state === "input-available" ? (
|
||||||
|
<pre
|
||||||
|
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all whitespace-pre-wrap"
|
||||||
|
style={{
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-mono), ui-monospace, monospace",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{input.xml}
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
<CodeBlock code={input.xml} language="xml" />
|
||||||
|
)
|
||||||
) : typeof input === "object" &&
|
) : typeof input === "object" &&
|
||||||
input.operations &&
|
input.operations &&
|
||||||
Array.isArray(input.operations) ? (
|
Array.isArray(input.operations) ? (
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ export function ModelConfigDialog({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 px-2">
|
<ScrollArea className="flex-1 px-2 min-h-0">
|
||||||
<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">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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"
|
||||||
|
|
||||||
@@ -63,8 +64,8 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
|
|||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
drawioUi: "min" | "sketch"
|
drawioUi: DrawioTheme
|
||||||
onToggleDrawioUi: () => void
|
onDrawioUiChange: (theme: DrawioTheme) => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
onToggleDarkMode: () => void
|
onToggleDarkMode: () => void
|
||||||
minimalStyle?: boolean
|
minimalStyle?: boolean
|
||||||
@@ -90,7 +91,7 @@ function SettingsContent({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onDrawioUiChange,
|
||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
minimalStyle = false,
|
minimalStyle = false,
|
||||||
@@ -134,8 +135,11 @@ function SettingsContent({
|
|||||||
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch if not cached in localStorage
|
// Re-fetch config whenever the dialog opens to ensure we always show
|
||||||
if (getStoredAccessCodeRequired() !== null) return
|
// the access code input if the server requires it. This fixes the case
|
||||||
|
// where a stale localStorage cache (from before ACCESS_CODE_LIST was
|
||||||
|
// configured) would hide the access code input.
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
fetch(getApiEndpoint("/api/config"))
|
fetch(getApiEndpoint("/api/config"))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -151,10 +155,9 @@ function SettingsContent({
|
|||||||
setAccessCodeRequired(required)
|
setAccessCodeRequired(required)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Don't cache on error - allow retry on next mount
|
// Keep existing cached value on error
|
||||||
setAccessCodeRequired(false)
|
|
||||||
})
|
})
|
||||||
}, [])
|
}, [open])
|
||||||
|
|
||||||
// Detect current language from pathname
|
// Detect current language from pathname
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -430,23 +433,40 @@ 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
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Select
|
||||||
id="drawio-ui"
|
value={drawioUi}
|
||||||
variant="outline"
|
onValueChange={(v) =>
|
||||||
onClick={onToggleDrawioUi}
|
onDrawioUiChange(v as DrawioTheme)
|
||||||
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
}
|
||||||
>
|
>
|
||||||
{dict.settings.switchTo}{" "}
|
<SelectTrigger
|
||||||
{drawioUi === "min"
|
id="drawio-ui-select"
|
||||||
? dict.settings.sketch
|
aria-label={dict.settings.drawioStyle}
|
||||||
: dict.settings.minimal}
|
className="w-[120px] h-9 rounded-xl"
|
||||||
</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 */}
|
||||||
|
|||||||
@@ -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>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
|
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
||||||
<img src="../../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
|
<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>GCP架构图</strong><br />
|
<strong>RAG技术图</strong><br />
|
||||||
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
|
||||||
<img src="../../public/gcp_demo.svg" alt="GCP架构图" width="480" />
|
<img src="../../public/rag_prod.svg" alt="RAG架构图" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>AWS架构图</strong><br />
|
<strong>React和AWS认证流程</strong><br />
|
||||||
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
|
||||||
<img src="../../public/aws_demo.svg" alt="AWS架构图" width="480" />
|
<img src="../../public/auth.svg" alt="认证架构图" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Azure架构图</strong><br />
|
<strong>开放式创新</strong><br />
|
||||||
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
|
||||||
<img src="../../public/azure_demo.svg" alt="Azure架构图" width="480" />
|
<img src="../../public/inno.svg" alt="开放式创新图" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>猫咪素描</strong><br />
|
<strong>猫咪素描</strong><br />
|
||||||
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
|
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||||
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
|
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
||||||
<img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
|
<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>GCPアーキテクチャ図</strong><br />
|
<strong>RAG技術ダイアグラム</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
|
||||||
<img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
|
<img src="../../public/rag_prod.svg" alt="RAGアーキテクチャ図" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>AWSアーキテクチャ図</strong><br />
|
<strong>ReactとAWSによる認証</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
|
||||||
<img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
|
<img src="../../public/auth.svg" alt="認証アーキテクチャ図" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Azureアーキテクチャ図</strong><br />
|
<strong>オープンイノベーション</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
|
||||||
<img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
|
<img src="../../public/inno.svg" alt="オープンイノベーション図" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>猫のスケッチ</strong><br />
|
<strong>猫のスケッチ</strong><br />
|
||||||
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
|
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||||
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
|
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ linux:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
|
- target: rpm
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- arm64
|
||||||
|
|
||||||
# Publish configuration (optional)
|
# Publish configuration (optional)
|
||||||
publish:
|
publish:
|
||||||
|
|||||||
2
electron/electron.d.ts
vendored
2
electron/electron.d.ts
vendored
@@ -101,8 +101,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ConfigPreset,
|
|
||||||
ApplyPresetResult,
|
ApplyPresetResult,
|
||||||
|
ConfigPreset,
|
||||||
ProxyConfig,
|
ProxyConfig,
|
||||||
SetProxyResult,
|
SetProxyResult,
|
||||||
SetUserLocaleResult,
|
SetUserLocaleResult,
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ 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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1262,7 +1262,6 @@ 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) {
|
||||||
@@ -1288,6 +1287,23 @@ 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`,
|
||||||
@@ -1357,10 +1373,12 @@ export function supportsImageInput(modelId: string): boolean {
|
|||||||
|
|
||||||
// Qwen text models (not vision variants like qwen-vl)
|
// Qwen text models (not vision variants like qwen-vl)
|
||||||
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input
|
// Qwen3.5 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
|
||||||
}
|
}
|
||||||
|
|||||||
17
lib/drawio-themes.ts
Normal file
17
lib/drawio-themes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const DRAWIO_THEMES = [
|
||||||
|
"kennedy",
|
||||||
|
"atlas",
|
||||||
|
"dark",
|
||||||
|
"min",
|
||||||
|
"sketch",
|
||||||
|
"simple",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type DrawioTheme = (typeof DRAWIO_THEMES)[number]
|
||||||
|
|
||||||
|
export function isDrawioTheme(value: unknown): value is DrawioTheme {
|
||||||
|
return (
|
||||||
|
typeof value === "string" &&
|
||||||
|
(DRAWIO_THEMES as readonly string[]).includes(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -102,10 +102,12 @@
|
|||||||
"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",
|
||||||
"switchTo": "Switch to",
|
"themeDefault": "Default",
|
||||||
"minimal": "Minimal",
|
"themeDark": "Dark",
|
||||||
"sketch": "Sketch",
|
"themeMinimal": "Minimal",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
||||||
"drawioStyle": "DrawIO スタイル",
|
"drawioStyle": "DrawIO スタイル",
|
||||||
"drawioStyleDescription": "キャンバススタイル:",
|
"drawioStyleDescription": "キャンバススタイル",
|
||||||
"switchTo": "切り替え",
|
"themeDefault": "デフォルト",
|
||||||
"minimal": "ミニマル",
|
"themeDark": "ダーク",
|
||||||
"sketch": "スケッチ",
|
"themeMinimal": "ミニマル",
|
||||||
|
"themeSketch": "スケッチ",
|
||||||
|
"themeSimple": "シンプル",
|
||||||
"diagramStyle": "ダイアグラムスタイル",
|
"diagramStyle": "ダイアグラムスタイル",
|
||||||
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
||||||
"sendShortcut": "送信ショートカット",
|
"sendShortcut": "送信ショートカット",
|
||||||
|
|||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "主題",
|
"theme": "主題",
|
||||||
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
|
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
|
||||||
"drawioStyle": "DrawIO 樣式",
|
"drawioStyle": "DrawIO 樣式",
|
||||||
"drawioStyleDescription": "畫布樣式:",
|
"drawioStyleDescription": "畫布樣式",
|
||||||
"switchTo": "切換到",
|
"themeDefault": "預設",
|
||||||
"minimal": "簡約",
|
"themeDark": "深色",
|
||||||
"sketch": "草圖",
|
"themeMinimal": "簡約",
|
||||||
|
"themeSketch": "草圖",
|
||||||
|
"themeSimple": "簡單",
|
||||||
"diagramStyle": "圖表樣式",
|
"diagramStyle": "圖表樣式",
|
||||||
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
|
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
|
||||||
"sendShortcut": "傳送快捷鍵",
|
"sendShortcut": "傳送快捷鍵",
|
||||||
|
|||||||
@@ -102,10 +102,12 @@
|
|||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
||||||
"drawioStyle": "DrawIO 样式",
|
"drawioStyle": "DrawIO 样式",
|
||||||
"drawioStyleDescription": "画布样式:",
|
"drawioStyleDescription": "画布样式",
|
||||||
"switchTo": "切换到",
|
"themeDefault": "默认",
|
||||||
"minimal": "简约",
|
"themeDark": "深色",
|
||||||
"sketch": "草图",
|
"themeMinimal": "简约",
|
||||||
|
"themeSketch": "草图",
|
||||||
|
"themeSimple": "简单",
|
||||||
"diagramStyle": "图表样式",
|
"diagramStyle": "图表样式",
|
||||||
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
||||||
"sendShortcut": "发送快捷键",
|
"sendShortcut": "发送快捷键",
|
||||||
|
|||||||
@@ -58,33 +58,6 @@ 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) {
|
||||||
@@ -115,23 +88,7 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
terminated() {
|
|
||||||
resetDBPromise()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
dbPromise
|
|
||||||
.then((db) => {
|
|
||||||
db.onversionchange = () => {
|
|
||||||
db.close()
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
db.onclose = () => {
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
resetDBPromise()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return dbPromise
|
return dbPromise
|
||||||
}
|
}
|
||||||
@@ -146,46 +103,31 @@ export function isIndexedDBAvailable(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if IndexedDB is actually usable (not just present).
|
|
||||||
// Note: Do NOT close the db here - getDB() returns a shared singleton connection
|
|
||||||
// that other code depends on.
|
|
||||||
export async function isIndexedDBUsable(): Promise<boolean> {
|
|
||||||
if (!isIndexedDBAvailable()) return false
|
|
||||||
try {
|
|
||||||
await getDB()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRUD Operations
|
// CRUD Operations
|
||||||
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
||||||
if (!isIndexedDBAvailable()) return []
|
if (!isIndexedDBAvailable()) return []
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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[] = []
|
||||||
|
|
||||||
// Use cursor to read only metadata fields (avoids loading full messages/XML)
|
// Use cursor to read only metadata fields (avoids loading full messages/XML)
|
||||||
let cursor = await index.openCursor(null, "prev") // newest first
|
let cursor = await index.openCursor(null, "prev") // newest first
|
||||||
while (cursor) {
|
while (cursor) {
|
||||||
const s = cursor.value
|
const s = cursor.value
|
||||||
metadata.push({
|
metadata.push({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title,
|
title: s.title,
|
||||||
createdAt: s.createdAt,
|
createdAt: s.createdAt,
|
||||||
updatedAt: s.updatedAt,
|
updatedAt: s.updatedAt,
|
||||||
messageCount: s.messages.length,
|
messageCount: s.messages.length,
|
||||||
hasDiagram:
|
hasDiagram: !!s.diagramXml && s.diagramXml.trim().length > 0,
|
||||||
!!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 []
|
||||||
@@ -195,9 +137,8 @@ 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 {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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
|
||||||
@@ -207,9 +148,8 @@ 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 {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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
|
||||||
@@ -221,9 +161,8 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
|
|||||||
await deleteOldestSession()
|
await deleteOldestSession()
|
||||||
// Retry once
|
// Retry once
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.put(STORE_NAME, session)
|
await db.put(STORE_NAME, session)
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -242,9 +181,8 @@ 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 {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -253,9 +191,8 @@ 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 {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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
|
||||||
@@ -265,15 +202,14 @@ 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 {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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()
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
export function isPrivateUrl(urlString: string): boolean {
|
export function isPrivateUrl(urlString: string): boolean {
|
||||||
try {
|
try {
|
||||||
const url = new URL(urlString)
|
const url = new URL(urlString)
|
||||||
const hostname = url.hostname.toLowerCase()
|
// Strip a trailing dot so FQDN forms like "localhost." (which still
|
||||||
|
// resolve to 127.0.0.1) cannot bypass the equality checks below.
|
||||||
|
const hostname = url.hostname.toLowerCase().replace(/\.$/, "")
|
||||||
|
|
||||||
// Block localhost
|
// Block localhost
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -57,33 +57,6 @@ 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) {
|
||||||
@@ -101,23 +74,7 @@ async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
terminated() {
|
|
||||||
resetDBPromise()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
dbPromise
|
|
||||||
.then((db) => {
|
|
||||||
db.onversionchange = () => {
|
|
||||||
db.close()
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
db.onclose = () => {
|
|
||||||
resetDBPromise()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
resetDBPromise()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return dbPromise
|
return dbPromise
|
||||||
}
|
}
|
||||||
@@ -137,10 +94,9 @@ export function isIndexedDBAvailable(): boolean {
|
|||||||
export async function getAllTemplates(): Promise<Template[]> {
|
export async function getAllTemplates(): Promise<Template[]> {
|
||||||
if (!isIndexedDBAvailable()) return []
|
if (!isIndexedDBAvailable()) return []
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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 []
|
||||||
@@ -150,9 +106,8 @@ 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 {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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
|
||||||
@@ -182,9 +137,8 @@ export async function createTemplate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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)
|
||||||
@@ -198,20 +152,19 @@ export async function updateTemplate(
|
|||||||
): Promise<Template | null> {
|
): Promise<Template | null> {
|
||||||
if (!isIndexedDBAvailable()) return null
|
if (!isIndexedDBAvailable()) return null
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const existing = await db.get(STORE_NAME, id)
|
const existing = await db.get(STORE_NAME, id)
|
||||||
if (!existing) return null
|
if (!existing) return null
|
||||||
|
|
||||||
const updated: Template = {
|
const updated: Template = {
|
||||||
...existing,
|
...existing,
|
||||||
...updates,
|
...updates,
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
createdAt: existing.createdAt,
|
createdAt: existing.createdAt,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -221,9 +174,8 @@ 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 {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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)
|
||||||
@@ -237,25 +189,24 @@ export async function duplicateTemplate(
|
|||||||
): Promise<Template | null> {
|
): Promise<Template | null> {
|
||||||
if (!isIndexedDBAvailable()) return null
|
if (!isIndexedDBAvailable()) return null
|
||||||
try {
|
try {
|
||||||
return await withDB(async (db) => {
|
const db = await getDB()
|
||||||
const existing = await db.get(STORE_NAME, id)
|
const existing = await db.get(STORE_NAME, id)
|
||||||
if (!existing) return null
|
if (!existing) return null
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const duplicate: Template = {
|
const duplicate: Template = {
|
||||||
...existing,
|
...existing,
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
title: `${existing.title} ${copySuffix}`,
|
title: `${existing.title} ${copySuffix}`,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
clickCount: 0,
|
clickCount: 0,
|
||||||
runCount: 0,
|
runCount: 0,
|
||||||
lastUsedAt: 0,
|
lastUsedAt: 0,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -267,13 +218,12 @@ 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 {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -282,15 +232,14 @@ 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 {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
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()
|
||||||
template.runCount += 1
|
template.runCount += 1
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -423,9 +372,8 @@ export async function importTemplates(
|
|||||||
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
|
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await withDB(async (db) => {
|
const db = await getDB()
|
||||||
await db.put(STORE_NAME, newTemplate)
|
await db.put(STORE_NAME, newTemplate)
|
||||||
})
|
|
||||||
existingKeys.add(key)
|
existingKeys.add(key)
|
||||||
imported++
|
imported++
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
2478
package-lock.json
generated
2478
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.14",
|
"version": "0.4.16",
|
||||||
"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": "^1.5.4",
|
"@openrouter/ai-sdk-provider": "^2.0.0",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@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": "^2.0.0",
|
"ollama-ai-provider-v2": "^3.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.10",
|
"@biomejs/biome": "2.4.13",
|
||||||
"@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.27.2",
|
"esbuild": "^0.28.0",
|
||||||
"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",
|
||||||
|
|||||||
16
packages/mcp-server/package-lock.json
generated
16
packages/mcp-server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.17",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.17",
|
"version": "0.2.0",
|
||||||
"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.0",
|
"version": "24.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
||||||
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
|
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2062,9 +2062,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.19",
|
"version": "0.2.0",
|
||||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
"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",
|
||||||
|
|||||||
@@ -155,19 +155,22 @@ server.registerTool(
|
|||||||
server.registerTool(
|
server.registerTool(
|
||||||
"create_new_diagram",
|
"create_new_diagram",
|
||||||
{
|
{
|
||||||
description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
|
description: `Create a NEW diagram from mxGraphModel XML. ONLY use this when creating a diagram from scratch.
|
||||||
|
|
||||||
|
⚠️ DO NOT use this tool to modify an existing diagram — it will DESTROY all existing content and user changes. Use edit_diagram instead for ANY modifications to an existing diagram.
|
||||||
|
|
||||||
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
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
|
- Creating a new diagram from scratch (no existing diagram)
|
||||||
- Replacing the current diagram with a completely different one
|
- The user explicitly asks to "start over" or "create a new diagram"
|
||||||
- Major structural changes that require regenerating the diagram
|
|
||||||
|
|
||||||
When to use edit_diagram instead:
|
When to use edit_diagram instead (ALWAYS prefer edit_diagram if a diagram already exists):
|
||||||
- Small modifications to existing diagram
|
- ANY modifications to an existing diagram
|
||||||
- Adding/removing individual elements
|
- Adding/removing/moving elements
|
||||||
- Changing labels, colors, or positions
|
- Changing labels, colors, styles, 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>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"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"]
|
||||||
|
|||||||
1
public/agile_scrum.svg
Normal file
1
public/agile_scrum.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 30 KiB |
1
public/auth.svg
Normal file
1
public/auth.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
1
public/inno.svg
Normal file
1
public/inno.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 27 KiB |
1
public/rag_prod.svg
Normal file
1
public/rag_prod.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 35 KiB |
1
public/resnet50.svg
Normal file
1
public/resnet50.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 39 KiB |
@@ -35,8 +35,10 @@ 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"]'))
|
||||||
).toBeVisible({ timeout: 10000 })
|
.filter({ visible: true })
|
||||||
|
.first(),
|
||||||
|
).toBeVisible({ timeout: 30000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("diagram XML is rendered in iframe after generation", async ({
|
test("diagram XML is rendered in iframe after generation", async ({
|
||||||
|
|||||||
@@ -208,6 +208,13 @@ 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)
|
||||||
@@ -238,6 +245,67 @@ 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> = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user