mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
Compare commits
2 Commits
v0.4.16
...
feat/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2158d26bbb | ||
|
|
e27afe6f0d |
13
.github/workflows/electron-release.yml
vendored
13
.github/workflows/electron-release.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Download draw.io static files for offline use
|
||||
run: |
|
||||
rm -rf public/drawio
|
||||
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
|
||||
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
|
||||
mkdir -p public/drawio
|
||||
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
|
||||
rm -rf public/drawio/WEB-INF
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf public/drawio
|
||||
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
|
||||
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
|
||||
mkdir -p public/drawio
|
||||
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
|
||||
rm -rf public/drawio/WEB-INF
|
||||
@@ -105,9 +105,14 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: release-signed
|
||||
|
||||
- name: Upload signed artifacts to release
|
||||
- name: Fix latest.yml with signed exe hashes
|
||||
run: node scripts/fix-latest-yml.mjs
|
||||
|
||||
- name: Upload signed artifacts and update metadata to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release-signed/*.exe
|
||||
files: |
|
||||
release-signed/*.exe
|
||||
release/latest*.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
26
README.md
26
README.md
@@ -31,7 +31,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Examples](#examples)
|
||||
- [Features](#features)
|
||||
- [MCP Server](#mcp-server)
|
||||
- [MCP Server (Preview)](#mcp-server-preview)
|
||||
- [Claude Code CLI](#claude-code-cli)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Try it Online](#try-it-online)
|
||||
@@ -63,24 +63,24 @@ Here are some example prompts and their generated diagrams:
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>RAG Technique Diagram</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
|
||||
<img src="./public/rag_prod.svg" alt="RAG Architecture Diagram" width="480" />
|
||||
<strong>GCP architecture diagram</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate a GCP architecture diagram with **GCP icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
|
||||
<img src="./public/gcp_demo.svg" alt="GCP Architecture Diagram" width="480" />
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>Authentication using React and AWS</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
|
||||
<img src="./public/auth.svg" alt="Authentication Architecture Diagram" width="480" />
|
||||
<strong>AWS architecture diagram</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate a AWS architecture diagram with **AWS icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
|
||||
<img src="./public/aws_demo.svg" alt="AWS Architecture Diagram" width="480" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>Open Innovation</strong><br />
|
||||
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
|
||||
<img src="./public/inno.svg" alt="Open Innovation Diagram" width="480" />
|
||||
<strong>Azure architecture diagram</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate a Azure architecture diagram with **Azure icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
|
||||
<img src="./public/azure_demo.svg" alt="Azure Architecture Diagram" width="480" />
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>Cat sketch</strong><br />
|
||||
<strong>Cat sketch prompt</strong><br />
|
||||
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||
<img src="./public/cat_demo.svg" alt="Cat Drawing" width="240" />
|
||||
</td>
|
||||
@@ -99,7 +99,9 @@ Here are some example prompts and their generated diagrams:
|
||||
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
||||
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
||||
|
||||
## MCP Server
|
||||
## MCP Server (Preview)
|
||||
|
||||
> **Preview Feature**: This feature is experimental and may not be stable.
|
||||
|
||||
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
|
||||
|
||||
|
||||
@@ -175,106 +175,92 @@ export default function AboutCN() {
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* ResNet50 Architecture */}
|
||||
{/* Animated Transformer */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
ResNet50模型架构动画
|
||||
动画Transformer连接器
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
<strong>Prompt:</strong> Give me an{" "}
|
||||
<strong>animated</strong> architecture diagram
|
||||
of the ResNet50 model.
|
||||
<strong>提示词:</strong> 给我一个带有
|
||||
<strong>动画连接器</strong>的Transformer架构图。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
|
||||
<Image
|
||||
src="/resnet50.svg"
|
||||
alt="ResNet50模型架构图"
|
||||
width={480}
|
||||
height={360}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/animated_connectors.svg"
|
||||
alt="带动画连接器的Transformer架构"
|
||||
width={480}
|
||||
height={360}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diagram Grid */}
|
||||
{/* Cloud Architecture Grid */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
RAG技术图
|
||||
GCP架构图
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate a RAG
|
||||
architecture diagram for{" "}
|
||||
<strong>chat application</strong>. Use
|
||||
connected diagram for data ingestion
|
||||
<strong>提示词:</strong> 使用
|
||||
<strong>GCP图标</strong>
|
||||
生成一个GCP架构图。用户连接到托管在实例上的前端。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/rag_prod.svg"
|
||||
alt="RAG架构图"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/gcp_demo.svg"
|
||||
alt="GCP架构图"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
React和AWS认证流程
|
||||
AWS架构图
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate
|
||||
authentication process using React with{" "}
|
||||
<strong>AWS</strong>. Use Serverless
|
||||
architecture.
|
||||
<strong>提示词:</strong> 使用
|
||||
<strong>AWS图标</strong>
|
||||
生成一个AWS架构图。用户连接到托管在实例上的前端。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/auth.svg"
|
||||
alt="认证架构图"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/aws_demo.svg"
|
||||
alt="AWS架构图"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
敏捷Scrum流程
|
||||
Azure架构图
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate agile
|
||||
scrum workflow diagram for software
|
||||
development team.
|
||||
<strong>提示词:</strong> 使用
|
||||
<strong>Azure图标</strong>
|
||||
生成一个Azure架构图。用户连接到托管在实例上的前端。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/agile_scrum.svg"
|
||||
alt="敏捷Scrum流程图"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/azure_demo.svg"
|
||||
alt="Azure架构图"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
开放式创新
|
||||
猫咪素描
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Create
|
||||
visualization of Henry Chesbrough's
|
||||
Open Innovation model.
|
||||
<strong>提示词:</strong>{" "}
|
||||
给我画一只可爱的猫。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/inno.svg"
|
||||
alt="开放式创新图"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/cat_demo.svg"
|
||||
alt="猫咪绘图"
|
||||
width={240}
|
||||
height={240}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,106 +168,93 @@ export default function AboutJA() {
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* ResNet50 Architecture */}
|
||||
{/* Animated Transformer */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
ResNet50モデルアーキテクチャアニメーション
|
||||
アニメーションTransformerコネクタ
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
<strong>Prompt:</strong> Give me an{" "}
|
||||
<strong>animated</strong> architecture diagram
|
||||
of the ResNet50 model.
|
||||
<strong>プロンプト:</strong>{" "}
|
||||
<strong>アニメーションコネクタ</strong>
|
||||
付きのTransformerアーキテクチャ図を作成してください。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
|
||||
<Image
|
||||
src="/resnet50.svg"
|
||||
alt="ResNet50モデルアーキテクチャ図"
|
||||
width={480}
|
||||
height={360}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/animated_connectors.svg"
|
||||
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
|
||||
width={480}
|
||||
height={360}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diagram Grid */}
|
||||
{/* Cloud Architecture Grid */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
RAG技術ダイアグラム
|
||||
GCPアーキテクチャ図
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate a RAG
|
||||
architecture diagram for{" "}
|
||||
<strong>chat application</strong>. Use
|
||||
connected diagram for data ingestion
|
||||
<strong>プロンプト:</strong>{" "}
|
||||
<strong>GCPアイコン</strong>
|
||||
を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/rag_prod.svg"
|
||||
alt="RAGアーキテクチャ図"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/gcp_demo.svg"
|
||||
alt="GCPアーキテクチャ図"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
ReactとAWSによる認証
|
||||
AWSアーキテクチャ図
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate
|
||||
authentication process using React with{" "}
|
||||
<strong>AWS</strong>. Use Serverless
|
||||
architecture.
|
||||
<strong>プロンプト:</strong>{" "}
|
||||
<strong>AWSアイコン</strong>
|
||||
を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/auth.svg"
|
||||
alt="認証アーキテクチャ図"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/aws_demo.svg"
|
||||
alt="AWSアーキテクチャ図"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
アジャイルスクラムプロセス
|
||||
Azureアーキテクチャ図
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate agile
|
||||
scrum workflow diagram for software
|
||||
development team.
|
||||
<strong>プロンプト:</strong>{" "}
|
||||
<strong>Azureアイコン</strong>
|
||||
を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/agile_scrum.svg"
|
||||
alt="アジャイルスクラム図"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/azure_demo.svg"
|
||||
alt="Azureアーキテクチャ図"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
オープンイノベーション
|
||||
猫のスケッチ
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Create
|
||||
visualization of Henry Chesbrough's
|
||||
Open Innovation model.
|
||||
<strong>プロンプト:</strong>{" "}
|
||||
かわいい猫を描いてください。
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/inno.svg"
|
||||
alt="オープンイノベーション図"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/cat_demo.svg"
|
||||
alt="猫の絵"
|
||||
width={240}
|
||||
height={240}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,106 +182,96 @@ export default function About() {
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* ResNet50 Architecture */}
|
||||
{/* Animated Transformer */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Animated ResNet50 Model Architecture
|
||||
Animated Transformer Connectors
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
<strong>Prompt:</strong> Give me an{" "}
|
||||
<strong>animated</strong> architecture diagram
|
||||
of the ResNet50 model.
|
||||
<strong>animated connector</strong> diagram of
|
||||
transformer's architecture.
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 inline-block">
|
||||
<Image
|
||||
src="/resnet50.svg"
|
||||
alt="Architecture diagram for ResNet50 model"
|
||||
width={480}
|
||||
height={360}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/animated_connectors.svg"
|
||||
alt="Transformer Architecture with Animated Connectors"
|
||||
width={480}
|
||||
height={360}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diagram Grid */}
|
||||
{/* Cloud Architecture Grid */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
RAG Technique Diagram
|
||||
GCP Architecture Diagram
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate a RAG
|
||||
architecture diagram for{" "}
|
||||
<strong>chat application</strong>. Use
|
||||
connected diagram for data ingestion
|
||||
<strong>Prompt:</strong> Generate a GCP
|
||||
architecture diagram with{" "}
|
||||
<strong>GCP icons</strong>. Users connect to
|
||||
a frontend hosted on an instance.
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/rag_prod.svg"
|
||||
alt="RAG Architecture Diagram"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/gcp_demo.svg"
|
||||
alt="GCP Architecture Diagram"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Authentication using React and AWS
|
||||
AWS Architecture Diagram
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate
|
||||
authentication process using React with{" "}
|
||||
<strong>AWS</strong>. Use Serverless
|
||||
architecture.
|
||||
<strong>Prompt:</strong> Generate an AWS
|
||||
architecture diagram with{" "}
|
||||
<strong>AWS icons</strong>. Users connect to
|
||||
a frontend hosted on an instance.
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/auth.svg"
|
||||
alt="Authentication Architecture Diagram"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/aws_demo.svg"
|
||||
alt="AWS Architecture Diagram"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Agile Scrum Process
|
||||
Azure Architecture Diagram
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Generate agile
|
||||
scrum workflow diagram for software
|
||||
development team.
|
||||
<strong>Prompt:</strong> Generate an Azure
|
||||
architecture diagram with{" "}
|
||||
<strong>Azure icons</strong>. Users connect
|
||||
to a frontend hosted on an instance.
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/agile_scrum.svg"
|
||||
alt="Agile Scrum Diagram"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/azure_demo.svg"
|
||||
alt="Azure Architecture Diagram"
|
||||
width={400}
|
||||
height={300}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Open Innovation
|
||||
Cat Sketch
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
<strong>Prompt:</strong> Create
|
||||
visualization of Henry Chesbrough's
|
||||
Open Innovation model.
|
||||
<strong>Prompt:</strong> Draw a cute cat for
|
||||
me.
|
||||
</p>
|
||||
<div className="bg-neutral-950 rounded-lg p-4 flex items-center justify-center w-full h-[400px]">
|
||||
<Image
|
||||
src="/inno.svg"
|
||||
alt="Open Innovation Diagram"
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/cat_demo.svg"
|
||||
alt="Cat Drawing"
|
||||
width={240}
|
||||
height={240}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { type DrawioTheme, isDrawioTheme } from "@/lib/drawio-themes"
|
||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||
import { isIndexedDBUsable } from "@/lib/session-storage"
|
||||
|
||||
export default function Home() {
|
||||
const {
|
||||
@@ -27,11 +27,13 @@ export default function Home() {
|
||||
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||
const [drawioUi, setDrawioUi] = useState<DrawioTheme>("kennedy")
|
||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||
const [darkMode, setDarkMode] = useState(false)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||
const [isElectron, setIsElectron] = useState(false)
|
||||
const [canPersist, setCanPersist] = useState(false)
|
||||
const [canPersistChecked, setCanPersistChecked] = useState(false)
|
||||
const [drawioBaseUrl, setDrawioBaseUrl] = useState(
|
||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net",
|
||||
)
|
||||
@@ -54,7 +56,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
const savedUi = localStorage.getItem("drawio-theme")
|
||||
if (isDrawioTheme(savedUi)) {
|
||||
if (savedUi === "min" || savedUi === "sketch") {
|
||||
setDrawioUi(savedUi)
|
||||
}
|
||||
|
||||
@@ -82,6 +84,11 @@ export default function Home() {
|
||||
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
const usable = await isIndexedDBUsable()
|
||||
setCanPersist(usable)
|
||||
setCanPersistChecked(true)
|
||||
})()
|
||||
setIsLoaded(true)
|
||||
}, [pathname, router])
|
||||
|
||||
@@ -90,6 +97,17 @@ export default function Home() {
|
||||
onDrawioLoad()
|
||||
}, [onDrawioLoad])
|
||||
|
||||
const handleDrawioAutoSave = useCallback(
|
||||
(data: { xml?: string }) => {
|
||||
handleDiagramAutoSave(data)
|
||||
// Only suppress modified state when persistence is available
|
||||
if (canPersist) {
|
||||
drawioRef.current?.status({ message: "", modified: false })
|
||||
}
|
||||
},
|
||||
[canPersist, drawioRef, handleDiagramAutoSave],
|
||||
)
|
||||
|
||||
const handleDarkModeChange = () => {
|
||||
const newValue = !darkMode
|
||||
setDarkMode(newValue)
|
||||
@@ -99,9 +117,10 @@ export default function Home() {
|
||||
resetDrawioReady()
|
||||
}
|
||||
|
||||
const handleDrawioUiChange = (theme: DrawioTheme) => {
|
||||
localStorage.setItem("drawio-theme", theme)
|
||||
setDrawioUi(theme)
|
||||
const handleDrawioUiChange = () => {
|
||||
const newUi = drawioUi === "min" ? "sketch" : "min"
|
||||
localStorage.setItem("drawio-theme", newUi)
|
||||
setDrawioUi(newUi)
|
||||
setIsDrawioReady(false)
|
||||
resetDrawioReady()
|
||||
}
|
||||
@@ -172,7 +191,7 @@ export default function Home() {
|
||||
}`}
|
||||
>
|
||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
|
||||
{isLoaded && (
|
||||
{isLoaded && canPersistChecked && (
|
||||
<div
|
||||
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
|
||||
>
|
||||
@@ -180,19 +199,28 @@ export default function Home() {
|
||||
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
|
||||
ref={drawioRef}
|
||||
autosave
|
||||
onAutoSave={handleDiagramAutoSave}
|
||||
onAutoSave={handleDrawioAutoSave}
|
||||
onExport={handleDiagramExport}
|
||||
onLoad={handleDrawioLoad}
|
||||
baseUrl={drawioBaseUrl}
|
||||
configuration={
|
||||
canPersist
|
||||
? { confirmExit: false }
|
||||
: undefined
|
||||
}
|
||||
urlParameters={{
|
||||
ui: drawioUi,
|
||||
spin: false,
|
||||
libraries: false,
|
||||
// Disable modified tracking only when persistence is available
|
||||
...(canPersist && {
|
||||
modified: false,
|
||||
keepmodified: false,
|
||||
}),
|
||||
saveAndExit: false,
|
||||
noSaveBtn: true,
|
||||
noExitBtn: true,
|
||||
dark:
|
||||
darkMode || drawioUi === "dark",
|
||||
dark: darkMode,
|
||||
lang: currentLang,
|
||||
// Enable offline mode in Electron to disable external service calls
|
||||
...(isElectron && {
|
||||
@@ -240,7 +268,7 @@ export default function Home() {
|
||||
isVisible={isChatVisible}
|
||||
onToggleVisibility={toggleChatPanel}
|
||||
drawioUi={drawioUi}
|
||||
onDrawioUiChange={handleDrawioUiChange}
|
||||
onToggleDrawioUi={handleDrawioUiChange}
|
||||
darkMode={darkMode}
|
||||
onToggleDarkMode={handleDarkModeChange}
|
||||
isMobile={isMobile}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
||||
@@ -141,6 +141,9 @@ export default function ExamplePanel({
|
||||
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
|
||||
{dict.examples.mcpServer}
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
|
||||
{dict.examples.preview}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{dict.examples.mcpDescription}
|
||||
|
||||
@@ -417,7 +417,6 @@ export function ChatMessageDisplay({
|
||||
|
||||
// Track previous message count to detect bulk loads vs streaming
|
||||
const prevMessageCountRef = useRef(0)
|
||||
const scrollThrottleRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current && messages.length > 0) {
|
||||
@@ -431,17 +430,8 @@ export function ChatMessageDisplay({
|
||||
return
|
||||
}
|
||||
|
||||
// Throttle scroll during streaming to avoid layout thrashing
|
||||
// Leading + trailing: scroll immediately, then once more after cooldown
|
||||
if (!scrollThrottleRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
scrollThrottleRef.current = setTimeout(() => {
|
||||
scrollThrottleRef.current = null
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
})
|
||||
}, 150)
|
||||
}
|
||||
// Single message added - smooth scroll
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import Image from "@/components/image-with-basepath"
|
||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||
import { SettingsDialog } from "@/components/settings-dialog"
|
||||
import { useDiagram } from "@/contexts/diagram-context"
|
||||
import { useAutoUpdate } from "@/hooks/use-auto-update"
|
||||
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
||||
@@ -32,7 +33,6 @@ import { useSessionManager } from "@/hooks/use-session-manager"
|
||||
import { useValidateDiagram } from "@/hooks/use-validate-diagram"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import { findCachedResponse } from "@/lib/cached-responses"
|
||||
import type { DrawioTheme } from "@/lib/drawio-themes"
|
||||
import { formatMessage } from "@/lib/i18n/utils"
|
||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||
import { sanitizeMessages } from "@/lib/session-storage"
|
||||
@@ -69,8 +69,8 @@ interface ChatMessage {
|
||||
interface ChatPanelProps {
|
||||
isVisible: boolean
|
||||
onToggleVisibility: () => void
|
||||
drawioUi: DrawioTheme
|
||||
onDrawioUiChange: (theme: DrawioTheme) => void
|
||||
drawioUi: "min" | "sketch"
|
||||
onToggleDrawioUi: () => void
|
||||
darkMode: boolean
|
||||
onToggleDarkMode: () => void
|
||||
isMobile?: boolean
|
||||
@@ -111,7 +111,7 @@ export default function ChatPanel({
|
||||
isVisible,
|
||||
onToggleVisibility,
|
||||
drawioUi,
|
||||
onDrawioUiChange,
|
||||
onToggleDrawioUi,
|
||||
darkMode,
|
||||
onToggleDarkMode,
|
||||
isMobile = false,
|
||||
@@ -1254,10 +1254,22 @@ export default function ChatPanel({
|
||||
sendChatMessage(newParts, savedXml, previousXml, sessionId)
|
||||
}
|
||||
|
||||
// Auto-update listener (must be before early return so it's always active)
|
||||
useAutoUpdate()
|
||||
|
||||
// Collapsed view (desktop only)
|
||||
if (!isVisible && !isMobile) {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
||||
<Toaster
|
||||
position="bottom-left"
|
||||
richColors
|
||||
expand
|
||||
toastOptions={{
|
||||
style: { maxWidth: "480px" },
|
||||
duration: 2000,
|
||||
}}
|
||||
/>
|
||||
<ButtonWithTooltip
|
||||
tooltipContent={dict.nav.showPanel}
|
||||
variant="ghost"
|
||||
@@ -1443,7 +1455,7 @@ export default function ChatPanel({
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
drawioUi={drawioUi}
|
||||
onDrawioUiChange={onDrawioUiChange}
|
||||
onToggleDrawioUi={onToggleDrawioUi}
|
||||
darkMode={darkMode}
|
||||
onToggleDarkMode={onToggleDarkMode}
|
||||
minimalStyle={minimalStyle}
|
||||
|
||||
@@ -107,8 +107,8 @@ export function ChatLobby({
|
||||
currentInput = "",
|
||||
dict,
|
||||
}: ChatLobbyProps) {
|
||||
const [templatesExpanded, setTemplatesExpanded] = useState(true)
|
||||
const [examplesExpanded, setExamplesExpanded] = useState(true)
|
||||
const [templatesExpanded, setTemplatesExpanded] = useState(false)
|
||||
const [examplesExpanded, setExamplesExpanded] = useState(false)
|
||||
const [panelVisibility, setPanelVisibility] = useState(getPanelVisibility)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [sessionToDelete, setSessionToDelete] = useState<string | null>(null)
|
||||
@@ -125,25 +125,19 @@ export function ChatLobby({
|
||||
const hasHistory = sessions.length > 0
|
||||
|
||||
if (!hasHistory) {
|
||||
if (!panelVisibility.myTemplates && !panelVisibility.quickExamples) {
|
||||
return null
|
||||
if (panelVisibility.myTemplates) {
|
||||
return (
|
||||
<TemplatePanel
|
||||
setInput={setInput}
|
||||
onSendTemplate={onSendTemplate}
|
||||
currentInput={currentInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="animate-fade-in">
|
||||
{panelVisibility.myTemplates && (
|
||||
<TemplatePanel
|
||||
setInput={setInput}
|
||||
onSendTemplate={onSendTemplate}
|
||||
currentInput={currentInput}
|
||||
/>
|
||||
)}
|
||||
{panelVisibility.quickExamples && (
|
||||
<div className={panelVisibility.myTemplates ? "mt-6" : ""}>
|
||||
<ExamplePanel setInput={setInput} setFiles={setFiles} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
if (panelVisibility.quickExamples) {
|
||||
return <ExamplePanel setInput={setInput} setFiles={setFiles} />
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Show history + collapsible examples when there are sessions
|
||||
|
||||
@@ -195,22 +195,7 @@ export function ToolCallCard({
|
||||
{input && isExpanded && (
|
||||
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
||||
{typeof input === "object" && input.xml ? (
|
||||
state === "input-streaming" ||
|
||||
state === "input-available" ? (
|
||||
<pre
|
||||
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all whitespace-pre-wrap"
|
||||
style={{
|
||||
fontFamily:
|
||||
"var(--font-mono), ui-monospace, monospace",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{input.xml}
|
||||
</pre>
|
||||
) : (
|
||||
<CodeBlock code={input.xml} language="xml" />
|
||||
)
|
||||
<CodeBlock code={input.xml} language="xml" />
|
||||
) : typeof input === "object" &&
|
||||
input.operations &&
|
||||
Array.isArray(input.operations) ? (
|
||||
|
||||
@@ -405,7 +405,7 @@ export function ModelConfigDialog({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 px-2 min-h-0">
|
||||
<ScrollArea className="flex-1 px-2">
|
||||
<div className="space-y-1 pb-2">
|
||||
{config.providers.length === 0 ? (
|
||||
<div className="px-3 py-8 text-center">
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Switch } from "@/components/ui/switch"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import type { DrawioTheme } from "@/lib/drawio-themes"
|
||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
|
||||
@@ -64,8 +63,8 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||
interface SettingsDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
drawioUi: DrawioTheme
|
||||
onDrawioUiChange: (theme: DrawioTheme) => void
|
||||
drawioUi: "min" | "sketch"
|
||||
onToggleDrawioUi: () => void
|
||||
darkMode: boolean
|
||||
onToggleDarkMode: () => void
|
||||
minimalStyle?: boolean
|
||||
@@ -91,7 +90,7 @@ function SettingsContent({
|
||||
open,
|
||||
onOpenChange,
|
||||
drawioUi,
|
||||
onDrawioUiChange,
|
||||
onToggleDrawioUi,
|
||||
darkMode,
|
||||
onToggleDarkMode,
|
||||
minimalStyle = false,
|
||||
@@ -135,11 +134,8 @@ function SettingsContent({
|
||||
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Re-fetch config whenever the dialog opens to ensure we always show
|
||||
// the access code input if the server requires it. This fixes the case
|
||||
// where a stale localStorage cache (from before ACCESS_CODE_LIST was
|
||||
// configured) would hide the access code input.
|
||||
if (!open) return
|
||||
// Only fetch if not cached in localStorage
|
||||
if (getStoredAccessCodeRequired() !== null) return
|
||||
|
||||
fetch(getApiEndpoint("/api/config"))
|
||||
.then((res) => {
|
||||
@@ -155,9 +151,10 @@ function SettingsContent({
|
||||
setAccessCodeRequired(required)
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep existing cached value on error
|
||||
// Don't cache on error - allow retry on next mount
|
||||
setAccessCodeRequired(false)
|
||||
})
|
||||
}, [open])
|
||||
}, [])
|
||||
|
||||
// Detect current language from pathname
|
||||
useEffect(() => {
|
||||
@@ -433,40 +430,23 @@ function SettingsContent({
|
||||
{/* Draw.io Style */}
|
||||
<SettingItem
|
||||
label={dict.settings.drawioStyle}
|
||||
description={dict.settings.drawioStyleDescription}
|
||||
description={`${dict.settings.drawioStyleDescription} ${
|
||||
drawioUi === "min"
|
||||
? dict.settings.minimal
|
||||
: dict.settings.sketch
|
||||
}`}
|
||||
>
|
||||
<Select
|
||||
value={drawioUi}
|
||||
onValueChange={(v) =>
|
||||
onDrawioUiChange(v as DrawioTheme)
|
||||
}
|
||||
<Button
|
||||
id="drawio-ui"
|
||||
variant="outline"
|
||||
onClick={onToggleDrawioUi}
|
||||
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="drawio-ui-select"
|
||||
aria-label={dict.settings.drawioStyle}
|
||||
className="w-[120px] h-9 rounded-xl"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="kennedy">
|
||||
{dict.settings.themeDefault}
|
||||
</SelectItem>
|
||||
<SelectItem value="atlas">Atlas</SelectItem>
|
||||
<SelectItem value="dark">
|
||||
{dict.settings.themeDark}
|
||||
</SelectItem>
|
||||
<SelectItem value="min">
|
||||
{dict.settings.themeMinimal}
|
||||
</SelectItem>
|
||||
<SelectItem value="sketch">
|
||||
{dict.settings.themeSketch}
|
||||
</SelectItem>
|
||||
<SelectItem value="simple">
|
||||
{dict.settings.themeSimple}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{dict.settings.switchTo}{" "}
|
||||
{drawioUi === "min"
|
||||
? dict.settings.sketch
|
||||
: dict.settings.minimal}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
{/* Diagram Style */}
|
||||
|
||||
124
components/update-toast.tsx
Normal file
124
components/update-toast.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
interface UpdateToastAvailableProps {
|
||||
variant: "download"
|
||||
version: string
|
||||
onDownload: () => void
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
interface UpdateToastManualProps {
|
||||
variant: "manual"
|
||||
version: string
|
||||
url: string
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
interface UpdateToastDownloadingProps {
|
||||
variant: "downloading"
|
||||
percent: number
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
type UpdateToastProps =
|
||||
| UpdateToastAvailableProps
|
||||
| UpdateToastManualProps
|
||||
| UpdateToastDownloadingProps
|
||||
|
||||
export function UpdateToast(props: UpdateToastProps) {
|
||||
const { variant, onDismiss } = props
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 flex-shrink-0">
|
||||
<svg
|
||||
className="w-4 h-4 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{variant === "downloading" ? (
|
||||
<span className="text-sm text-foreground">
|
||||
Downloading update... {Math.round(props.percent)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-foreground">
|
||||
Version {props.version} is available
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{variant === "download" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onDownload()
|
||||
}}
|
||||
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
|
||||
{variant === "manual" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
window.open(props.url, "_blank")
|
||||
}}
|
||||
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- [目录](#目录)
|
||||
- [示例](#示例)
|
||||
- [功能特性](#功能特性)
|
||||
- [MCP服务器](#mcp服务器)
|
||||
- [MCP服务器(预览)](#mcp服务器预览)
|
||||
- [Claude Code CLI](#claude-code-cli)
|
||||
- [快速开始](#快速开始)
|
||||
- [在线试用](#在线试用)
|
||||
@@ -56,31 +56,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
<tr>
|
||||
<td colspan="2" valign="top" align="center">
|
||||
<strong>动画Transformer连接器</strong><br />
|
||||
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
||||
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
|
||||
<img src="../../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>RAG技术图</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
|
||||
<img src="../../public/rag_prod.svg" alt="RAG架构图" width="480" />
|
||||
<strong>GCP架构图</strong><br />
|
||||
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||
<img src="../../public/gcp_demo.svg" alt="GCP架构图" width="480" />
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>React和AWS认证流程</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
|
||||
<img src="../../public/auth.svg" alt="认证架构图" width="480" />
|
||||
<strong>AWS架构图</strong><br />
|
||||
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||
<img src="../../public/aws_demo.svg" alt="AWS架构图" width="480" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>开放式创新</strong><br />
|
||||
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
|
||||
<img src="../../public/inno.svg" alt="开放式创新图" width="480" />
|
||||
<strong>Azure架构图</strong><br />
|
||||
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||
<img src="../../public/azure_demo.svg" alt="Azure架构图" width="480" />
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>猫咪素描</strong><br />
|
||||
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
|
||||
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
||||
</td>
|
||||
</tr>
|
||||
@@ -98,7 +98,9 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
||||
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||
|
||||
## MCP服务器
|
||||
## MCP服务器(预览)
|
||||
|
||||
> **预览功能**:此功能为实验性功能,可能不稳定。
|
||||
|
||||
通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- [目次](#目次)
|
||||
- [例](#例)
|
||||
- [機能](#機能)
|
||||
- [MCPサーバー](#mcpサーバー)
|
||||
- [MCPサーバー(プレビュー)](#mcpサーバープレビュー)
|
||||
- [Claude Code CLI](#claude-code-cli)
|
||||
- [はじめに](#はじめに)
|
||||
- [オンラインで試す](#オンラインで試す)
|
||||
@@ -54,31 +54,31 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
<tr>
|
||||
<td colspan="2" valign="top" align="center">
|
||||
<strong>アニメーションTransformerコネクタ</strong><br />
|
||||
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
||||
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
|
||||
<img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>RAG技術ダイアグラム</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate a RAG architecture diagram for **chat application**. Use connected diagram for data ingestion</p>
|
||||
<img src="../../public/rag_prod.svg" alt="RAGアーキテクチャ図" width="480" />
|
||||
<strong>GCPアーキテクチャ図</strong><br />
|
||||
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||
<img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>ReactとAWSによる認証</strong><br />
|
||||
<p><strong>Prompt:</strong> Generate authentication process using React with **AWS**. Use Serverless architecture.</p>
|
||||
<img src="../../public/auth.svg" alt="認証アーキテクチャ図" width="480" />
|
||||
<strong>AWSアーキテクチャ図</strong><br />
|
||||
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||
<img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>オープンイノベーション</strong><br />
|
||||
<p><strong>Prompt:</strong> Create visualization of Henry Chesbrough's Open Innovation model.</p>
|
||||
<img src="../../public/inno.svg" alt="オープンイノベーション図" width="480" />
|
||||
<strong>Azureアーキテクチャ図</strong><br />
|
||||
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||
<img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>猫のスケッチ</strong><br />
|
||||
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
|
||||
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
|
||||
</td>
|
||||
</tr>
|
||||
@@ -96,7 +96,9 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
||||
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||
|
||||
## MCPサーバー
|
||||
## MCPサーバー(プレビュー)
|
||||
|
||||
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
|
||||
|
||||
MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
|
||||
|
||||
|
||||
@@ -95,10 +95,6 @@ linux:
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: rpm
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
|
||||
# Publish configuration (optional)
|
||||
publish:
|
||||
|
||||
16
electron/electron.d.ts
vendored
16
electron/electron.d.ts
vendored
@@ -2,6 +2,14 @@
|
||||
* Type declarations for Electron API exposed via preload script
|
||||
*/
|
||||
|
||||
/** Update status data sent from main process */
|
||||
type UpdateStatusData =
|
||||
| { status: "available"; version: string }
|
||||
| { status: "available-manual"; version: string; url: string }
|
||||
| { status: "downloading"; percent: number }
|
||||
| { status: "downloaded" }
|
||||
| { status: "error"; message: string }
|
||||
|
||||
/** Configuration preset interface */
|
||||
interface ConfigPreset {
|
||||
id: string
|
||||
@@ -74,6 +82,12 @@ declare global {
|
||||
>
|
||||
/** Set user's preferred locale */
|
||||
setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
|
||||
/** Listen for update status events from main process */
|
||||
onUpdateStatus: (
|
||||
callback: (data: UpdateStatusData) => void,
|
||||
) => () => void
|
||||
/** Start downloading the available update */
|
||||
startDownload: () => Promise<void>
|
||||
}
|
||||
|
||||
/** Settings window Electron API */
|
||||
@@ -101,8 +115,8 @@ declare global {
|
||||
}
|
||||
|
||||
export type {
|
||||
ApplyPresetResult,
|
||||
ConfigPreset,
|
||||
ApplyPresetResult,
|
||||
ProxyConfig,
|
||||
SetProxyResult,
|
||||
SetUserLocaleResult,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type MenuItemConstructorOptions,
|
||||
shell,
|
||||
} from "electron"
|
||||
import { checkForUpdatesManual } from "./auto-updater"
|
||||
import {
|
||||
applyPresetToEnv,
|
||||
getAllPresets,
|
||||
@@ -156,6 +157,11 @@ function getMenuTemplate(): MenuItemConstructorOptions[] {
|
||||
template.push({
|
||||
label: t.help,
|
||||
submenu: [
|
||||
{
|
||||
label: t.checkForUpdates,
|
||||
click: () => checkForUpdatesManual(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: t.documentation,
|
||||
click: async () => {
|
||||
|
||||
282
electron/main/auto-updater.ts
Normal file
282
electron/main/auto-updater.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import https from "node:https"
|
||||
import { app, dialog, ipcMain } from "electron"
|
||||
import electronUpdater from "electron-updater"
|
||||
import { getMainWindow } from "./window-manager"
|
||||
|
||||
const { autoUpdater } = electronUpdater
|
||||
|
||||
const CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
|
||||
const STARTUP_DELAY = 10_000 // 10 seconds
|
||||
const GITHUB_API_URL =
|
||||
"https://api.github.com/repos/DayuanJiang/next-ai-draw-io/releases/latest"
|
||||
|
||||
let isChecking = false
|
||||
let updateDownloaded = false
|
||||
|
||||
/**
|
||||
* Whether this platform supports electron-updater auto-update.
|
||||
* macOS: disabled because builds are ad-hoc signed (no Apple Developer cert).
|
||||
*/
|
||||
function supportsAutoUpdate(): boolean {
|
||||
if (process.platform === "darwin") return false
|
||||
if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR)
|
||||
return false
|
||||
if (process.platform === "linux" && !process.env.APPIMAGE) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semver-like version strings numerically.
|
||||
* Returns true if remote > local.
|
||||
* Strips prerelease/build metadata (e.g., "1.2.3-beta.1" → "1.2.3").
|
||||
*/
|
||||
function isNewerVersion(remote: string, local: string): boolean {
|
||||
const strip = (v: string) => v.replace(/^v/, "").split("-")[0].split("+")[0]
|
||||
const r = strip(remote).split(".").map(Number)
|
||||
const l = strip(local).split(".").map(Number)
|
||||
const len = Math.max(r.length, l.length)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const rv = r[i] || 0
|
||||
const lv = l[i] || 0
|
||||
if (rv > lv) return true
|
||||
if (rv < lv) return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Send update status to the renderer via IPC
|
||||
*/
|
||||
function sendStatus(data: Record<string, unknown>) {
|
||||
const win = getMainWindow()
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send("update-status", data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check GitHub API for latest release (used on macOS and Linux DEB)
|
||||
*/
|
||||
function checkGitHubRelease(manual: boolean) {
|
||||
const req = https.get(
|
||||
GITHUB_API_URL,
|
||||
{
|
||||
headers: { "User-Agent": "next-ai-draw-io" },
|
||||
timeout: 15000,
|
||||
},
|
||||
(res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(`GitHub API returned status ${res.statusCode}`)
|
||||
if (manual) {
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
title: "Update Check Failed",
|
||||
message:
|
||||
"Could not check for updates. Please try again later.",
|
||||
})
|
||||
}
|
||||
isChecking = false
|
||||
return
|
||||
}
|
||||
|
||||
res.setEncoding("utf8")
|
||||
let body = ""
|
||||
res.on("data", (chunk: string) => {
|
||||
body += chunk
|
||||
})
|
||||
res.on("end", () => {
|
||||
try {
|
||||
const data = JSON.parse(body)
|
||||
const remoteVersion = data.tag_name || ""
|
||||
const localVersion = app.getVersion()
|
||||
|
||||
if (isNewerVersion(remoteVersion, localVersion)) {
|
||||
sendStatus({
|
||||
status: "available-manual",
|
||||
version: remoteVersion.replace(/^v/, ""),
|
||||
url: data.html_url,
|
||||
})
|
||||
} else if (manual) {
|
||||
dialog.showMessageBox({
|
||||
type: "info",
|
||||
title: "No Updates",
|
||||
message: "You're up to date!",
|
||||
detail: `Version ${localVersion} is the latest version.`,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to parse GitHub release:", err)
|
||||
if (manual) {
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
title: "Update Check Failed",
|
||||
message:
|
||||
"Could not check for updates. Please try again later.",
|
||||
})
|
||||
}
|
||||
}
|
||||
isChecking = false
|
||||
})
|
||||
},
|
||||
)
|
||||
req.on("timeout", () => {
|
||||
req.destroy()
|
||||
isChecking = false
|
||||
})
|
||||
req.on("error", (err) => {
|
||||
console.error("GitHub API request failed:", err)
|
||||
if (manual) {
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
title: "Update Check Failed",
|
||||
message: "Could not check for updates. Please try again later.",
|
||||
})
|
||||
}
|
||||
isChecking = false
|
||||
})
|
||||
req.end()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up electron-updater event handlers (Windows NSIS / Linux AppImage)
|
||||
*/
|
||||
function setupAutoUpdater() {
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
console.log("Update available:", info.version)
|
||||
sendStatus({ status: "available", version: info.version })
|
||||
isChecking = false
|
||||
})
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("No update available")
|
||||
isChecking = false
|
||||
})
|
||||
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
sendStatus({
|
||||
status: "downloading",
|
||||
percent: progress.percent,
|
||||
})
|
||||
})
|
||||
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
updateDownloaded = true
|
||||
console.log("Update downloaded")
|
||||
sendStatus({ status: "downloaded" })
|
||||
showRestartDialog()
|
||||
})
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.error("Auto-update error:", err)
|
||||
sendStatus({ status: "error", message: String(err) })
|
||||
isChecking = false
|
||||
})
|
||||
|
||||
// IPC: renderer requests download
|
||||
ipcMain.handle("updater:start-download", () => {
|
||||
return autoUpdater.downloadUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Show restart dialog after update downloaded
|
||||
*/
|
||||
async function showRestartDialog() {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: "info",
|
||||
buttons: ["Restart Now", "Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
title: "Update Ready",
|
||||
message: "A new version has been downloaded.",
|
||||
detail: "Restart the app to install the update.",
|
||||
})
|
||||
if (result.response === 0) {
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single update check
|
||||
*/
|
||||
function doCheck(manual: boolean) {
|
||||
if (isChecking) return
|
||||
isChecking = true
|
||||
|
||||
// If update already downloaded, just re-show restart dialog
|
||||
if (updateDownloaded && supportsAutoUpdate()) {
|
||||
isChecking = false
|
||||
showRestartDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (supportsAutoUpdate()) {
|
||||
// For manual check, register listeners BEFORE triggering check
|
||||
// to avoid race where event fires before listeners are attached
|
||||
if (manual) {
|
||||
const onNotAvailable = () => {
|
||||
dialog.showMessageBox({
|
||||
type: "info",
|
||||
title: "No Updates",
|
||||
message: "You're up to date!",
|
||||
detail: `Version ${app.getVersion()} is the latest version.`,
|
||||
})
|
||||
cleanup()
|
||||
}
|
||||
const onAvailable = () => cleanup()
|
||||
const onError = () => cleanup()
|
||||
const cleanup = () => {
|
||||
autoUpdater.off("update-not-available", onNotAvailable)
|
||||
autoUpdater.off("update-available", onAvailable)
|
||||
autoUpdater.off("error", onError)
|
||||
}
|
||||
autoUpdater.once("update-not-available", onNotAvailable)
|
||||
autoUpdater.once("update-available", onAvailable)
|
||||
autoUpdater.once("error", onError)
|
||||
}
|
||||
autoUpdater.checkForUpdates().catch((err) => {
|
||||
console.error("checkForUpdates failed:", err)
|
||||
isChecking = false
|
||||
})
|
||||
} else {
|
||||
checkGitHubRelease(manual)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize auto-updater. Call once after createWindow().
|
||||
*/
|
||||
export function initAutoUpdater() {
|
||||
if (!app.isPackaged) return
|
||||
|
||||
if (supportsAutoUpdate()) {
|
||||
setupAutoUpdater()
|
||||
} else {
|
||||
// Register no-op handler so renderer doesn't get an unhandled error
|
||||
ipcMain.handle("updater:start-download", () => {})
|
||||
}
|
||||
|
||||
// First check after startup delay
|
||||
setTimeout(() => doCheck(false), STARTUP_DELAY)
|
||||
|
||||
// Periodic checks
|
||||
setInterval(() => doCheck(false), CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual update check from menu item
|
||||
*/
|
||||
export function checkForUpdatesManual() {
|
||||
if (!app.isPackaged) {
|
||||
dialog.showMessageBox({
|
||||
type: "info",
|
||||
title: "Development Mode",
|
||||
message: "Auto-update is not available in development mode.",
|
||||
})
|
||||
return
|
||||
}
|
||||
doCheck(true)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app, BrowserWindow, dialog, shell } from "electron"
|
||||
import { buildAppMenu } from "./app-menu"
|
||||
import { initAutoUpdater } from "./auto-updater"
|
||||
import { getCurrentPresetEnv } from "./config-manager"
|
||||
import { loadEnvFile } from "./env-loader"
|
||||
import { registerIpcHandlers } from "./ipc-handlers"
|
||||
@@ -58,6 +59,11 @@ if (!gotTheLock) {
|
||||
|
||||
// Create main window
|
||||
createWindow(serverUrl)
|
||||
|
||||
// Initialize auto-updater (production only)
|
||||
if (!isDev) {
|
||||
initAutoUpdater()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to start application:", error)
|
||||
dialog.showErrorBox(
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface MenuTranslations {
|
||||
help: string
|
||||
documentation: string
|
||||
reportIssue: string
|
||||
checkForUpdates: string
|
||||
}
|
||||
|
||||
const translations: Record<MenuLocale, MenuTranslations> = {
|
||||
@@ -62,6 +63,7 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
||||
help: "Help",
|
||||
documentation: "Documentation",
|
||||
reportIssue: "Report Issue",
|
||||
checkForUpdates: "Check for Updates...",
|
||||
},
|
||||
|
||||
zh: {
|
||||
@@ -90,6 +92,7 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
||||
help: "帮助",
|
||||
documentation: "文档",
|
||||
reportIssue: "报告问题",
|
||||
checkForUpdates: "检查更新...",
|
||||
},
|
||||
|
||||
ja: {
|
||||
@@ -118,6 +121,7 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
||||
help: "ヘルプ",
|
||||
documentation: "ドキュメント",
|
||||
reportIssue: "問題を報告",
|
||||
checkForUpdates: "アップデートを確認...",
|
||||
},
|
||||
|
||||
"zh-Hant": {
|
||||
@@ -146,6 +150,7 @@ const translations: Record<MenuLocale, MenuTranslations> = {
|
||||
help: "說明",
|
||||
documentation: "文件",
|
||||
reportIssue: "回報問題",
|
||||
checkForUpdates: "檢查更新...",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -60,13 +60,6 @@ export function createWindow(serverUrl: string): BrowserWindow {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
// Override the draw.io iframe's beforeunload handler so the window can
|
||||
// close after the user edits text in a shape (fixes #815). Diagrams are
|
||||
// already persisted via autosave, so the prompt is unnecessary.
|
||||
mainWindow.webContents.on("will-prevent-unload", (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { contextBridge, ipcRenderer } from "electron"
|
||||
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron"
|
||||
|
||||
// Locally defined to avoid dependency on electron.d.ts compilation scope
|
||||
type UpdateStatus =
|
||||
| { status: "available"; version: string }
|
||||
| { status: "available-manual"; version: string; url: string }
|
||||
| { status: "downloading"; percent: number }
|
||||
| { status: "downloaded" }
|
||||
| { status: "error"; message: string }
|
||||
|
||||
/**
|
||||
* Expose safe APIs to the renderer process
|
||||
@@ -31,4 +39,13 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getUserLocale: () => ipcRenderer.invoke("get-user-locale"),
|
||||
setUserLocale: (locale: string) =>
|
||||
ipcRenderer.invoke("set-user-locale", locale),
|
||||
|
||||
// Auto-update
|
||||
onUpdateStatus: (callback: (data: UpdateStatus) => void) => {
|
||||
const handler = (_event: IpcRendererEvent, data: UpdateStatus) =>
|
||||
callback(data)
|
||||
ipcRenderer.on("update-status", handler)
|
||||
return () => ipcRenderer.removeListener("update-status", handler)
|
||||
},
|
||||
startDownload: () => ipcRenderer.invoke("updater:start-download"),
|
||||
})
|
||||
|
||||
86
hooks/use-auto-update.ts
Normal file
86
hooks/use-auto-update.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createElement, useEffect, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { UpdateToast } from "@/components/update-toast"
|
||||
|
||||
export function useAutoUpdate() {
|
||||
const downloadToastId = useRef<string | number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const api = (window as Window).electronAPI
|
||||
if (!api?.onUpdateStatus) return
|
||||
|
||||
const cleanup = api.onUpdateStatus((data) => {
|
||||
switch (data.status) {
|
||||
case "available":
|
||||
toast.custom(
|
||||
(t) =>
|
||||
createElement(UpdateToast, {
|
||||
variant: "download",
|
||||
version: data.version,
|
||||
onDownload: () => {
|
||||
toast.dismiss(t)
|
||||
api.startDownload().catch(() => {
|
||||
// Error will come through update-status channel
|
||||
})
|
||||
},
|
||||
onDismiss: () => toast.dismiss(t),
|
||||
}),
|
||||
{ duration: 15000 },
|
||||
)
|
||||
break
|
||||
|
||||
case "available-manual":
|
||||
toast.custom(
|
||||
(t) =>
|
||||
createElement(UpdateToast, {
|
||||
variant: "manual",
|
||||
version: data.version,
|
||||
url: data.url,
|
||||
onDismiss: () => toast.dismiss(t),
|
||||
}),
|
||||
{ duration: Number.POSITIVE_INFINITY },
|
||||
)
|
||||
break
|
||||
|
||||
case "downloading": {
|
||||
const id =
|
||||
downloadToastId.current ??
|
||||
`download-progress-${Date.now()}`
|
||||
downloadToastId.current = id
|
||||
toast.custom(
|
||||
(t) =>
|
||||
createElement(UpdateToast, {
|
||||
variant: "downloading",
|
||||
percent: data.percent,
|
||||
onDismiss: () => toast.dismiss(t),
|
||||
}),
|
||||
{ id, duration: Number.POSITIVE_INFINITY },
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "downloaded":
|
||||
// Dismiss progress toast — native restart dialog handles the rest
|
||||
if (downloadToastId.current) {
|
||||
toast.dismiss(downloadToastId.current)
|
||||
downloadToastId.current = null
|
||||
}
|
||||
break
|
||||
|
||||
case "error":
|
||||
if (downloadToastId.current) {
|
||||
// Error during active download — inform the user
|
||||
toast.dismiss(downloadToastId.current)
|
||||
downloadToastId.current = null
|
||||
toast.error(
|
||||
"Update download failed. Please try again later.",
|
||||
)
|
||||
}
|
||||
// Otherwise: silent — background check failure, logged in main process
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return cleanup
|
||||
}, [])
|
||||
}
|
||||
@@ -1262,6 +1262,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
case "glm":
|
||||
case "qwen":
|
||||
case "qiniu":
|
||||
case "kimi":
|
||||
case "novita": {
|
||||
const envVar = PROVIDER_ENV_VARS[provider]
|
||||
if (!envVar) {
|
||||
@@ -1287,23 +1288,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
break
|
||||
}
|
||||
|
||||
case "kimi": {
|
||||
const apiKey = resolveApiKey(overrides, "KIMI_API_KEY")
|
||||
const baseURL = resolveBaseURL(
|
||||
overrides?.apiKey,
|
||||
overrides?.baseUrl,
|
||||
resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"),
|
||||
PROVIDER_INFO["kimi"]?.defaultBaseUrl,
|
||||
)
|
||||
// Use createDeepSeek to properly handle reasoning_content for Kimi
|
||||
// thinking models (e.g., kimi-k2.6). Kimi's API uses the same
|
||||
// reasoning_content field as DeepSeek, so this provider correctly
|
||||
// captures and replays reasoning in multi-turn conversations.
|
||||
const customProvider = createDeepSeek({ apiKey, baseURL })
|
||||
model = customProvider(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`,
|
||||
@@ -1372,13 +1356,12 @@ export function supportsImageInput(modelId: string): boolean {
|
||||
}
|
||||
|
||||
// Qwen text models (not vision variants like qwen-vl)
|
||||
// Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input
|
||||
// QvQ (Qwen Visual QA) models are vision models — exclude them even when prefixed with "qwen/"
|
||||
// qwen3.5-plus is a vision model
|
||||
if (
|
||||
lowerModelId.includes("qwen") &&
|
||||
!hasVisionIndicator &&
|
||||
!lowerModelId.includes("qwen3.5") &&
|
||||
!lowerModelId.includes("qvq")
|
||||
!lowerModelId.includes("qwen3.5-plus") &&
|
||||
!lowerModelId.includes("qwen3.5-flash")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -76,7 +76,8 @@
|
||||
"creativeDescription": "Draw something fun and creative",
|
||||
"cachedNote": "Examples are cached for instant response",
|
||||
"mcpServer": "MCP Server",
|
||||
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor"
|
||||
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor",
|
||||
"preview": "PREVIEW"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -102,12 +103,10 @@
|
||||
"theme": "Theme",
|
||||
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
|
||||
"drawioStyle": "DrawIO Style",
|
||||
"drawioStyleDescription": "Canvas style",
|
||||
"themeDefault": "Default",
|
||||
"themeDark": "Dark",
|
||||
"themeMinimal": "Minimal",
|
||||
"themeSketch": "Sketch",
|
||||
"themeSimple": "Simple",
|
||||
"drawioStyleDescription": "Canvas style:",
|
||||
"switchTo": "Switch to",
|
||||
"minimal": "Minimal",
|
||||
"sketch": "Sketch",
|
||||
"diagramStyle": "Diagram Style",
|
||||
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
|
||||
"sendShortcut": "Send Shortcut",
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"creativeDescription": "楽しくてクリエイティブなものを描く",
|
||||
"cachedNote": "例はキャッシュされ、即座に応答します",
|
||||
"mcpServer": "MCP サーバー",
|
||||
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用"
|
||||
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用",
|
||||
"preview": "プレビュー"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
@@ -102,12 +103,10 @@
|
||||
"theme": "テーマ",
|
||||
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
|
||||
"drawioStyle": "DrawIO スタイル",
|
||||
"drawioStyleDescription": "キャンバススタイル",
|
||||
"themeDefault": "デフォルト",
|
||||
"themeDark": "ダーク",
|
||||
"themeMinimal": "ミニマル",
|
||||
"themeSketch": "スケッチ",
|
||||
"themeSimple": "シンプル",
|
||||
"drawioStyleDescription": "キャンバススタイル:",
|
||||
"switchTo": "切り替え",
|
||||
"minimal": "ミニマル",
|
||||
"sketch": "スケッチ",
|
||||
"diagramStyle": "ダイアグラムスタイル",
|
||||
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
||||
"sendShortcut": "送信ショートカット",
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"creativeDescription": "繪製有趣且富有創意的內容",
|
||||
"cachedNote": "範例已快取,可即時回應",
|
||||
"mcpServer": "MCP 伺服器",
|
||||
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
|
||||
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
|
||||
"preview": "預覽"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
@@ -102,12 +103,10 @@
|
||||
"theme": "主題",
|
||||
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
|
||||
"drawioStyle": "DrawIO 樣式",
|
||||
"drawioStyleDescription": "畫布樣式",
|
||||
"themeDefault": "預設",
|
||||
"themeDark": "深色",
|
||||
"themeMinimal": "簡約",
|
||||
"themeSketch": "草圖",
|
||||
"themeSimple": "簡單",
|
||||
"drawioStyleDescription": "畫布樣式:",
|
||||
"switchTo": "切換到",
|
||||
"minimal": "簡約",
|
||||
"sketch": "草圖",
|
||||
"diagramStyle": "圖表樣式",
|
||||
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
|
||||
"sendShortcut": "傳送快捷鍵",
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"creativeDescription": "绘制有趣且富有创意的内容",
|
||||
"cachedNote": "示例已缓存,可即时响应",
|
||||
"mcpServer": "MCP 服务器",
|
||||
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
|
||||
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用",
|
||||
"preview": "预览"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
@@ -102,12 +103,10 @@
|
||||
"theme": "主题",
|
||||
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
|
||||
"drawioStyle": "DrawIO 样式",
|
||||
"drawioStyleDescription": "画布样式",
|
||||
"themeDefault": "默认",
|
||||
"themeDark": "深色",
|
||||
"themeMinimal": "简约",
|
||||
"themeSketch": "草图",
|
||||
"themeSimple": "简单",
|
||||
"drawioStyleDescription": "画布样式:",
|
||||
"switchTo": "切换到",
|
||||
"minimal": "简约",
|
||||
"sketch": "草图",
|
||||
"diagramStyle": "图表样式",
|
||||
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
||||
"sendShortcut": "发送快捷键",
|
||||
|
||||
@@ -58,6 +58,33 @@ interface ChatSessionDB extends DBSchema {
|
||||
|
||||
// Database singleton
|
||||
let dbPromise: Promise<IDBPDatabase<ChatSessionDB>> | null = null
|
||||
const resetDBPromise = () => {
|
||||
dbPromise = null
|
||||
}
|
||||
|
||||
const isClosingError = (error: unknown): boolean => {
|
||||
return (
|
||||
error instanceof DOMException &&
|
||||
error.name === "InvalidStateError" &&
|
||||
/closing/i.test(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
const withDB = async <T>(
|
||||
action: (db: IDBPDatabase<ChatSessionDB>) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
const db = await getDB()
|
||||
return await action(db)
|
||||
} catch (error) {
|
||||
if (isClosingError(error)) {
|
||||
resetDBPromise()
|
||||
const db = await getDB()
|
||||
return await action(db)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
|
||||
if (!dbPromise) {
|
||||
@@ -88,7 +115,23 @@ async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
|
||||
}
|
||||
}
|
||||
},
|
||||
terminated() {
|
||||
resetDBPromise()
|
||||
},
|
||||
})
|
||||
dbPromise
|
||||
.then((db) => {
|
||||
db.onversionchange = () => {
|
||||
db.close()
|
||||
resetDBPromise()
|
||||
}
|
||||
db.onclose = () => {
|
||||
resetDBPromise()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
resetDBPromise()
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
@@ -103,31 +146,46 @@ export function isIndexedDBAvailable(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if IndexedDB is actually usable (not just present).
|
||||
// Note: Do NOT close the db here - getDB() returns a shared singleton connection
|
||||
// that other code depends on.
|
||||
export async function isIndexedDBUsable(): Promise<boolean> {
|
||||
if (!isIndexedDBAvailable()) return false
|
||||
try {
|
||||
await getDB()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CRUD Operations
|
||||
export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
||||
if (!isIndexedDBAvailable()) return []
|
||||
try {
|
||||
const db = await getDB()
|
||||
const tx = db.transaction(STORE_NAME, "readonly")
|
||||
const index = tx.store.index("by-updated")
|
||||
const metadata: SessionMetadata[] = []
|
||||
return await withDB(async (db) => {
|
||||
const tx = db.transaction(STORE_NAME, "readonly")
|
||||
const index = tx.store.index("by-updated")
|
||||
const metadata: SessionMetadata[] = []
|
||||
|
||||
// Use cursor to read only metadata fields (avoids loading full messages/XML)
|
||||
let cursor = await index.openCursor(null, "prev") // newest first
|
||||
while (cursor) {
|
||||
const s = cursor.value
|
||||
metadata.push({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
createdAt: s.createdAt,
|
||||
updatedAt: s.updatedAt,
|
||||
messageCount: s.messages.length,
|
||||
hasDiagram: !!s.diagramXml && s.diagramXml.trim().length > 0,
|
||||
thumbnailDataUrl: s.thumbnailDataUrl,
|
||||
})
|
||||
cursor = await cursor.continue()
|
||||
}
|
||||
return metadata
|
||||
// Use cursor to read only metadata fields (avoids loading full messages/XML)
|
||||
let cursor = await index.openCursor(null, "prev") // newest first
|
||||
while (cursor) {
|
||||
const s = cursor.value
|
||||
metadata.push({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
createdAt: s.createdAt,
|
||||
updatedAt: s.updatedAt,
|
||||
messageCount: s.messages.length,
|
||||
hasDiagram:
|
||||
!!s.diagramXml && s.diagramXml.trim().length > 0,
|
||||
thumbnailDataUrl: s.thumbnailDataUrl,
|
||||
})
|
||||
cursor = await cursor.continue()
|
||||
}
|
||||
return metadata
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to get session metadata:", error)
|
||||
return []
|
||||
@@ -137,8 +195,9 @@ export async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
|
||||
export async function getSession(id: string): Promise<ChatSession | null> {
|
||||
if (!isIndexedDBAvailable()) return null
|
||||
try {
|
||||
const db = await getDB()
|
||||
return (await db.get(STORE_NAME, id)) || null
|
||||
return await withDB(async (db) => {
|
||||
return (await db.get(STORE_NAME, id)) || null
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to get session:", error)
|
||||
return null
|
||||
@@ -148,8 +207,9 @@ export async function getSession(id: string): Promise<ChatSession | null> {
|
||||
export async function saveSession(session: ChatSession): Promise<boolean> {
|
||||
if (!isIndexedDBAvailable()) return false
|
||||
try {
|
||||
const db = await getDB()
|
||||
await db.put(STORE_NAME, session)
|
||||
await withDB(async (db) => {
|
||||
await db.put(STORE_NAME, session)
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
// Handle quota exceeded
|
||||
@@ -161,8 +221,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
|
||||
await deleteOldestSession()
|
||||
// Retry once
|
||||
try {
|
||||
const db = await getDB()
|
||||
await db.put(STORE_NAME, session)
|
||||
await withDB(async (db) => {
|
||||
await db.put(STORE_NAME, session)
|
||||
})
|
||||
return true
|
||||
} catch (retryError) {
|
||||
console.error(
|
||||
@@ -181,8 +242,9 @@ export async function saveSession(session: ChatSession): Promise<boolean> {
|
||||
export async function deleteSession(id: string): Promise<void> {
|
||||
if (!isIndexedDBAvailable()) return
|
||||
try {
|
||||
const db = await getDB()
|
||||
await db.delete(STORE_NAME, id)
|
||||
await withDB(async (db) => {
|
||||
await db.delete(STORE_NAME, id)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to delete session:", error)
|
||||
}
|
||||
@@ -191,8 +253,9 @@ export async function deleteSession(id: string): Promise<void> {
|
||||
export async function getSessionCount(): Promise<number> {
|
||||
if (!isIndexedDBAvailable()) return 0
|
||||
try {
|
||||
const db = await getDB()
|
||||
return await db.count(STORE_NAME)
|
||||
return await withDB(async (db) => {
|
||||
return await db.count(STORE_NAME)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to get session count:", error)
|
||||
return 0
|
||||
@@ -202,14 +265,15 @@ export async function getSessionCount(): Promise<number> {
|
||||
export async function deleteOldestSession(): Promise<void> {
|
||||
if (!isIndexedDBAvailable()) return
|
||||
try {
|
||||
const db = await getDB()
|
||||
const tx = db.transaction(STORE_NAME, "readwrite")
|
||||
const index = tx.store.index("by-updated")
|
||||
const cursor = await index.openCursor()
|
||||
if (cursor) {
|
||||
await cursor.delete()
|
||||
}
|
||||
await tx.done
|
||||
await withDB(async (db) => {
|
||||
const tx = db.transaction(STORE_NAME, "readwrite")
|
||||
const index = tx.store.index("by-updated")
|
||||
const cursor = await index.openCursor()
|
||||
if (cursor) {
|
||||
await cursor.delete()
|
||||
}
|
||||
await tx.done
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to delete oldest session:", error)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,33 @@ export function generateDefaultTitle(prompt: string): string {
|
||||
|
||||
// Database singleton
|
||||
let dbPromise: Promise<IDBPDatabase<TemplateDB>> | null = null
|
||||
const resetDBPromise = () => {
|
||||
dbPromise = null
|
||||
}
|
||||
|
||||
const isClosingError = (error: unknown): boolean => {
|
||||
return (
|
||||
error instanceof DOMException &&
|
||||
error.name === "InvalidStateError" &&
|
||||
/closing/i.test(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
const withDB = async <T>(
|
||||
action: (db: IDBPDatabase<TemplateDB>) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
const db = await getDB()
|
||||
return await action(db)
|
||||
} catch (error) {
|
||||
if (isClosingError(error)) {
|
||||
resetDBPromise()
|
||||
const db = await getDB()
|
||||
return await action(db)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
|
||||
if (!dbPromise) {
|
||||
@@ -74,7 +101,23 @@ async function getDB(): Promise<IDBPDatabase<TemplateDB>> {
|
||||
}
|
||||
}
|
||||
},
|
||||
terminated() {
|
||||
resetDBPromise()
|
||||
},
|
||||
})
|
||||
dbPromise
|
||||
.then((db) => {
|
||||
db.onversionchange = () => {
|
||||
db.close()
|
||||
resetDBPromise()
|
||||
}
|
||||
db.onclose = () => {
|
||||
resetDBPromise()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
resetDBPromise()
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
@@ -94,9 +137,10 @@ export function isIndexedDBAvailable(): boolean {
|
||||
export async function getAllTemplates(): Promise<Template[]> {
|
||||
if (!isIndexedDBAvailable()) return []
|
||||
try {
|
||||
const db = await getDB()
|
||||
const templates = await db.getAll(STORE_NAME)
|
||||
return sortTemplates(templates)
|
||||
return await withDB(async (db) => {
|
||||
const templates = await db.getAll(STORE_NAME)
|
||||
return sortTemplates(templates)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to get templates:", error)
|
||||
return []
|
||||
@@ -106,8 +150,9 @@ export async function getAllTemplates(): Promise<Template[]> {
|
||||
export async function getTemplate(id: string): Promise<Template | null> {
|
||||
if (!isIndexedDBAvailable()) return null
|
||||
try {
|
||||
const db = await getDB()
|
||||
return (await db.get(STORE_NAME, id)) || null
|
||||
return await withDB(async (db) => {
|
||||
return (await db.get(STORE_NAME, id)) || null
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to get template:", error)
|
||||
return null
|
||||
@@ -137,8 +182,9 @@ export async function createTemplate(
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await getDB()
|
||||
await db.put(STORE_NAME, template)
|
||||
await withDB(async (db) => {
|
||||
await db.put(STORE_NAME, template)
|
||||
})
|
||||
return template
|
||||
} catch (error) {
|
||||
console.error("Failed to create template:", error)
|
||||
@@ -152,19 +198,20 @@ export async function updateTemplate(
|
||||
): Promise<Template | null> {
|
||||
if (!isIndexedDBAvailable()) return null
|
||||
try {
|
||||
const db = await getDB()
|
||||
const existing = await db.get(STORE_NAME, id)
|
||||
if (!existing) return null
|
||||
return await withDB(async (db) => {
|
||||
const existing = await db.get(STORE_NAME, id)
|
||||
if (!existing) return null
|
||||
|
||||
const updated: Template = {
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await db.put(STORE_NAME, updated)
|
||||
return updated
|
||||
const updated: Template = {
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await db.put(STORE_NAME, updated)
|
||||
return updated
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to update template:", error)
|
||||
return null
|
||||
@@ -174,8 +221,9 @@ export async function updateTemplate(
|
||||
export async function deleteTemplate(id: string): Promise<boolean> {
|
||||
if (!isIndexedDBAvailable()) return false
|
||||
try {
|
||||
const db = await getDB()
|
||||
await db.delete(STORE_NAME, id)
|
||||
await withDB(async (db) => {
|
||||
await db.delete(STORE_NAME, id)
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Failed to delete template:", error)
|
||||
@@ -189,24 +237,25 @@ export async function duplicateTemplate(
|
||||
): Promise<Template | null> {
|
||||
if (!isIndexedDBAvailable()) return null
|
||||
try {
|
||||
const db = await getDB()
|
||||
const existing = await db.get(STORE_NAME, id)
|
||||
if (!existing) return null
|
||||
return await withDB(async (db) => {
|
||||
const existing = await db.get(STORE_NAME, id)
|
||||
if (!existing) return null
|
||||
|
||||
const now = Date.now()
|
||||
const duplicate: Template = {
|
||||
...existing,
|
||||
id: nanoid(),
|
||||
title: `${existing.title} ${copySuffix}`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
clickCount: 0,
|
||||
runCount: 0,
|
||||
lastUsedAt: 0,
|
||||
pinned: false,
|
||||
}
|
||||
await db.put(STORE_NAME, duplicate)
|
||||
return duplicate
|
||||
const now = Date.now()
|
||||
const duplicate: Template = {
|
||||
...existing,
|
||||
id: nanoid(),
|
||||
title: `${existing.title} ${copySuffix}`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
clickCount: 0,
|
||||
runCount: 0,
|
||||
lastUsedAt: 0,
|
||||
pinned: false,
|
||||
}
|
||||
await db.put(STORE_NAME, duplicate)
|
||||
return duplicate
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to duplicate template:", error)
|
||||
return null
|
||||
@@ -218,12 +267,13 @@ export async function duplicateTemplate(
|
||||
export async function incrementClickCount(id: string): Promise<void> {
|
||||
if (!isIndexedDBAvailable()) return
|
||||
try {
|
||||
const db = await getDB()
|
||||
const template = await db.get(STORE_NAME, id)
|
||||
if (!template) return
|
||||
template.clickCount += 1
|
||||
template.updatedAt = Date.now()
|
||||
await db.put(STORE_NAME, template)
|
||||
await withDB(async (db) => {
|
||||
const template = await db.get(STORE_NAME, id)
|
||||
if (!template) return
|
||||
template.clickCount += 1
|
||||
template.updatedAt = Date.now()
|
||||
await db.put(STORE_NAME, template)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to increment click count:", error)
|
||||
}
|
||||
@@ -232,14 +282,15 @@ export async function incrementClickCount(id: string): Promise<void> {
|
||||
export async function incrementRunCount(id: string): Promise<void> {
|
||||
if (!isIndexedDBAvailable()) return
|
||||
try {
|
||||
const db = await getDB()
|
||||
const template = await db.get(STORE_NAME, id)
|
||||
if (!template) return
|
||||
const now = Date.now()
|
||||
template.runCount += 1
|
||||
template.lastUsedAt = now
|
||||
template.updatedAt = now
|
||||
await db.put(STORE_NAME, template)
|
||||
await withDB(async (db) => {
|
||||
const template = await db.get(STORE_NAME, id)
|
||||
if (!template) return
|
||||
const now = Date.now()
|
||||
template.runCount += 1
|
||||
template.lastUsedAt = now
|
||||
template.updatedAt = now
|
||||
await db.put(STORE_NAME, template)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to increment run count:", error)
|
||||
}
|
||||
@@ -372,8 +423,9 @@ export async function importTemplates(
|
||||
pinned: typeof t.pinned === "boolean" ? t.pinned : false,
|
||||
}
|
||||
try {
|
||||
const db = await getDB()
|
||||
await db.put(STORE_NAME, newTemplate)
|
||||
await withDB(async (db) => {
|
||||
await db.put(STORE_NAME, newTemplate)
|
||||
})
|
||||
existingKeys.add(key)
|
||||
imported++
|
||||
} catch (error) {
|
||||
|
||||
2590
package-lock.json
generated
2590
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-ai-draw-io",
|
||||
"version": "0.4.16",
|
||||
"version": "0.4.14",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"main": "dist-electron/main/index.js",
|
||||
@@ -49,9 +49,9 @@
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
"@next/third-parties": "^16.0.6",
|
||||
"@opennextjs/cloudflare": "^1.17.1",
|
||||
"@openrouter/ai-sdk-provider": "^2.0.0",
|
||||
"@openrouter/ai-sdk-provider": "^1.5.4",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.216.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -70,6 +70,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"electron-updater": "^6.8.3",
|
||||
"idb": "^8.0.3",
|
||||
"jsonrepair": "^3.13.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
@@ -77,7 +78,7 @@
|
||||
"nanoid": "^5.0.0",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.0.7",
|
||||
"ollama-ai-provider-v2": "^3.0.0",
|
||||
"ollama-ai-provider-v2": "^2.0.0",
|
||||
"pako": "^2.1.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^19.1.2",
|
||||
@@ -108,7 +109,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@biomejs/biome": "2.4.13",
|
||||
"@biomejs/biome": "2.4.10",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
@@ -127,7 +128,7 @@
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^39.2.7",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild": "^0.27.2",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"husky": "^9.1.7",
|
||||
|
||||
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",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.17",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
@@ -521,9 +521,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
||||
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||
"version": "24.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
||||
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2062,9 +2062,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz",
|
||||
"integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==",
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.17",
|
||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -4,29 +4,6 @@
|
||||
*/
|
||||
|
||||
import http from "node:http"
|
||||
|
||||
const MAX_BODY_BYTES = 10 * 1024 * 1024 // 10 MiB
|
||||
|
||||
function readBody(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
cb: (body: string) => void,
|
||||
): void {
|
||||
let body = ""
|
||||
let size = 0
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
size += chunk.length
|
||||
if (size > MAX_BODY_BYTES) {
|
||||
res.writeHead(413, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ error: "Payload too large" }))
|
||||
req.destroy()
|
||||
return
|
||||
}
|
||||
body += chunk
|
||||
})
|
||||
req.on("end", () => cb(body))
|
||||
}
|
||||
|
||||
import {
|
||||
addHistory,
|
||||
clearHistory,
|
||||
@@ -178,7 +155,7 @@ export function startHttpServer(port = 6002): Promise<number> {
|
||||
}
|
||||
})
|
||||
|
||||
server.listen(port, "127.0.0.1", () => {
|
||||
server.listen(port, () => {
|
||||
serverPort = port
|
||||
log.info(`HTTP server running on http://localhost:${port}`)
|
||||
resolve(port)
|
||||
@@ -289,7 +266,11 @@ function handleStateApi(
|
||||
}),
|
||||
)
|
||||
} else if (req.method === "POST") {
|
||||
readBody(req, res, (body) => {
|
||||
let body = ""
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk
|
||||
})
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const data = JSON.parse(body)
|
||||
const { sessionId } = data
|
||||
@@ -366,7 +347,11 @@ function handleRestoreApi(
|
||||
return
|
||||
}
|
||||
|
||||
readBody(req, res, (body) => {
|
||||
let body = ""
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk
|
||||
})
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const { sessionId, index } = JSON.parse(body)
|
||||
if (!sessionId || index === undefined) {
|
||||
@@ -408,7 +393,11 @@ function handleHistorySvgApi(
|
||||
return
|
||||
}
|
||||
|
||||
readBody(req, res, (body) => {
|
||||
let body = ""
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk
|
||||
})
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const { sessionId, svg } = JSON.parse(body)
|
||||
if (!sessionId || !svg) {
|
||||
|
||||
@@ -155,22 +155,19 @@ server.registerTool(
|
||||
server.registerTool(
|
||||
"create_new_diagram",
|
||||
{
|
||||
description: `Create a NEW diagram from mxGraphModel XML. ONLY use this when creating a diagram from scratch.
|
||||
|
||||
⚠️ DO NOT use this tool to modify an existing diagram — it will DESTROY all existing content and user changes. Use edit_diagram instead for ANY modifications to an existing diagram.
|
||||
description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
|
||||
|
||||
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
||||
|
||||
When to use this tool:
|
||||
- Creating a new diagram from scratch (no existing diagram)
|
||||
- The user explicitly asks to "start over" or "create a new diagram"
|
||||
- Creating a new diagram from scratch
|
||||
- Replacing the current diagram with a completely different one
|
||||
- Major structural changes that require regenerating the diagram
|
||||
|
||||
When to use edit_diagram instead (ALWAYS prefer edit_diagram if a diagram already exists):
|
||||
- ANY modifications to an existing diagram
|
||||
- Adding/removing/moving elements
|
||||
- Changing labels, colors, styles, or positions
|
||||
- Restructuring or reorganizing existing content
|
||||
- Adding new elements to an existing diagram
|
||||
When to use edit_diagram instead:
|
||||
- Small modifications to existing diagram
|
||||
- Adding/removing individual elements
|
||||
- Changing labels, colors, or positions
|
||||
|
||||
XML FORMAT - Full mxGraphModel structure:
|
||||
<mxGraphModel>
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 30 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 42 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 35 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 39 KiB |
134
scripts/fix-latest-yml.mjs
Normal file
134
scripts/fix-latest-yml.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Fix latest.yml metadata after Windows code signing.
|
||||
*
|
||||
* electron-builder generates latest.yml with hashes of unsigned executables.
|
||||
* After SignPath signs them, the hashes change. This script reads each
|
||||
* latest*.yml in release/, finds the matching signed exe in release-signed/,
|
||||
* and rewrites the sha512 and size fields.
|
||||
*
|
||||
* Usage: node scripts/fix-latest-yml.mjs
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto"
|
||||
import {
|
||||
createReadStream,
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
const RELEASE_DIR = "release"
|
||||
const SIGNED_DIR = "release-signed"
|
||||
|
||||
// Verify directories exist
|
||||
if (!existsSync(RELEASE_DIR)) {
|
||||
console.error(`Error: ${RELEASE_DIR}/ directory does not exist`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!existsSync(SIGNED_DIR)) {
|
||||
console.error(`Error: ${SIGNED_DIR}/ directory does not exist`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Find all latest*.yml files
|
||||
const ymlFiles = readdirSync(RELEASE_DIR).filter(
|
||||
(f) => f.startsWith("latest") && f.endsWith(".yml"),
|
||||
)
|
||||
|
||||
if (ymlFiles.length === 0) {
|
||||
console.log("No latest*.yml files found in release/")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute SHA-512 hash of a file using streaming (avoids loading large exe into memory)
|
||||
*/
|
||||
function hashFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = createHash("sha512")
|
||||
const stream = createReadStream(filePath)
|
||||
stream.on("data", (chunk) => hash.update(chunk))
|
||||
stream.on("end", () => resolve(hash.digest("base64")))
|
||||
stream.on("error", reject)
|
||||
})
|
||||
}
|
||||
|
||||
// Build a map of signed exe filenames to their hash and size
|
||||
const signedFiles = new Map()
|
||||
for (const f of readdirSync(SIGNED_DIR)) {
|
||||
if (!f.endsWith(".exe")) continue
|
||||
const filePath = join(SIGNED_DIR, f)
|
||||
const sha512 = await hashFile(filePath)
|
||||
const size = statSync(filePath).size
|
||||
signedFiles.set(f, { sha512, size })
|
||||
}
|
||||
|
||||
if (signedFiles.size === 0) {
|
||||
console.error("No signed .exe files found in release-signed/")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Found ${signedFiles.size} signed exe(s):`,
|
||||
[...signedFiles.keys()].join(", "),
|
||||
)
|
||||
|
||||
for (const ymlFile of ymlFiles) {
|
||||
const ymlPath = join(RELEASE_DIR, ymlFile)
|
||||
const content = readFileSync(ymlPath, "utf-8")
|
||||
const lines = content.split("\n")
|
||||
const outputLines = []
|
||||
|
||||
// Track the current file entry being processed (by url field)
|
||||
let currentExeName = null
|
||||
|
||||
for (const line of lines) {
|
||||
// Match " url: SomeFile.exe" inside the files array
|
||||
const urlMatch = line.match(/^\s+url:\s+(.+\.exe)\s*$/)
|
||||
if (urlMatch) {
|
||||
currentExeName = urlMatch[1]
|
||||
outputLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Match top-level "path: SomeFile.exe"
|
||||
const pathMatch = line.match(/^path:\s+(.+\.exe)\s*$/)
|
||||
if (pathMatch) {
|
||||
currentExeName = pathMatch[1]
|
||||
outputLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace sha512 lines
|
||||
const sha512Match = line.match(/^(\s*)sha512:\s+/)
|
||||
if (sha512Match && currentExeName && signedFiles.has(currentExeName)) {
|
||||
const indent = sha512Match[1]
|
||||
outputLines.push(
|
||||
`${indent}sha512: ${signedFiles.get(currentExeName).sha512}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace size lines
|
||||
const sizeMatch = line.match(/^(\s*)size:\s+\d+/)
|
||||
if (sizeMatch && currentExeName && signedFiles.has(currentExeName)) {
|
||||
const indent = sizeMatch[1]
|
||||
outputLines.push(
|
||||
`${indent}size: ${signedFiles.get(currentExeName).size}`,
|
||||
)
|
||||
// Reset current exe after processing both sha512 and size
|
||||
currentExeName = null
|
||||
continue
|
||||
}
|
||||
|
||||
outputLines.push(line)
|
||||
}
|
||||
|
||||
writeFileSync(ymlPath, outputLines.join("\n"))
|
||||
console.log(`Updated ${ymlFile}`)
|
||||
}
|
||||
|
||||
console.log("Done!")
|
||||
@@ -35,10 +35,8 @@ test.describe("Iframe Interaction", () => {
|
||||
await expect(
|
||||
frame
|
||||
.locator('text="Diagram"')
|
||||
.or(frame.locator('[title*="Diagram"]'))
|
||||
.filter({ visible: true })
|
||||
.first(),
|
||||
).toBeVisible({ timeout: 30000 })
|
||||
.or(frame.locator('[title*="Diagram"]')),
|
||||
).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test("diagram XML is rendered in iframe after generation", async ({
|
||||
|
||||
@@ -200,21 +200,12 @@ describe("supportsImageInput", () => {
|
||||
|
||||
it("returns true for Qwen vision models", () => {
|
||||
expect(supportsImageInput("qwen-vl")).toBe(true)
|
||||
expect(supportsImageInput("Qwen3.5")).toBe(true)
|
||||
expect(supportsImageInput("qwen3.5")).toBe(true)
|
||||
expect(supportsImageInput("qwen3.5-plus")).toBe(true)
|
||||
expect(supportsImageInput("qwen3.5-flash")).toBe(true)
|
||||
expect(supportsImageInput("qwen3-vl-plus")).toBe(true)
|
||||
expect(supportsImageInput("qwen3-vl-flash")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true for QvQ (Qwen Visual QA) models including OpenRouter-prefixed names", () => {
|
||||
expect(supportsImageInput("qvq-72b-preview")).toBe(true)
|
||||
expect(supportsImageInput("qvq-max")).toBe(true)
|
||||
expect(supportsImageInput("qwen/qvq-72b-preview")).toBe(true)
|
||||
expect(supportsImageInput("qwen/qvq-max")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for GLM text models", () => {
|
||||
expect(supportsImageInput("glm-4")).toBe(false)
|
||||
expect(supportsImageInput("glm-4-plus")).toBe(false)
|
||||
@@ -245,67 +236,6 @@ vi.mock("ollama-ai-provider-v2", () => {
|
||||
return { createOllama: mockCreateOllama, ollama: mockOllama }
|
||||
})
|
||||
|
||||
vi.mock("@ai-sdk/deepseek", () => {
|
||||
const mockModel = { modelId: "test-model" }
|
||||
const mockProviderFn = vi.fn(() => mockModel)
|
||||
const mockCreateDeepSeek = vi.fn(() => mockProviderFn)
|
||||
const mockDeepseek = vi.fn(() => mockModel)
|
||||
return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek }
|
||||
})
|
||||
|
||||
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
|
||||
let createDeepSeekMock: ReturnType<typeof vi.fn>
|
||||
const savedEnv: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(async () => {
|
||||
savedEnv.KIMI_API_KEY = process.env.KIMI_API_KEY
|
||||
savedEnv.KIMI_BASE_URL = process.env.KIMI_BASE_URL
|
||||
delete process.env.KIMI_BASE_URL
|
||||
|
||||
const mod = await import("@ai-sdk/deepseek")
|
||||
createDeepSeekMock = mod.createDeepSeek as ReturnType<typeof vi.fn>
|
||||
createDeepSeekMock.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.KIMI_API_KEY = savedEnv.KIMI_API_KEY
|
||||
process.env.KIMI_BASE_URL = savedEnv.KIMI_BASE_URL
|
||||
})
|
||||
|
||||
it("uses createDeepSeek with Kimi default base URL for reasoning_content support", () => {
|
||||
process.env.KIMI_API_KEY = "test-kimi-key"
|
||||
|
||||
getAIModel({
|
||||
provider: "kimi",
|
||||
apiKey: "test-kimi-key",
|
||||
modelId: "moonshot-v1-8k",
|
||||
})
|
||||
|
||||
expect(createDeepSeekMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseURL: "https://api.moonshot.cn/v1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("uses custom base URL when provided for kimi provider", () => {
|
||||
process.env.KIMI_API_KEY = "test-kimi-key"
|
||||
|
||||
getAIModel({
|
||||
provider: "kimi",
|
||||
apiKey: "test-kimi-key",
|
||||
baseUrl: "https://custom-kimi-endpoint.com/v1",
|
||||
modelId: "kimi-k2.6",
|
||||
})
|
||||
|
||||
expect(createDeepSeekMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseURL: "https://custom-kimi-endpoint.com/v1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Ollama API key security", () => {
|
||||
let createOllamaMock: ReturnType<typeof vi.fn>
|
||||
const savedEnv: Record<string, string | undefined> = {}
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"packages",
|
||||
"electron",
|
||||
"electron-standalone",
|
||||
"dist-electron",
|
||||
"release"
|
||||
"dist-electron"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user