mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
1 Commits
fix/404-er
...
54146f78cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54146f78cd |
7
.github/workflows/auto-format.yml
vendored
7
.github/workflows/auto-format.yml
vendored
@@ -20,10 +20,13 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Biome
|
||||||
|
run: npm install --save-dev @biomejs/biome
|
||||||
|
|
||||||
- name: Run Biome format
|
- name: Run Biome format
|
||||||
run: npx @biomejs/biome@latest check --write --no-errors-on-unmatched .
|
run: npx @biomejs/biome check --write --no-errors-on-unmatched .
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
id: changes
|
id: changes
|
||||||
|
|||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -25,11 +25,11 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '20'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
|
|||||||
2
.github/workflows/electron-release.yml
vendored
2
.github/workflows/electron-release.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 20
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Multi-stage Dockerfile for Next.js
|
# Multi-stage Dockerfile for Next.js
|
||||||
|
|
||||||
# Stage 1: Install dependencies
|
# Stage 1: Install dependencies
|
||||||
FROM node:24-alpine AS deps
|
FROM node:20-alpine AS deps
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -9,10 +9,10 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm ci
|
||||||
|
|
||||||
# Stage 2: Build application
|
# Stage 2: Build application
|
||||||
FROM node:24-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy node_modules from deps stage
|
# Copy node_modules from deps stage
|
||||||
@@ -34,7 +34,7 @@ ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE}
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: Production runtime
|
# Stage 3: Production runtime
|
||||||
FROM node:24-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -19,7 +19,6 @@ English | [中文](./docs/README_CN.md) | [日本語](./docs/README_JA.md)
|
|||||||
|
|
||||||
A Next.js web application that integrates AI capabilities with draw.io diagrams. Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
A Next.js web application that integrates AI capabilities with draw.io diagrams. Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
||||||
|
|
||||||
> Note: Thanks to <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) sponsorship, the demo site now uses the powerful K2-thinking model!
|
|
||||||
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
||||||
@@ -27,20 +26,17 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
|||||||
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Next AI Draw.io](#next-ai-drawio)
|
- [Next AI Draw.io ](#next-ai-drawio-)
|
||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [MCP Server (Preview)](#mcp-server-preview)
|
- [MCP Server (Preview)](#mcp-server-preview)
|
||||||
- [Claude Code CLI](#claude-code-cli)
|
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Try it Online](#try-it-online)
|
- [Try it Online](#try-it-online)
|
||||||
- [Desktop Application](#desktop-application)
|
- [Desktop Application](#desktop-application)
|
||||||
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Deployment](#deployment)
|
- [Deployment](#deployment)
|
||||||
- [Deploy on Vercel (Recommended)](#deploy-on-vercel-recommended)
|
|
||||||
- [Deploy on Cloudflare Workers](#deploy-on-cloudflare-workers)
|
|
||||||
- [Multi-Provider Support](#multi-provider-support)
|
- [Multi-Provider Support](#multi-provider-support)
|
||||||
- [How It Works](#how-it-works)
|
- [How It Works](#how-it-works)
|
||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
@@ -136,7 +132,7 @@ No installation needed! Try the app directly on our demo site:
|
|||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
> Note: Due to high traffic, the demo site currently uses minimax-m2. For best results, we recommend self-hosting with Claude Sonnet 4.5 or Claude Opus 4.5.
|
||||||
|
|
||||||
> **Bring Your Own API Key**: You can use your own API key to bypass usage limits on the demo site. Click the Settings icon in the chat panel to configure your provider and API key. Your key is stored locally in your browser and is never stored on the server.
|
> **Bring Your Own API Key**: You can use your own API key to bypass usage limits on the demo site. Click the Settings icon in the chat panel to configure your provider and API key. Your key is stored locally in your browser and is never stored on the server.
|
||||||
|
|
||||||
@@ -217,7 +213,7 @@ cp env.example .env.local
|
|||||||
|
|
||||||
Edit `.env.local` and configure your chosen provider:
|
Edit `.env.local` and configure your chosen provider:
|
||||||
|
|
||||||
- Set `AI_PROVIDER` to your chosen provider (doubao,bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||||
- Set `AI_MODEL` to the specific model you want to use
|
- Set `AI_MODEL` to the specific model you want to use
|
||||||
- Add the required API keys for your provider
|
- Add the required API keys for your provider
|
||||||
- `TEMPERATURE`: Optional temperature setting (e.g., `0` for deterministic output). Leave unset for models that don't support it (e.g., reasoning models).
|
- `TEMPERATURE`: Optional temperature setting (e.g., `0` for deterministic output). Leave unset for models that don't support it (e.g., reasoning models).
|
||||||
@@ -237,15 +233,16 @@ npm run dev
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Deploy on Vercel (Recommended)
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
|
|
||||||
|
Or you can deploy by this button.
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
The easiest way to deploy is using [Vercel](https://vercel.com/new), the creators of Next.js. Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
|
Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
|
||||||
|
|
||||||
See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
## Deploy on Cloudflare Workers
|
||||||
|
|
||||||
### Deploy on Cloudflare Workers
|
|
||||||
|
|
||||||
[Go to Cloudflare Deploy Guide](./docs/Cloudflare_Deploy.md)
|
[Go to Cloudflare Deploy Guide](./docs/Cloudflare_Deploy.md)
|
||||||
|
|
||||||
@@ -253,7 +250,6 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-
|
|||||||
|
|
||||||
## Multi-Provider Support
|
## Multi-Provider Support
|
||||||
|
|
||||||
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
|
||||||
- AWS Bedrock (default)
|
- AWS Bedrock (default)
|
||||||
- OpenAI
|
- OpenAI
|
||||||
- Anthropic
|
- Anthropic
|
||||||
@@ -264,7 +260,6 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-
|
|||||||
- DeepSeek
|
- DeepSeek
|
||||||
- SiliconFlow
|
- SiliconFlow
|
||||||
|
|
||||||
|
|
||||||
All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
||||||
|
|
||||||
📖 **[Detailed Provider Configuration Guide](./docs/ai-providers.md)** - See setup instructions for each provider.
|
📖 **[Detailed Provider Configuration Guide](./docs/ai-providers.md)** - See setup instructions for each provider.
|
||||||
@@ -305,8 +300,6 @@ public/ # Static assets including example images
|
|||||||
|
|
||||||
## Support & Contact
|
## Support & Contact
|
||||||
|
|
||||||
**Special thanks to [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) for sponsoring the API token usage of the demo site!** Register on the ARK platform to get 500K free tokens for all models!
|
|
||||||
|
|
||||||
If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site!
|
If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site!
|
||||||
|
|
||||||
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:
|
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:
|
||||||
|
|||||||
@@ -96,68 +96,72 @@ export default function AboutCN() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-rose-50 p-[1px] shadow-lg">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-rose-400 opacity-20" />
|
||||||
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
由字节跳动豆包提供支持
|
模型变更与用量限制{" "}
|
||||||
|
<span className="text-sm text-amber-600 font-medium italic font-normal">
|
||||||
|
(或者说:我的钱包顶不住了)
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Story */}
|
{/* Story */}
|
||||||
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
||||||
<p>
|
<p>
|
||||||
好消息!感谢{" "}
|
大家对这个项目的热情太高了——看来大家都真的很喜欢画图!但这也带来了一个幸福的烦恼:我们经常触发出上游
|
||||||
<a
|
AI 接口的频率限制
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
(TPS/TPM)。一旦超限,系统就会暂停,导致请求失败。
|
||||||
target="_blank"
|
</p>
|
||||||
rel="noopener noreferrer"
|
<p>
|
||||||
className="font-semibold text-blue-600 hover:underline"
|
由于使用量过高,我已将模型从 Opus 4.5 更换为{" "}
|
||||||
>
|
|
||||||
字节跳动豆包
|
|
||||||
</a>
|
|
||||||
的慷慨赞助,演示站点现已接入强大的{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
<span className="font-semibold text-amber-700">
|
||||||
K2-thinking
|
Haiku 4.5
|
||||||
</span>{" "}
|
|
||||||
模型,图表生成效果更佳!点击链接注册即可领取{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
50万免费Token
|
|
||||||
</span>
|
</span>
|
||||||
,适用于所有模型!
|
,以降低成本。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
作为一个
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
独立开发者
|
||||||
|
</span>
|
||||||
|
,目前的 API
|
||||||
|
费用全是我自己在掏腰包(纯属为爱发电)。为了保证服务能细水长流,同时也为了避免我个人陷入财务危机,我还设置了以下临时用量限制:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Usage Limits */}
|
{/* Limits Cards */}
|
||||||
<p className="text-sm text-gray-600 mb-3">
|
<div className="grid grid-cols-2 gap-3 mb-5">
|
||||||
当前使用限制:
|
<div className="rounded-xl bg-gradient-to-br from-amber-100 to-orange-100 p-4 text-center">
|
||||||
</p>
|
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
||||||
<div className="grid grid-cols-3 gap-3 mb-5">
|
Token 用量
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
</div>
|
||||||
<p className="text-lg font-bold text-amber-600">
|
<div className="text-lg font-bold text-gray-900">
|
||||||
{formatNumber(dailyRequestLimit)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
请求/天
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
|
||||||
<p className="text-lg font-bold text-amber-600">
|
|
||||||
{formatNumber(dailyTokenLimit)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
Token/天
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
|
||||||
<p className="text-lg font-bold text-amber-600">
|
|
||||||
{formatNumber(tpmLimit)}
|
{formatNumber(tpmLimit)}
|
||||||
</p>
|
<span className="text-sm font-normal text-gray-600">
|
||||||
<p className="text-xs text-gray-500">
|
/分钟
|
||||||
Token/分钟
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<div className="text-lg font-bold text-gray-900">
|
||||||
|
{formatNumber(dailyTokenLimit)}
|
||||||
|
<span className="text-sm font-normal text-gray-600">
|
||||||
|
/天
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-gradient-to-br from-amber-100 to-orange-100 p-4 text-center">
|
||||||
|
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
||||||
|
每日请求数
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{dailyRequestLimit}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
次
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -167,19 +171,48 @@ export default function AboutCN() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bring Your Own Key */}
|
{/* Bring Your Own Key */}
|
||||||
<div className="text-center">
|
<div className="text-center mb-5">
|
||||||
<h4 className="text-base font-bold text-gray-900 mb-2">
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
使用自己的 API Key
|
使用自己的 API Key
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
||||||
您也可以使用自己的 API
|
您可以使用自己的 API Key
|
||||||
Key,支持多种服务商。点击聊天面板中的设置图标即可配置。
|
来绕过这些限制。点击聊天面板中的设置图标即可配置您的
|
||||||
|
Provider 和 API Key。
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
||||||
您的 Key
|
您的 Key
|
||||||
仅保存在浏览器本地,不会被存储在服务器上。
|
仅保存在浏览器本地,不会被存储在服务器上。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 mb-5">
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sponsorship CTA */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
|
寻求赞助 (求大佬捞一把)
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-4 max-w-md mx-auto">
|
||||||
|
要想彻底解除这些限制,扩容后端是唯一的办法。我正在积极寻求
|
||||||
|
AI API 提供商或云平台的赞助。
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mb-4 max-w-md mx-auto">
|
||||||
|
作为回报(无论是额度支持还是资金支持),我将在
|
||||||
|
GitHub 仓库和 Live Demo
|
||||||
|
网站的显眼位置展示贵公司的 Logo
|
||||||
|
作为平台赞助商。
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="mailto:me@jiang.jp"
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white font-medium text-sm shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||||
|
>
|
||||||
|
联系我
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -344,16 +377,6 @@ export default function AboutCN() {
|
|||||||
多提供商支持
|
多提供商支持
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
字节跳动豆包
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>AWS Bedrock(默认)</li>
|
<li>AWS Bedrock(默认)</li>
|
||||||
<li>
|
<li>
|
||||||
OpenAI / OpenAI兼容API(通过{" "}
|
OpenAI / OpenAI兼容API(通过{" "}
|
||||||
@@ -365,7 +388,6 @@ export default function AboutCN() {
|
|||||||
<li>Ollama</li>
|
<li>Ollama</li>
|
||||||
<li>OpenRouter</li>
|
<li>OpenRouter</li>
|
||||||
<li>DeepSeek</li>
|
<li>DeepSeek</li>
|
||||||
<li>SiliconFlow</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
注意:<code>claude-sonnet-4-5</code>{" "}
|
注意:<code>claude-sonnet-4-5</code>{" "}
|
||||||
@@ -373,21 +395,18 @@ export default function AboutCN() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
支持与联系
|
<h2 className="text-2xl font-semibold text-gray-900">
|
||||||
</h2>
|
支持与联系
|
||||||
<p className="text-gray-700 mb-4 font-semibold">
|
</h2>
|
||||||
特别感谢{" "}
|
<iframe
|
||||||
<a
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
title="Sponsor DayuanJiang"
|
||||||
target="_blank"
|
height="32"
|
||||||
rel="noopener noreferrer"
|
width="114"
|
||||||
className="text-blue-600 hover:underline"
|
style={{ border: 0, borderRadius: 6 }}
|
||||||
>
|
/>
|
||||||
字节跳动豆包
|
</div>
|
||||||
</a>{" "}
|
|
||||||
为本站提供 API Token 支持!
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
如果您觉得这个项目有用,请考虑{" "}
|
如果您觉得这个项目有用,请考虑{" "}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -104,68 +104,73 @@ export default function AboutJA() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-rose-50 p-[1px] shadow-lg">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-rose-400 opacity-20" />
|
||||||
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
ByteDance Doubao提供
|
モデル変更と利用制限について{" "}
|
||||||
|
<span className="text-sm text-amber-600 font-medium italic font-normal">
|
||||||
|
(別名:お財布が悲鳴を上げています)
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Story */}
|
{/* Story */}
|
||||||
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
||||||
<p>
|
<p>
|
||||||
朗報です!
|
予想以上の反響をいただき、ありがとうございます!皆様にダイアグラム作成を楽しんでいただいているのは嬉しい限りですが、その熱量により
|
||||||
<a
|
AI API のレート制限 (TPS/TPM)
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
に頻繁に引っかかってしまっています。制限に達するとシステムが一時停止し、エラーが発生してしまいます。
|
||||||
target="_blank"
|
</p>
|
||||||
rel="noopener noreferrer"
|
<p>
|
||||||
className="font-semibold text-blue-600 hover:underline"
|
利用量の増加に伴い、コスト削減のためモデルを
|
||||||
>
|
Opus 4.5 から{" "}
|
||||||
ByteDance Doubao
|
|
||||||
</a>
|
|
||||||
様のご支援により、デモサイトでは強力な{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
<span className="font-semibold text-amber-700">
|
||||||
K2-thinking
|
Haiku 4.5
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
モデルを利用できるようになり、より高品質なダイアグラム生成が可能になりました。リンクから登録すると、すべてのモデルで使える{" "}
|
に変更しました。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
私は現在、
|
||||||
<span className="font-semibold text-amber-700">
|
<span className="font-semibold text-amber-700">
|
||||||
50万トークン
|
個人開発者
|
||||||
</span>
|
</span>
|
||||||
が無料でもらえます!
|
として API
|
||||||
|
費用を全額自腹で負担しています。サービスを継続し、かつ私自身が借金を背負わないようにするため(笑)、一時的に以下の利用制限も設けさせていただきました。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Usage Limits */}
|
{/* Limits Cards */}
|
||||||
<p className="text-sm text-gray-600 mb-3">
|
<div className="grid grid-cols-2 gap-3 mb-5">
|
||||||
現在の使用制限:
|
<div className="rounded-xl bg-gradient-to-br from-amber-100 to-orange-100 p-4 text-center">
|
||||||
</p>
|
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
||||||
<div className="grid grid-cols-3 gap-3 mb-5">
|
トークン使用量
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
</div>
|
||||||
<p className="text-lg font-bold text-amber-600">
|
<div className="text-lg font-bold text-gray-900">
|
||||||
{formatNumber(dailyRequestLimit)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
リクエスト/日
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
|
||||||
<p className="text-lg font-bold text-amber-600">
|
|
||||||
{formatNumber(dailyTokenLimit)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
トークン/日
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
|
||||||
<p className="text-lg font-bold text-amber-600">
|
|
||||||
{formatNumber(tpmLimit)}
|
{formatNumber(tpmLimit)}
|
||||||
</p>
|
<span className="text-sm font-normal text-gray-600">
|
||||||
<p className="text-xs text-gray-500">
|
/分
|
||||||
トークン/分
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<div className="text-lg font-bold text-gray-900">
|
||||||
|
{formatNumber(dailyTokenLimit)}
|
||||||
|
<span className="text-sm font-normal text-gray-600">
|
||||||
|
/日
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-gradient-to-br from-amber-100 to-orange-100 p-4 text-center">
|
||||||
|
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
||||||
|
1日のリクエスト数
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{dailyRequestLimit}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
回
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,17 +180,44 @@ export default function AboutJA() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bring Your Own Key */}
|
{/* Bring Your Own Key */}
|
||||||
<div className="text-center">
|
<div className="text-center mb-5">
|
||||||
<h4 className="text-base font-bold text-gray-900 mb-2">
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
自分のAPIキーを使用
|
自分のAPIキーを使用
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
||||||
お好みのプロバイダーで自分のAPIキーを使用することもできます。チャットパネルの設定アイコンをクリックして設定してください。
|
自分のAPIキーを使用することで、これらの制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
||||||
キーはブラウザのローカルに保存され、サーバーには保存されません。
|
キーはブラウザのローカルに保存され、サーバーには保存されません。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 mb-5">
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sponsorship CTA */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
|
スポンサー募集
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-4 max-w-md mx-auto">
|
||||||
|
これらの制限を取り払い、バックエンドをスケールさせるには皆様の支援が必要です。現在、AI
|
||||||
|
API
|
||||||
|
プロバイダー様やクラウドプラットフォーム様からのスポンサー支援を積極的に募集しています。
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mb-4 max-w-md mx-auto">
|
||||||
|
ご支援(クレジット提供や資金援助)をいただける場合、GitHub
|
||||||
|
リポジトリおよびデモサイトにて、プラットフォームスポンサーとして貴社を大々的にご紹介させていただきます。
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="mailto:me@jiang.jp"
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white font-medium text-sm shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||||
|
>
|
||||||
|
お問い合わせ
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -359,16 +391,6 @@ export default function AboutJA() {
|
|||||||
マルチプロバイダーサポート
|
マルチプロバイダーサポート
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
ByteDance Doubao
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>AWS Bedrock(デフォルト)</li>
|
<li>AWS Bedrock(デフォルト)</li>
|
||||||
<li>
|
<li>
|
||||||
OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>
|
OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>
|
||||||
@@ -380,7 +402,6 @@ export default function AboutJA() {
|
|||||||
<li>Ollama</li>
|
<li>Ollama</li>
|
||||||
<li>OpenRouter</li>
|
<li>OpenRouter</li>
|
||||||
<li>DeepSeek</li>
|
<li>DeepSeek</li>
|
||||||
<li>SiliconFlow</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
注:<code>claude-sonnet-4-5</code>
|
注:<code>claude-sonnet-4-5</code>
|
||||||
@@ -388,21 +409,18 @@ export default function AboutJA() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
サポート&お問い合わせ
|
<h2 className="text-2xl font-semibold text-gray-900">
|
||||||
</h2>
|
サポート&お問い合わせ
|
||||||
<p className="text-gray-700 mb-4 font-semibold">
|
</h2>
|
||||||
デモサイトのAPIトークン使用を支援してくださった{" "}
|
<iframe
|
||||||
<a
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
title="Sponsor DayuanJiang"
|
||||||
target="_blank"
|
height="32"
|
||||||
rel="noopener noreferrer"
|
width="114"
|
||||||
className="text-blue-600 hover:underline"
|
style={{ border: 0, borderRadius: 6 }}
|
||||||
>
|
/>
|
||||||
ByteDance Doubao
|
</div>
|
||||||
</a>{" "}
|
|
||||||
様に、心より感謝申し上げます。
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -104,70 +104,79 @@ export default function About() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-rose-50 p-[1px] shadow-lg">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-rose-400 opacity-20" />
|
||||||
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
Sponsored by ByteDance Doubao
|
Model Change & Usage Limits{" "}
|
||||||
|
<span className="text-sm text-amber-600 font-medium italic font-normal">
|
||||||
|
(Or: Why My Wallet is Crying)
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Story */}
|
{/* Story */}
|
||||||
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
||||||
<p>
|
<p>
|
||||||
Great news! Thanks to the generous
|
The response to this project has been
|
||||||
sponsorship from{" "}
|
incredible—you all love making diagrams!
|
||||||
<a
|
However, this enthusiasm means we are
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
frequently hitting the AI API rate limits
|
||||||
target="_blank"
|
(TPS/TPM). When this happens, the system
|
||||||
rel="noopener noreferrer"
|
pauses, leading to failed requests.
|
||||||
className="font-semibold text-blue-600 hover:underline"
|
</p>
|
||||||
>
|
<p>
|
||||||
ByteDance Doubao
|
Due to the high usage, I have changed the
|
||||||
</a>
|
model from Opus 4.5 to{" "}
|
||||||
, the demo site now uses the powerful{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
<span className="font-semibold text-amber-700">
|
||||||
K2-thinking
|
Haiku 4.5
|
||||||
</span>{" "}
|
</span>
|
||||||
model for better diagram generation! Sign up
|
, which is more cost-effective.
|
||||||
via the link to get{" "}
|
</p>
|
||||||
|
<p>
|
||||||
|
As an{" "}
|
||||||
<span className="font-semibold text-amber-700">
|
<span className="font-semibold text-amber-700">
|
||||||
500K free tokens
|
indie developer
|
||||||
</span>{" "}
|
</span>
|
||||||
for all models!
|
, I am currently footing the entire API
|
||||||
|
bill. To keep the lights on and ensure the
|
||||||
|
service remains available to everyone
|
||||||
|
without sending me into debt, I have also
|
||||||
|
implemented the following temporary caps:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Usage Limits */}
|
{/* Limits Cards */}
|
||||||
<p className="text-sm text-gray-600 mb-3">
|
<div className="grid grid-cols-2 gap-3 mb-5">
|
||||||
Please note the current usage limits:
|
<div className="rounded-xl bg-gradient-to-br from-amber-100 to-orange-100 p-4 text-center">
|
||||||
</p>
|
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
||||||
<div className="grid grid-cols-3 gap-3 mb-5">
|
Token Usage
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
</div>
|
||||||
<p className="text-lg font-bold text-amber-600">
|
<div className="text-lg font-bold text-gray-900">
|
||||||
{formatNumber(dailyRequestLimit)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
requests/day
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
|
||||||
<p className="text-lg font-bold text-amber-600">
|
|
||||||
{formatNumber(dailyTokenLimit)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
tokens/day
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-3 bg-white/60 rounded-lg">
|
|
||||||
<p className="text-lg font-bold text-amber-600">
|
|
||||||
{formatNumber(tpmLimit)}
|
{formatNumber(tpmLimit)}
|
||||||
</p>
|
<span className="text-sm font-normal text-gray-600">
|
||||||
<p className="text-xs text-gray-500">
|
/min
|
||||||
tokens/min
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<div className="text-lg font-bold text-gray-900">
|
||||||
|
{formatNumber(dailyTokenLimit)}
|
||||||
|
<span className="text-sm font-normal text-gray-600">
|
||||||
|
/day
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-gradient-to-br from-amber-100 to-orange-100 p-4 text-center">
|
||||||
|
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
||||||
|
Daily Requests
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
|
{dailyRequestLimit}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
requests
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -177,21 +186,51 @@ export default function About() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bring Your Own Key */}
|
{/* Bring Your Own Key */}
|
||||||
<div className="text-center">
|
<div className="text-center mb-5">
|
||||||
<h4 className="text-base font-bold text-gray-900 mb-2">
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
Bring Your Own API Key
|
Bring Your Own API Key
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
||||||
You can also use your own API key with any
|
You can use your own API key to bypass these
|
||||||
supported provider. Click the Settings icon
|
limits. Click the Settings icon in the chat
|
||||||
in the chat panel to configure your provider
|
panel to configure your provider and API
|
||||||
and API key.
|
key.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
||||||
Your key is stored locally in your browser
|
Your key is stored locally in your browser
|
||||||
and is never stored on the server.
|
and is never stored on the server.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 mb-5">
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sponsorship CTA */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
|
Call for Sponsorship
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-4 max-w-md mx-auto">
|
||||||
|
Scaling the backend is the only way to
|
||||||
|
remove these limits. I am actively seeking
|
||||||
|
sponsorship from AI API providers or Cloud
|
||||||
|
Platforms.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mb-4 max-w-md mx-auto">
|
||||||
|
In return for support (credits or funding),
|
||||||
|
I will prominently feature your company as a
|
||||||
|
platform sponsor on both the GitHub
|
||||||
|
repository and the live demo site.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="mailto:me@jiang.jp"
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white font-medium text-sm shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||||
|
>
|
||||||
|
Contact Me
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -378,16 +417,6 @@ export default function About() {
|
|||||||
Multi-Provider Support
|
Multi-Provider Support
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
ByteDance Doubao
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>AWS Bedrock (default)</li>
|
<li>AWS Bedrock (default)</li>
|
||||||
<li>
|
<li>
|
||||||
OpenAI / OpenAI-compatible APIs (via{" "}
|
OpenAI / OpenAI-compatible APIs (via{" "}
|
||||||
@@ -399,7 +428,6 @@ export default function About() {
|
|||||||
<li>Ollama</li>
|
<li>Ollama</li>
|
||||||
<li>OpenRouter</li>
|
<li>OpenRouter</li>
|
||||||
<li>DeepSeek</li>
|
<li>DeepSeek</li>
|
||||||
<li>SiliconFlow</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
Note that <code>claude-sonnet-4-5</code> has trained on
|
Note that <code>claude-sonnet-4-5</code> has trained on
|
||||||
@@ -409,21 +437,18 @@ export default function About() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
Support & Contact
|
<h2 className="text-2xl font-semibold text-gray-900">
|
||||||
</h2>
|
Support & Contact
|
||||||
<p className="text-gray-700 mb-4 font-semibold">
|
</h2>
|
||||||
Special thanks to{" "}
|
<iframe
|
||||||
<a
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
title="Sponsor DayuanJiang"
|
||||||
target="_blank"
|
height="32"
|
||||||
rel="noopener noreferrer"
|
width="114"
|
||||||
className="text-blue-600 hover:underline"
|
style={{ border: 0, borderRadius: 6 }}
|
||||||
>
|
/>
|
||||||
ByteDance Doubao
|
</div>
|
||||||
</a>{" "}
|
|
||||||
for sponsoring the API token usage of the demo site!
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
If you find this project useful, please consider{" "}
|
If you find this project useful, please consider{" "}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ Notes:
|
|||||||
Operations:
|
Operations:
|
||||||
- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.
|
- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.
|
||||||
- add: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
- add: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
||||||
- delete: Remove a cell. Cascade is automatic: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id.
|
- delete: Remove a cell by its id. Only cell_id is needed.
|
||||||
|
|
||||||
For update/add, new_xml must be a complete mxCell element including mxGeometry.
|
For update/add, new_xml must be a complete mxCell element including mxGeometry.
|
||||||
|
|
||||||
@@ -614,8 +614,8 @@ For update/add, new_xml must be a complete mxCell element including mxGeometry.
|
|||||||
Example - Add a rectangle:
|
Example - Add a rectangle:
|
||||||
{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
|
{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
|
||||||
|
|
||||||
Example - Delete container (children & edges auto-deleted):
|
Example - Delete a cell:
|
||||||
{"operations": [{"operation": "delete", "cell_id": "2"}]}`,
|
{"operations": [{"operation": "delete", "cell_id": "rect-1"}]}`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
operations: z
|
operations: z
|
||||||
.array(
|
.array(
|
||||||
|
|||||||
@@ -225,27 +225,6 @@ export async function POST(req: Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "sglang": {
|
|
||||||
// SGLang is OpenAI-compatible
|
|
||||||
const sglang = createOpenAI({
|
|
||||||
apiKey: apiKey || "not-needed",
|
|
||||||
baseURL: baseUrl || "http://127.0.0.1:8000/v1",
|
|
||||||
})
|
|
||||||
model = sglang.chat(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "doubao": {
|
|
||||||
// ByteDance Doubao uses DeepSeek-compatible API
|
|
||||||
const doubao = createDeepSeek({
|
|
||||||
apiKey,
|
|
||||||
baseURL:
|
|
||||||
baseUrl || "https://ark.cn-beijing.volces.com/api/v3",
|
|
||||||
})
|
|
||||||
model = doubao(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ valid: false, error: `Unknown provider: ${provider}` },
|
{ valid: false, error: `Unknown provider: ${provider}` },
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ import { HistoryDialog } from "@/components/history-dialog"
|
|||||||
import { ModelSelector } from "@/components/model-selector"
|
import { ModelSelector } from "@/components/model-selector"
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SaveDialog } from "@/components/save-dialog"
|
import { SaveDialog } from "@/components/save-dialog"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { formatMessage } from "@/lib/i18n/utils"
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
@@ -147,9 +152,12 @@ interface ChatInputProps {
|
|||||||
File,
|
File,
|
||||||
{ text: string; charCount: number; isExtracting: boolean }
|
{ text: string; charCount: number; isExtracting: boolean }
|
||||||
>
|
>
|
||||||
|
showHistory?: boolean
|
||||||
|
onToggleHistory?: (show: boolean) => void
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
error?: Error | null
|
error?: Error | null
|
||||||
|
minimalStyle?: boolean
|
||||||
|
onMinimalStyleChange?: (value: boolean) => void
|
||||||
// Model selector props
|
// Model selector props
|
||||||
models?: FlattenedModel[]
|
models?: FlattenedModel[]
|
||||||
selectedModelId?: string
|
selectedModelId?: string
|
||||||
@@ -167,8 +175,12 @@ export function ChatInput({
|
|||||||
files = [],
|
files = [],
|
||||||
onFileChange = () => {},
|
onFileChange = () => {},
|
||||||
pdfData = new Map(),
|
pdfData = new Map(),
|
||||||
|
showHistory = false,
|
||||||
|
onToggleHistory = () => {},
|
||||||
sessionId,
|
sessionId,
|
||||||
error = null,
|
error = null,
|
||||||
|
minimalStyle = false,
|
||||||
|
onMinimalStyleChange = () => {},
|
||||||
models = [],
|
models = [],
|
||||||
selectedModelId,
|
selectedModelId,
|
||||||
onModelSelect = () => {},
|
onModelSelect = () => {},
|
||||||
@@ -176,14 +188,16 @@ export function ChatInput({
|
|||||||
onConfigureModels = () => {},
|
onConfigureModels = () => {},
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
const {
|
||||||
|
diagramHistory,
|
||||||
|
saveDiagramToFile,
|
||||||
|
showSaveDialog,
|
||||||
|
setShowSaveDialog,
|
||||||
|
} = useDiagram()
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [showClearDialog, setShowClearDialog] = useState(false)
|
const [showClearDialog, setShowClearDialog] = useState(false)
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
|
||||||
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
|
||||||
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
(status === "streaming" || status === "submitted") && !error
|
(status === "streaming" || status === "submitted") && !error
|
||||||
@@ -371,58 +385,99 @@ export function ChatInput({
|
|||||||
onOpenChange={setShowClearDialog}
|
onOpenChange={setShowClearDialog}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<HistoryDialog
|
||||||
|
showHistory={showHistory}
|
||||||
|
onToggleHistory={onToggleHistory}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Switch
|
||||||
|
id="minimal-style"
|
||||||
|
checked={minimalStyle}
|
||||||
|
onCheckedChange={onMinimalStyleChange}
|
||||||
|
className="scale-75"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="minimal-style"
|
||||||
|
className={`text-xs cursor-pointer select-none ${
|
||||||
|
minimalStyle
|
||||||
|
? "text-primary font-medium"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{minimalStyle
|
||||||
|
? dict.chat.minimalStyle
|
||||||
|
: dict.chat.styledMode}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{dict.chat.minimalTooltip}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
||||||
<div className="flex items-center gap-1 overflow-x-hidden">
|
<ButtonWithTooltip
|
||||||
<ButtonWithTooltip
|
type="button"
|
||||||
type="button"
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => onToggleHistory(true)}
|
||||||
onClick={() => setShowHistory(true)}
|
disabled={isDisabled || diagramHistory.length === 0}
|
||||||
disabled={
|
tooltipContent={dict.chat.diagramHistory}
|
||||||
isDisabled || diagramHistory.length === 0
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
}
|
>
|
||||||
tooltipContent={dict.chat.diagramHistory}
|
<History className="h-4 w-4" />
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
</ButtonWithTooltip>
|
||||||
>
|
|
||||||
<History className="h-4 w-4" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowSaveDialog(true)}
|
onClick={() => setShowSaveDialog(true)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
tooltipContent={dict.chat.saveDiagram}
|
tooltipContent={dict.chat.saveDiagram}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
<ButtonWithTooltip
|
<SaveDialog
|
||||||
type="button"
|
open={showSaveDialog}
|
||||||
variant="ghost"
|
onOpenChange={setShowSaveDialog}
|
||||||
size="sm"
|
onSave={(filename, format) =>
|
||||||
onClick={triggerFileInput}
|
saveDiagramToFile(filename, format, sessionId)
|
||||||
disabled={isDisabled}
|
}
|
||||||
tooltipContent={dict.chat.uploadFile}
|
defaultFilename={`diagram-${new Date()
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
.toISOString()
|
||||||
>
|
.slice(0, 10)}`}
|
||||||
<ImageIcon className="h-4 w-4" />
|
/>
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
<ButtonWithTooltip
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={triggerFileInput}
|
||||||
|
disabled={isDisabled}
|
||||||
|
tooltipContent={dict.chat.uploadFile}
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ImageIcon className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
|
||||||
|
multiple
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
className="hidden"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
|
|
||||||
multiple
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
models={models}
|
models={models}
|
||||||
selectedModelId={selectedModelId}
|
selectedModelId={selectedModelId}
|
||||||
@@ -431,7 +486,9 @@ export function ChatInput({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
showUnvalidatedModels={showUnvalidatedModels}
|
showUnvalidatedModels={showUnvalidatedModels}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-border mx-1" />
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isDisabled || !input.trim()}
|
disabled={isDisabled || !input.trim()}
|
||||||
@@ -453,20 +510,6 @@ export function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HistoryDialog
|
|
||||||
showHistory={showHistory}
|
|
||||||
onToggleHistory={setShowHistory}
|
|
||||||
/>
|
|
||||||
<SaveDialog
|
|
||||||
open={showSaveDialog}
|
|
||||||
onOpenChange={setShowSaveDialog}
|
|
||||||
onSave={(filename, format) =>
|
|
||||||
saveDiagramToFile(filename, format, sessionId)
|
|
||||||
}
|
|
||||||
defaultFilename={`diagram-${new Date()
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, 10)}`}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ import Link from "next/link"
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { flushSync } from "react-dom"
|
import { flushSync } from "react-dom"
|
||||||
|
import { FaGithub } from "react-icons/fa"
|
||||||
import { Toaster, toast } from "sonner"
|
import { Toaster, toast } from "sonner"
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
import { ChatInput } from "@/components/chat-input"
|
import { ChatInput } from "@/components/chat-input"
|
||||||
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
|
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
@@ -148,6 +154,7 @@ export default function ChatPanel({
|
|||||||
// File processing using extracted hook
|
// File processing using extracted hook
|
||||||
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
|
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
|
||||||
|
|
||||||
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||||
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
||||||
|
|
||||||
@@ -185,7 +192,6 @@ export default function ChatPanel({
|
|||||||
dailyRequestLimit,
|
dailyRequestLimit,
|
||||||
dailyTokenLimit,
|
dailyTokenLimit,
|
||||||
tpmLimit,
|
tpmLimit,
|
||||||
onConfigModel: () => setShowModelConfigDialog(true),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
|
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
|
||||||
@@ -241,178 +247,182 @@ export default function ChatPanel({
|
|||||||
onExport,
|
onExport,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { messages, sendMessage, addToolOutput, status, error, setMessages } =
|
const {
|
||||||
useChat({
|
messages,
|
||||||
transport: new DefaultChatTransport({
|
sendMessage,
|
||||||
api: getApiEndpoint("/api/chat"),
|
addToolOutput,
|
||||||
}),
|
stop,
|
||||||
onToolCall: async ({ toolCall }) => {
|
status,
|
||||||
await handleToolCall({ toolCall }, addToolOutput)
|
error,
|
||||||
},
|
setMessages,
|
||||||
onError: (error) => {
|
} = useChat({
|
||||||
// Handle server-side quota limit (429 response)
|
transport: new DefaultChatTransport({
|
||||||
// AI SDK puts the full response body in error.message for non-OK responses
|
api: getApiEndpoint("/api/chat"),
|
||||||
try {
|
}),
|
||||||
const data = JSON.parse(error.message)
|
onToolCall: async ({ toolCall }) => {
|
||||||
if (data.type === "request") {
|
await handleToolCall({ toolCall }, addToolOutput)
|
||||||
quotaManager.showQuotaLimitToast(data.used, data.limit)
|
},
|
||||||
return
|
onError: (error) => {
|
||||||
}
|
// Handle server-side quota limit (429 response)
|
||||||
if (data.type === "token") {
|
// AI SDK puts the full response body in error.message for non-OK responses
|
||||||
quotaManager.showTokenLimitToast(data.used, data.limit)
|
try {
|
||||||
return
|
const data = JSON.parse(error.message)
|
||||||
}
|
if (data.type === "request") {
|
||||||
if (data.type === "tpm") {
|
quotaManager.showQuotaLimitToast(data.used, data.limit)
|
||||||
quotaManager.showTPMLimitToast(data.limit)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not JSON, fall through to string matching for backwards compatibility
|
|
||||||
}
|
}
|
||||||
|
if (data.type === "token") {
|
||||||
|
quotaManager.showTokenLimitToast(data.used, data.limit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data.type === "tpm") {
|
||||||
|
quotaManager.showTPMLimitToast(data.limit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, fall through to string matching for backwards compatibility
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to string matching
|
// Fallback to string matching
|
||||||
if (error.message.includes("Daily request limit")) {
|
if (error.message.includes("Daily request limit")) {
|
||||||
quotaManager.showQuotaLimitToast()
|
quotaManager.showQuotaLimitToast()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (error.message.includes("Daily token limit")) {
|
if (error.message.includes("Daily token limit")) {
|
||||||
quotaManager.showTokenLimitToast()
|
quotaManager.showTokenLimitToast()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
error.message.includes("Rate limit exceeded") ||
|
error.message.includes("Rate limit exceeded") ||
|
||||||
error.message.includes("tokens per minute")
|
error.message.includes("tokens per minute")
|
||||||
) {
|
) {
|
||||||
quotaManager.showTPMLimitToast()
|
quotaManager.showTPMLimitToast()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Silence access code error in console since it's handled by UI
|
// Silence access code error in console since it's handled by UI
|
||||||
if (!error.message.includes("Invalid or missing access code")) {
|
if (!error.message.includes("Invalid or missing access code")) {
|
||||||
console.error("Chat error:", error)
|
console.error("Chat error:", error)
|
||||||
// Debug: Log messages structure when error occurs
|
// Debug: Log messages structure when error occurs
|
||||||
console.log("[onError] messages count:", messages.length)
|
console.log("[onError] messages count:", messages.length)
|
||||||
messages.forEach((msg, idx) => {
|
messages.forEach((msg, idx) => {
|
||||||
console.log(`[onError] Message ${idx}:`, {
|
console.log(`[onError] Message ${idx}:`, {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
partsCount: msg.parts?.length,
|
partsCount: msg.parts?.length,
|
||||||
})
|
|
||||||
if (msg.parts) {
|
|
||||||
msg.parts.forEach((part: any, partIdx: number) => {
|
|
||||||
console.log(
|
|
||||||
`[onError] Part ${partIdx}:`,
|
|
||||||
JSON.stringify({
|
|
||||||
type: part.type,
|
|
||||||
toolName: part.toolName,
|
|
||||||
hasInput: !!part.input,
|
|
||||||
inputType: typeof part.input,
|
|
||||||
inputKeys:
|
|
||||||
part.input &&
|
|
||||||
typeof part.input === "object"
|
|
||||||
? Object.keys(part.input)
|
|
||||||
: null,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
if (msg.parts) {
|
||||||
|
msg.parts.forEach((part: any, partIdx: number) => {
|
||||||
// Translate technical errors into user-friendly messages
|
console.log(
|
||||||
// The server now handles detailed error messages, so we can display them directly.
|
`[onError] Part ${partIdx}:`,
|
||||||
// But we still handle connection/network errors that happen before reaching the server.
|
JSON.stringify({
|
||||||
let friendlyMessage = error.message
|
type: part.type,
|
||||||
|
toolName: part.toolName,
|
||||||
// Simple check for network errors if message is generic
|
hasInput: !!part.input,
|
||||||
if (friendlyMessage === "Failed to fetch") {
|
inputType: typeof part.input,
|
||||||
friendlyMessage =
|
inputKeys:
|
||||||
"Network error. Please check your connection."
|
part.input &&
|
||||||
}
|
typeof part.input === "object"
|
||||||
|
? Object.keys(part.input)
|
||||||
// Truncated tool input error (model output limit too low)
|
: null,
|
||||||
if (friendlyMessage.includes("toolUse.input is invalid")) {
|
}),
|
||||||
friendlyMessage =
|
)
|
||||||
"Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength."
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Translate image not supported error
|
|
||||||
if (friendlyMessage.includes("image content block")) {
|
|
||||||
friendlyMessage = "This model doesn't support image input."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add system message for error so it can be cleared
|
|
||||||
setMessages((currentMessages) => {
|
|
||||||
const errorMessage = {
|
|
||||||
id: `error-${Date.now()}`,
|
|
||||||
role: "system" as const,
|
|
||||||
content: friendlyMessage,
|
|
||||||
parts: [
|
|
||||||
{ type: "text" as const, text: friendlyMessage },
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
return [...currentMessages, errorMessage]
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (error.message.includes("Invalid or missing access code")) {
|
// Translate technical errors into user-friendly messages
|
||||||
// Show settings dialog to help user fix it
|
// The server now handles detailed error messages, so we can display them directly.
|
||||||
setShowSettingsDialog(true)
|
// But we still handle connection/network errors that happen before reaching the server.
|
||||||
|
let friendlyMessage = error.message
|
||||||
|
|
||||||
|
// Simple check for network errors if message is generic
|
||||||
|
if (friendlyMessage === "Failed to fetch") {
|
||||||
|
friendlyMessage = "Network error. Please check your connection."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncated tool input error (model output limit too low)
|
||||||
|
if (friendlyMessage.includes("toolUse.input is invalid")) {
|
||||||
|
friendlyMessage =
|
||||||
|
"Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate image not supported error
|
||||||
|
if (friendlyMessage.includes("image content block")) {
|
||||||
|
friendlyMessage = "This model doesn't support image input."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add system message for error so it can be cleared
|
||||||
|
setMessages((currentMessages) => {
|
||||||
|
const errorMessage = {
|
||||||
|
id: `error-${Date.now()}`,
|
||||||
|
role: "system" as const,
|
||||||
|
content: friendlyMessage,
|
||||||
|
parts: [{ type: "text" as const, text: friendlyMessage }],
|
||||||
}
|
}
|
||||||
},
|
return [...currentMessages, errorMessage]
|
||||||
onFinish: ({ message }) => {
|
})
|
||||||
// Track actual token usage from server metadata
|
|
||||||
const metadata = message?.metadata as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
// DEBUG: Log finish reason to diagnose truncation
|
if (error.message.includes("Invalid or missing access code")) {
|
||||||
console.log("[onFinish] finishReason:", metadata?.finishReason)
|
// Show settings dialog to help user fix it
|
||||||
},
|
setShowSettingsDialog(true)
|
||||||
sendAutomaticallyWhen: ({ messages }) => {
|
}
|
||||||
const isInContinuationMode = partialXmlRef.current.length > 0
|
},
|
||||||
|
onFinish: ({ message }) => {
|
||||||
|
// Track actual token usage from server metadata
|
||||||
|
const metadata = message?.metadata as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined
|
||||||
|
|
||||||
const shouldRetry = hasToolErrors(
|
// DEBUG: Log finish reason to diagnose truncation
|
||||||
messages as unknown as ChatMessage[],
|
console.log("[onFinish] finishReason:", metadata?.finishReason)
|
||||||
)
|
},
|
||||||
|
sendAutomaticallyWhen: ({ messages }) => {
|
||||||
|
const isInContinuationMode = partialXmlRef.current.length > 0
|
||||||
|
|
||||||
if (!shouldRetry) {
|
const shouldRetry = hasToolErrors(
|
||||||
// No error, reset retry count and clear state
|
messages as unknown as ChatMessage[],
|
||||||
autoRetryCountRef.current = 0
|
)
|
||||||
|
|
||||||
|
if (!shouldRetry) {
|
||||||
|
// No error, reset retry count and clear state
|
||||||
|
autoRetryCountRef.current = 0
|
||||||
|
continuationRetryCountRef.current = 0
|
||||||
|
partialXmlRef.current = ""
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continuation mode: limited retries for truncation handling
|
||||||
|
if (isInContinuationMode) {
|
||||||
|
if (
|
||||||
|
continuationRetryCountRef.current >=
|
||||||
|
MAX_CONTINUATION_RETRY_COUNT
|
||||||
|
) {
|
||||||
|
toast.error(
|
||||||
|
`Continuation retry limit reached (${MAX_CONTINUATION_RETRY_COUNT}). The diagram may be too complex.`,
|
||||||
|
)
|
||||||
continuationRetryCountRef.current = 0
|
continuationRetryCountRef.current = 0
|
||||||
partialXmlRef.current = ""
|
partialXmlRef.current = ""
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
continuationRetryCountRef.current++
|
||||||
// Continuation mode: limited retries for truncation handling
|
} else {
|
||||||
if (isInContinuationMode) {
|
// Regular error: check retry count limit
|
||||||
if (
|
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
|
||||||
continuationRetryCountRef.current >=
|
toast.error(
|
||||||
MAX_CONTINUATION_RETRY_COUNT
|
`Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`,
|
||||||
) {
|
)
|
||||||
toast.error(
|
autoRetryCountRef.current = 0
|
||||||
`Continuation retry limit reached (${MAX_CONTINUATION_RETRY_COUNT}). The diagram may be too complex.`,
|
partialXmlRef.current = ""
|
||||||
)
|
return false
|
||||||
continuationRetryCountRef.current = 0
|
|
||||||
partialXmlRef.current = ""
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
continuationRetryCountRef.current++
|
|
||||||
} else {
|
|
||||||
// Regular error: check retry count limit
|
|
||||||
if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) {
|
|
||||||
toast.error(
|
|
||||||
`Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`,
|
|
||||||
)
|
|
||||||
autoRetryCountRef.current = 0
|
|
||||||
partialXmlRef.current = ""
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Increment retry count for actual errors
|
|
||||||
autoRetryCountRef.current++
|
|
||||||
}
|
}
|
||||||
|
// Increment retry count for actual errors
|
||||||
|
autoRetryCountRef.current++
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ref to track latest messages for unload persistence
|
// Ref to track latest messages for unload persistence
|
||||||
const messagesRef = useRef(messages)
|
const messagesRef = useRef(messages)
|
||||||
@@ -949,32 +959,18 @@ export default function ChatPanel({
|
|||||||
Next AI Drawio
|
Next AI Drawio
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{!isMobile && (
|
{!isMobile &&
|
||||||
<Link
|
process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
||||||
href="/about"
|
"true" && (
|
||||||
target="_blank"
|
<Link
|
||||||
rel="noopener noreferrer"
|
href="/about"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
About
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{!isMobile && (
|
|
||||||
<Link
|
|
||||||
href="/about"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<ButtonWithTooltip
|
|
||||||
tooltipContent="Sponsored by ByteDance Doubao K2-thinking. See About page for details."
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6 text-amber-500 hover:text-amber-600"
|
|
||||||
>
|
>
|
||||||
<AlertTriangle className="h-4 w-4" />
|
About
|
||||||
</ButtonWithTooltip>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 justify-end overflow-visible">
|
<div className="flex items-center gap-1 justify-end overflow-visible">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
@@ -988,6 +984,23 @@ export default function ChatPanel({
|
|||||||
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||||
/>
|
/>
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-center h-9 w-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<FaGithub
|
||||||
|
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{dict.nav.github}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent={dict.nav.settings}
|
tooltipContent={dict.nav.settings}
|
||||||
@@ -1037,9 +1050,6 @@ export default function ChatPanel({
|
|||||||
<DevXmlSimulator
|
<DevXmlSimulator
|
||||||
setMessages={setMessages}
|
setMessages={setMessages}
|
||||||
onDisplayChart={onDisplayChart}
|
onDisplayChart={onDisplayChart}
|
||||||
onShowQuotaToast={() =>
|
|
||||||
quotaManager.showQuotaLimitToast(50, 50)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1056,8 +1066,12 @@ export default function ChatPanel({
|
|||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
pdfData={pdfData}
|
pdfData={pdfData}
|
||||||
|
showHistory={showHistory}
|
||||||
|
onToggleHistory={setShowHistory}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
error={error}
|
error={error}
|
||||||
|
minimalStyle={minimalStyle}
|
||||||
|
onMinimalStyleChange={setMinimalStyle}
|
||||||
models={modelConfig.models}
|
models={modelConfig.models}
|
||||||
selectedModelId={modelConfig.selectedModelId}
|
selectedModelId={modelConfig.selectedModelId}
|
||||||
onModelSelect={modelConfig.setSelectedModelId}
|
onModelSelect={modelConfig.setSelectedModelId}
|
||||||
@@ -1074,8 +1088,6 @@ export default function ChatPanel({
|
|||||||
onToggleDrawioUi={onToggleDrawioUi}
|
onToggleDrawioUi={onToggleDrawioUi}
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
onToggleDarkMode={onToggleDarkMode}
|
onToggleDarkMode={onToggleDarkMode}
|
||||||
minimalStyle={minimalStyle}
|
|
||||||
onMinimalStyleChange={setMinimalStyle}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModelConfigDialog
|
<ModelConfigDialog
|
||||||
|
|||||||
@@ -134,13 +134,11 @@ const DEV_XML_PRESETS: Record<string, string> = {
|
|||||||
interface DevXmlSimulatorProps {
|
interface DevXmlSimulatorProps {
|
||||||
setMessages: React.Dispatch<React.SetStateAction<any[]>>
|
setMessages: React.Dispatch<React.SetStateAction<any[]>>
|
||||||
onDisplayChart: (xml: string) => void
|
onDisplayChart: (xml: string) => void
|
||||||
onShowQuotaToast?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DevXmlSimulator({
|
export function DevXmlSimulator({
|
||||||
setMessages,
|
setMessages,
|
||||||
onDisplayChart,
|
onDisplayChart,
|
||||||
onShowQuotaToast,
|
|
||||||
}: DevXmlSimulatorProps) {
|
}: DevXmlSimulatorProps) {
|
||||||
const [devXml, setDevXml] = useState("")
|
const [devXml, setDevXml] = useState("")
|
||||||
const [isSimulating, setIsSimulating] = useState(false)
|
const [isSimulating, setIsSimulating] = useState(false)
|
||||||
@@ -344,15 +342,6 @@ export function DevXmlSimulator({
|
|||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onShowQuotaToast && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onShowQuotaToast}
|
|
||||||
className="px-3 py-1 text-xs bg-purple-500 text-white rounded hover:bg-purple-600"
|
|
||||||
>
|
|
||||||
Test Quota Toast
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
openrouter: "openrouter",
|
openrouter: "openrouter",
|
||||||
deepseek: "deepseek",
|
deepseek: "deepseek",
|
||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible
|
|
||||||
gateway: "vercel",
|
gateway: "vercel",
|
||||||
doubao: "bytedance",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider logo component
|
// Provider logo component
|
||||||
@@ -89,16 +87,10 @@ function ProviderLogo({
|
|||||||
provider: ProviderName
|
provider: ProviderName
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
// Use Lucide icons for providers without models.dev logos
|
// Use Lucide icon for bedrock since models.dev doesn't have a good AWS icon
|
||||||
if (provider === "bedrock") {
|
if (provider === "bedrock") {
|
||||||
return <Cloud className={cn("size-4", className)} />
|
return <Cloud className={cn("size-4", className)} />
|
||||||
}
|
}
|
||||||
if (provider === "sglang") {
|
|
||||||
return <Server className={cn("size-4", className)} />
|
|
||||||
}
|
|
||||||
if (provider === "doubao") {
|
|
||||||
return <Sparkles className={cn("size-4", className)} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoName = PROVIDER_LOGO_MAP[provider] || provider
|
const logoName = PROVIDER_LOGO_MAP[provider] || provider
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
Settings2,
|
Settings2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import {
|
import {
|
||||||
ModelSelectorContent,
|
ModelSelectorContent,
|
||||||
ModelSelectorEmpty,
|
ModelSelectorEmpty,
|
||||||
@@ -46,9 +46,7 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
openrouter: "openrouter",
|
openrouter: "openrouter",
|
||||||
deepseek: "deepseek",
|
deepseek: "deepseek",
|
||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
|
|
||||||
gateway: "vercel",
|
gateway: "vercel",
|
||||||
doubao: "bytedance",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group models by providerLabel (handles duplicate providers)
|
// Group models by providerLabel (handles duplicate providers)
|
||||||
@@ -114,189 +112,134 @@ export function ModelSelector({
|
|||||||
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
|
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
|
||||||
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
|
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
const [showLabel, setShowLabel] = useState(true)
|
|
||||||
|
|
||||||
// Threshold (px) under which we hide the label (tweak as needed)
|
|
||||||
const HIDE_THRESHOLD = 240
|
|
||||||
const SHOW_THRESHOLD = 260
|
|
||||||
useEffect(() => {
|
|
||||||
const el = wrapperRef.current
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
const target = el.parentElement ?? el
|
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries) => {
|
|
||||||
for (const entry of entries) {
|
|
||||||
const width = entry.contentRect.width
|
|
||||||
setShowLabel((prev) => {
|
|
||||||
// if currently showing and width dropped below hide threshold -> hide
|
|
||||||
if (prev && width <= HIDE_THRESHOLD) return false
|
|
||||||
// if currently hidden and width rose above show threshold -> show
|
|
||||||
if (!prev && width >= SHOW_THRESHOLD) return true
|
|
||||||
// otherwise keep previous state (hysteresis)
|
|
||||||
return prev
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ro.observe(target)
|
|
||||||
|
|
||||||
const initialWidth = target.getBoundingClientRect().width
|
|
||||||
setShowLabel(initialWidth >= SHOW_THRESHOLD)
|
|
||||||
|
|
||||||
return () => ro.disconnect()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className="inline-block">
|
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
||||||
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
<ModelSelectorTrigger asChild>
|
||||||
<ModelSelectorTrigger asChild>
|
<ButtonWithTooltip
|
||||||
<ButtonWithTooltip
|
tooltipContent={tooltipContent}
|
||||||
tooltipContent={tooltipContent}
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
disabled={disabled}
|
||||||
disabled={disabled}
|
className="hover:bg-accent gap-1.5 h-8 max-w-[180px] px-2"
|
||||||
className={cn(
|
>
|
||||||
"hover:bg-accent gap-1.5 h-8 px-2 transition-all duration-150 ease-in-out",
|
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||||
!showLabel && "px-1.5 justify-center",
|
<span className="text-xs truncate">
|
||||||
)}
|
{selectedModel
|
||||||
// accessibility: expose label to screen readers
|
? selectedModel.modelId
|
||||||
aria-label={tooltipContent}
|
: dict.modelConfig.default}
|
||||||
>
|
</span>
|
||||||
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
||||||
{/* show/hide visible label based on measured width */}
|
</ButtonWithTooltip>
|
||||||
{showLabel ? (
|
</ModelSelectorTrigger>
|
||||||
<span className="text-xs truncate">
|
<ModelSelectorContent title={dict.modelConfig.selectModel}>
|
||||||
{selectedModel
|
<ModelSelectorInput
|
||||||
? selectedModel.modelId
|
placeholder={dict.modelConfig.searchModels}
|
||||||
: dict.modelConfig.default}
|
/>
|
||||||
</span>
|
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
) : (
|
<ModelSelectorEmpty>
|
||||||
// Keep an sr-only label for screen readers when hidden
|
{displayModels.length === 0 && models.length > 0
|
||||||
<span className="sr-only">
|
? dict.modelConfig.noVerifiedModels
|
||||||
{selectedModel
|
: dict.modelConfig.noModelsFound}
|
||||||
? selectedModel.modelId
|
</ModelSelectorEmpty>
|
||||||
: dict.modelConfig.default}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
</ModelSelectorTrigger>
|
|
||||||
|
|
||||||
<ModelSelectorContent title={dict.modelConfig.selectModel}>
|
{/* Server Default Option */}
|
||||||
<ModelSelectorInput
|
<ModelSelectorGroup heading={dict.modelConfig.default}>
|
||||||
placeholder={dict.modelConfig.searchModels}
|
<ModelSelectorItem
|
||||||
/>
|
value="__server_default__"
|
||||||
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
onSelect={handleSelect}
|
||||||
<ModelSelectorEmpty>
|
className={cn(
|
||||||
{displayModels.length === 0 && models.length > 0
|
"cursor-pointer",
|
||||||
? dict.modelConfig.noVerifiedModels
|
!selectedModelId && "bg-accent",
|
||||||
: dict.modelConfig.noModelsFound}
|
)}
|
||||||
</ModelSelectorEmpty>
|
>
|
||||||
|
<Check
|
||||||
{/* Server Default Option */}
|
|
||||||
<ModelSelectorGroup heading={dict.modelConfig.default}>
|
|
||||||
<ModelSelectorItem
|
|
||||||
value="__server_default__"
|
|
||||||
onSelect={handleSelect}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer",
|
"mr-2 h-4 w-4",
|
||||||
!selectedModelId && "bg-accent",
|
!selectedModelId
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
<Check
|
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
className={cn(
|
<ModelSelectorName>
|
||||||
"mr-2 h-4 w-4",
|
{dict.modelConfig.serverDefault}
|
||||||
!selectedModelId
|
</ModelSelectorName>
|
||||||
? "opacity-100"
|
</ModelSelectorItem>
|
||||||
: "opacity-0",
|
</ModelSelectorGroup>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<ModelSelectorName>
|
|
||||||
{dict.modelConfig.serverDefault}
|
|
||||||
</ModelSelectorName>
|
|
||||||
</ModelSelectorItem>
|
|
||||||
</ModelSelectorGroup>
|
|
||||||
|
|
||||||
{/* Configured Models by Provider */}
|
{/* Configured Models by Provider */}
|
||||||
{Array.from(groupedModels.entries()).map(
|
{Array.from(groupedModels.entries()).map(
|
||||||
([
|
([
|
||||||
providerLabel,
|
providerLabel,
|
||||||
{ provider, models: providerModels },
|
{ provider, models: providerModels },
|
||||||
]) => (
|
]) => (
|
||||||
<ModelSelectorGroup
|
<ModelSelectorGroup
|
||||||
key={providerLabel}
|
key={providerLabel}
|
||||||
heading={providerLabel}
|
heading={providerLabel}
|
||||||
>
|
>
|
||||||
{providerModels.map((model) => (
|
{providerModels.map((model) => (
|
||||||
<ModelSelectorItem
|
<ModelSelectorItem
|
||||||
key={model.id}
|
key={model.id}
|
||||||
value={model.modelId}
|
value={model.modelId}
|
||||||
onSelect={() =>
|
onSelect={() => handleSelect(model.id)}
|
||||||
handleSelect(model.id)
|
className="cursor-pointer"
|
||||||
}
|
>
|
||||||
className="cursor-pointer"
|
<Check
|
||||||
>
|
className={cn(
|
||||||
<Check
|
"mr-2 h-4 w-4",
|
||||||
className={cn(
|
selectedModelId === model.id
|
||||||
"mr-2 h-4 w-4",
|
? "opacity-100"
|
||||||
selectedModelId === model.id
|
: "opacity-0",
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ModelSelectorLogo
|
|
||||||
provider={
|
|
||||||
PROVIDER_LOGO_MAP[
|
|
||||||
provider
|
|
||||||
] || provider
|
|
||||||
}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<ModelSelectorName>
|
|
||||||
{model.modelId}
|
|
||||||
</ModelSelectorName>
|
|
||||||
{model.validated !== true && (
|
|
||||||
<span
|
|
||||||
title={
|
|
||||||
dict.modelConfig
|
|
||||||
.unvalidatedModelWarning
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</ModelSelectorItem>
|
/>
|
||||||
))}
|
<ModelSelectorLogo
|
||||||
</ModelSelectorGroup>
|
provider={
|
||||||
),
|
PROVIDER_LOGO_MAP[provider] ||
|
||||||
)}
|
provider
|
||||||
|
}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<ModelSelectorName>
|
||||||
|
{model.modelId}
|
||||||
|
</ModelSelectorName>
|
||||||
|
{model.validated !== true && (
|
||||||
|
<span
|
||||||
|
title={
|
||||||
|
dict.modelConfig
|
||||||
|
.unvalidatedModelWarning
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ModelSelectorItem>
|
||||||
|
))}
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Configure Option */}
|
{/* Configure Option */}
|
||||||
<ModelSelectorSeparator />
|
<ModelSelectorSeparator />
|
||||||
<ModelSelectorGroup>
|
<ModelSelectorGroup>
|
||||||
<ModelSelectorItem
|
<ModelSelectorItem
|
||||||
value="__configure__"
|
value="__configure__"
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<Settings2 className="mr-2 h-4 w-4" />
|
<Settings2 className="mr-2 h-4 w-4" />
|
||||||
<ModelSelectorName>
|
<ModelSelectorName>
|
||||||
{dict.modelConfig.configureModels}
|
{dict.modelConfig.configureModels}
|
||||||
</ModelSelectorName>
|
</ModelSelectorName>
|
||||||
</ModelSelectorItem>
|
</ModelSelectorItem>
|
||||||
</ModelSelectorGroup>
|
</ModelSelectorGroup>
|
||||||
{/* Info text */}
|
{/* Info text */}
|
||||||
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
||||||
{showUnvalidatedModels
|
{showUnvalidatedModels
|
||||||
? dict.modelConfig.allModelsShown
|
? dict.modelConfig.allModelsShown
|
||||||
: dict.modelConfig.onlyVerifiedShown}
|
: dict.modelConfig.onlyVerifiedShown}
|
||||||
</div>
|
</div>
|
||||||
</ModelSelectorList>
|
</ModelSelectorList>
|
||||||
</ModelSelectorContent>
|
</ModelSelectorContent>
|
||||||
</ModelSelectorRoot>
|
</ModelSelectorRoot>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Coffee, Settings, X } from "lucide-react"
|
import { Coffee, X } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { FaGithub } from "react-icons/fa"
|
import { FaGithub } from "react-icons/fa"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
@@ -11,7 +12,6 @@ interface QuotaLimitToastProps {
|
|||||||
used: number
|
used: number
|
||||||
limit: number
|
limit: number
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
onConfigModel?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuotaLimitToast({
|
export function QuotaLimitToast({
|
||||||
@@ -19,7 +19,6 @@ export function QuotaLimitToast({
|
|||||||
used,
|
used,
|
||||||
limit,
|
limit,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
onConfigModel,
|
|
||||||
}: QuotaLimitToastProps) {
|
}: QuotaLimitToastProps) {
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
const isTokenLimit = type === "token"
|
const isTokenLimit = type === "token"
|
||||||
@@ -76,36 +75,16 @@ export function QuotaLimitToast({
|
|||||||
? dict.quota.messageToken
|
? dict.quota.messageToken
|
||||||
: dict.quota.messageApi}
|
: dict.quota.messageApi}
|
||||||
</p>
|
</p>
|
||||||
<p
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: formatMessage(dict.quota.doubaoSponsorship, {
|
|
||||||
link: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
|
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
|
||||||
<p>{dict.quota.reset}</p>
|
<p>{dict.quota.reset}</p>
|
||||||
</div>{" "}
|
</div>{" "}
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{onConfigModel && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
onConfigModel()
|
|
||||||
onDismiss()
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
||||||
>
|
|
||||||
<Settings className="w-3.5 h-3.5" />
|
|
||||||
{dict.quota.configModel}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||||
>
|
>
|
||||||
<FaGithub className="w-3.5 h-3.5" />
|
<FaGithub className="w-3.5 h-3.5" />
|
||||||
{dict.quota.selfHost}
|
{dict.quota.selfHost}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Github, Info, Moon, Sun, Tag } from "lucide-react"
|
import { Moon, Sun } from "lucide-react"
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
import { Suspense, useEffect, useState } from "react"
|
import { Suspense, useEffect, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -24,6 +24,7 @@ import { Switch } from "@/components/ui/switch"
|
|||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
import { i18n, type Locale } from "@/lib/i18n/config"
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
// Reusable setting item component for consistent layout
|
// Reusable setting item component for consistent layout
|
||||||
function SettingItem({
|
function SettingItem({
|
||||||
@@ -64,8 +65,6 @@ interface SettingsDialogProps {
|
|||||||
onToggleDrawioUi: () => void
|
onToggleDrawioUi: () => void
|
||||||
darkMode: boolean
|
darkMode: boolean
|
||||||
onToggleDarkMode: () => void
|
onToggleDarkMode: () => void
|
||||||
minimalStyle?: boolean
|
|
||||||
onMinimalStyleChange?: (value: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
@@ -87,8 +86,6 @@ function SettingsContent({
|
|||||||
onToggleDrawioUi,
|
onToggleDrawioUi,
|
||||||
darkMode,
|
darkMode,
|
||||||
onToggleDarkMode,
|
onToggleDarkMode,
|
||||||
minimalStyle = false,
|
|
||||||
onMinimalStyleChange = () => {},
|
|
||||||
}: SettingsDialogProps) {
|
}: SettingsDialogProps) {
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -351,61 +348,14 @@ function SettingsContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
{/* Diagram Style */}
|
|
||||||
<SettingItem
|
|
||||||
label={dict.settings.diagramStyle}
|
|
||||||
description={dict.settings.diagramStyleDescription}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
id="minimal-style"
|
|
||||||
checked={minimalStyle}
|
|
||||||
onCheckedChange={onMinimalStyleChange}
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{minimalStyle
|
|
||||||
? dict.chat.minimalStyle
|
|
||||||
: dict.chat.styledMode}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SettingItem>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
||||||
<div className="flex items-center justify-center gap-3">
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
Version {process.env.APP_VERSION}
|
||||||
<Tag className="h-3 w-3" />
|
</p>
|
||||||
{process.env.APP_VERSION}
|
|
||||||
</span>
|
|
||||||
<span className="text-muted-foreground">·</span>
|
|
||||||
<a
|
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Github className="h-3 w-3" />
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
{process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
|
||||||
"true" && (
|
|
||||||
<>
|
|
||||||
<span className="text-muted-foreground">·</span>
|
|
||||||
<a
|
|
||||||
href="/about"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Info className="h-3 w-3" />
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
||||||
|
|
||||||
> 注:感谢 <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 的赞助支持,本项目的 Demo 现已接入强大的 K2-thinking 模型!
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
@@ -129,6 +127,8 @@ claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
|||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
> 注意:由于访问量较大,演示站点目前使用 minimax-m2 模型。如需获得最佳效果,建议使用 Claude Sonnet 4.5 或 Claude Opus 4.5 自行部署。
|
||||||
|
|
||||||
> **使用自己的 API Key**:您可以使用自己的 API Key 来绕过演示站点的用量限制。点击聊天面板中的设置图标即可配置您的 Provider 和 API Key。您的 Key 仅保存在浏览器本地,不会被存储在服务器上。
|
> **使用自己的 API Key**:您可以使用自己的 API Key 来绕过演示站点的用量限制。点击聊天面板中的设置图标即可配置您的 Provider 和 API Key。您的 Key 仅保存在浏览器本地,不会被存储在服务器上。
|
||||||
|
|
||||||
### 使用Docker运行(推荐)
|
### 使用Docker运行(推荐)
|
||||||
@@ -186,7 +186,7 @@ cp env.example .env.local
|
|||||||
|
|
||||||
编辑 `.env.local` 并配置您选择的提供商:
|
编辑 `.env.local` 并配置您选择的提供商:
|
||||||
|
|
||||||
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, doubao)
|
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||||
- 将 `AI_MODEL` 设置为您要使用的特定模型
|
- 将 `AI_MODEL` 设置为您要使用的特定模型
|
||||||
- 添加您的提供商所需的API密钥
|
- 添加您的提供商所需的API密钥
|
||||||
- `TEMPERATURE`:可选的温度设置(例如 `0` 表示确定性输出)。对于不支持此参数的模型(如推理模型),请不要设置。
|
- `TEMPERATURE`:可选的温度设置(例如 `0` 表示确定性输出)。对于不支持此参数的模型(如推理模型),请不要设置。
|
||||||
@@ -218,7 +218,6 @@ npm run dev
|
|||||||
|
|
||||||
## 多提供商支持
|
## 多提供商支持
|
||||||
|
|
||||||
- [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
|
||||||
- AWS Bedrock(默认)
|
- AWS Bedrock(默认)
|
||||||
- OpenAI
|
- OpenAI
|
||||||
- Anthropic
|
- Anthropic
|
||||||
@@ -269,8 +268,6 @@ public/ # 静态资源包括示例图片
|
|||||||
|
|
||||||
## 支持与联系
|
## 支持与联系
|
||||||
|
|
||||||
**特别感谢[字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)赞助演示站点的 API Token 使用!** 注册火山引擎 ARK 平台即可获得50万免费Token!
|
|
||||||
|
|
||||||
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
||||||
|
|
||||||
如需支持或咨询,请在GitHub仓库上提交issue或联系维护者:
|
如需支持或咨询,请在GitHub仓库上提交issue或联系维护者:
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
||||||
|
|
||||||
> 注:<img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) のご支援により、デモサイトに強力な K2-thinking モデルを導入しました!
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
## 目次
|
## 目次
|
||||||
@@ -129,6 +127,8 @@ Claudeにダイアグラムの作成を依頼:
|
|||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
> 注意:アクセス数が多いため、デモサイトでは現在 minimax-m2 モデルを使用しています。最高の結果を得るには、Claude Sonnet 4.5 または Claude Opus 4.5 でのセルフホスティングをお勧めします。
|
||||||
|
|
||||||
> **自分のAPIキーを使用**:自分のAPIキーを使用することで、デモサイトの利用制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。キーはブラウザのローカルに保存され、サーバーには保存されません。
|
> **自分のAPIキーを使用**:自分のAPIキーを使用することで、デモサイトの利用制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。キーはブラウザのローカルに保存され、サーバーには保存されません。
|
||||||
|
|
||||||
### Dockerで実行(推奨)
|
### Dockerで実行(推奨)
|
||||||
@@ -186,7 +186,7 @@ cp env.example .env.local
|
|||||||
|
|
||||||
`.env.local`を編集して選択したプロバイダーを設定:
|
`.env.local`を編集して選択したプロバイダーを設定:
|
||||||
|
|
||||||
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, doubao)
|
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
||||||
- `AI_MODEL`を使用する特定のモデルに設定
|
- `AI_MODEL`を使用する特定のモデルに設定
|
||||||
- プロバイダーに必要なAPIキーを追加
|
- プロバイダーに必要なAPIキーを追加
|
||||||
- `TEMPERATURE`:オプションの温度設定(例:`0`で決定論的な出力)。温度をサポートしないモデル(推論モデルなど)では設定しないでください。
|
- `TEMPERATURE`:オプションの温度設定(例:`0`で決定論的な出力)。温度をサポートしないモデル(推論モデルなど)では設定しないでください。
|
||||||
@@ -218,7 +218,6 @@ Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成
|
|||||||
|
|
||||||
## マルチプロバイダーサポート
|
## マルチプロバイダーサポート
|
||||||
|
|
||||||
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
|
||||||
- AWS Bedrock(デフォルト)
|
- AWS Bedrock(デフォルト)
|
||||||
- OpenAI
|
- OpenAI
|
||||||
- Anthropic
|
- Anthropic
|
||||||
@@ -269,8 +268,6 @@ public/ # サンプル画像を含む静的アセット
|
|||||||
|
|
||||||
## サポート&お問い合わせ
|
## サポート&お問い合わせ
|
||||||
|
|
||||||
**デモサイトのAPIトークン使用を支援してくださった[ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に特別な感謝を申し上げます!** ARKプラットフォームに登録すると、50万トークンが無料でもらえます!
|
|
||||||
|
|
||||||
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
||||||
|
|
||||||
サポートやお問い合わせについては、GitHubリポジトリでissueを開くか、メンテナーにご連絡ください:
|
サポートやお問い合わせについては、GitHubリポジトリでissueを開くか、メンテナーにご連絡ください:
|
||||||
|
|||||||
@@ -11,15 +11,6 @@ This guide explains how to configure different AI model providers for next-ai-dr
|
|||||||
|
|
||||||
## Supported Providers
|
## Supported Providers
|
||||||
|
|
||||||
### Doubao (ByteDance Volcengine)
|
|
||||||
|
|
||||||
> **Free tokens**: Register on the [Volcengine ARK platform](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) to get 500K free tokens for all models!
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DOUBAO_API_KEY=your_api_key
|
|
||||||
AI_MODEL=doubao-seed-1-8-251215 # or other Doubao model
|
|
||||||
```
|
|
||||||
|
|
||||||
### Google Gemini
|
### Google Gemini
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -85,8 +76,6 @@ Optional custom endpoint (defaults to the recommended domain):
|
|||||||
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflow.cn/v1
|
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflow.cn/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Azure OpenAI
|
### Azure OpenAI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -190,7 +179,7 @@ If you only configure **one** provider's API key, the system will automatically
|
|||||||
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway
|
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama, gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
## Model Capability Requirements
|
## Model Capability Requirements
|
||||||
|
|||||||
0
electron/electron.d.ts → electron.d.ts
vendored
0
electron/electron.d.ts → electron.d.ts
vendored
@@ -72,10 +72,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# SGLANG_API_KEY=your-sglang-api-key
|
# SGLANG_API_KEY=your-sglang-api-key
|
||||||
# SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint
|
# SGLANG_BASE_URL=http://127.0.0.1:8000/v1 # Your SGLang endpoint
|
||||||
|
|
||||||
# ByteDance Doubao Configuration (via Volcengine)
|
|
||||||
# DOUBAO_API_KEY=your-doubao-api-key
|
|
||||||
# DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 # ByteDance Volcengine endpoint
|
|
||||||
|
|
||||||
# Vercel AI Gateway Configuration
|
# Vercel AI Gateway Configuration
|
||||||
# Get your API key from: https://vercel.com/ai-gateway
|
# Get your API key from: https://vercel.com/ai-gateway
|
||||||
# Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
|
# Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export type ProviderName =
|
|||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
| "sglang"
|
||||||
| "gateway"
|
| "gateway"
|
||||||
| "doubao"
|
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
model: any
|
model: any
|
||||||
@@ -54,7 +53,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|||||||
"siliconflow",
|
"siliconflow",
|
||||||
"sglang",
|
"sglang",
|
||||||
"gateway",
|
"gateway",
|
||||||
"doubao",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// Bedrock provider options for Anthropic beta features
|
// Bedrock provider options for Anthropic beta features
|
||||||
@@ -348,8 +346,7 @@ function buildProviderOptions(
|
|||||||
case "openrouter":
|
case "openrouter":
|
||||||
case "siliconflow":
|
case "siliconflow":
|
||||||
case "sglang":
|
case "sglang":
|
||||||
case "gateway":
|
case "gateway": {
|
||||||
case "doubao": {
|
|
||||||
// These providers don't have reasoning configs in AI SDK yet
|
// These providers don't have reasoning configs in AI SDK yet
|
||||||
// Gateway passes through to underlying providers which handle their own configs
|
// Gateway passes through to underlying providers which handle their own configs
|
||||||
break
|
break
|
||||||
@@ -375,7 +372,6 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
|||||||
siliconflow: "SILICONFLOW_API_KEY",
|
siliconflow: "SILICONFLOW_API_KEY",
|
||||||
sglang: "SGLANG_API_KEY",
|
sglang: "SGLANG_API_KEY",
|
||||||
gateway: "AI_GATEWAY_API_KEY",
|
gateway: "AI_GATEWAY_API_KEY",
|
||||||
doubao: "DOUBAO_API_KEY",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -840,23 +836,9 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "doubao": {
|
|
||||||
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
|
|
||||||
const baseURL =
|
|
||||||
overrides?.baseUrl ||
|
|
||||||
process.env.DOUBAO_BASE_URL ||
|
|
||||||
"https://ark.cn-beijing.volces.com/api/v3"
|
|
||||||
const doubaoProvider = createDeepSeek({
|
|
||||||
apiKey,
|
|
||||||
baseURL,
|
|
||||||
})
|
|
||||||
model = doubaoProvider(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, doubao`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,13 +98,7 @@
|
|||||||
"minimal": "Minimal",
|
"minimal": "Minimal",
|
||||||
"sketch": "Sketch",
|
"sketch": "Sketch",
|
||||||
"closeProtection": "Close Protection",
|
"closeProtection": "Close Protection",
|
||||||
"closeProtectionDescription": "Show confirmation when leaving the page.",
|
"closeProtectionDescription": "Show confirmation when leaving the page."
|
||||||
"diagramStyle": "Diagram Style",
|
|
||||||
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
|
|
||||||
"diagramActions": "Diagram Actions",
|
|
||||||
"diagramActionsDescription": "Manage diagram history and exports",
|
|
||||||
"history": "History",
|
|
||||||
"download": "Download"
|
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "Save Diagram",
|
"title": "Save Diagram",
|
||||||
@@ -157,12 +151,10 @@
|
|||||||
"tpmLimit": "Rate Limit",
|
"tpmLimit": "Rate Limit",
|
||||||
"tpmMessage": "Too many requests. Please wait a moment.",
|
"tpmMessage": "Too many requests. Please wait a moment.",
|
||||||
"tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.",
|
"tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.",
|
||||||
"messageApi": "Looks like you've reached today's demo limit. We're thrilled you're enjoying it, and while ByteDance Doubao generously sponsors this demo, we've had to set a few boundaries to keep things fair for everyone.",
|
"messageApi": "Oops — you've reached the daily API limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.",
|
||||||
"messageToken": "Looks like you've reached today's token limit. We're thrilled you're enjoying it, and while ByteDance Doubao generously sponsors this demo, we've had to set a few boundaries to keep things fair for everyone.",
|
"messageToken": "Oops — you've reached the daily token limit for this demo! As an indie developer covering all the API costs myself, I have to set these limits to keep things sustainable.",
|
||||||
"tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.",
|
"tip": "<strong>Tip:</strong> You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.",
|
||||||
"reset": "Your limit resets tomorrow. Thanks for understanding.",
|
"reset": "Your limit resets tomorrow. Thanks for understanding!",
|
||||||
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">Register here</a> to get 500K free tokens per model (including Doubao, DeepSeek and Kimi), then configure your API key in model settings.",
|
|
||||||
"configModel": "Use Your API Key",
|
|
||||||
"selfHost": "Self-host",
|
"selfHost": "Self-host",
|
||||||
"sponsor": "Sponsor",
|
"sponsor": "Sponsor",
|
||||||
"learnMore": "Learn more →",
|
"learnMore": "Learn more →",
|
||||||
|
|||||||
@@ -98,13 +98,7 @@
|
|||||||
"minimal": "ミニマル",
|
"minimal": "ミニマル",
|
||||||
"sketch": "スケッチ",
|
"sketch": "スケッチ",
|
||||||
"closeProtection": "ページ離脱確認",
|
"closeProtection": "ページ離脱確認",
|
||||||
"closeProtectionDescription": "ページを離れる際に確認を表示します。",
|
"closeProtectionDescription": "ページを離れる際に確認を表示します。"
|
||||||
"diagramStyle": "ダイアグラムスタイル",
|
|
||||||
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
|
|
||||||
"diagramActions": "ダイアグラム操作",
|
|
||||||
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
|
|
||||||
"history": "履歴",
|
|
||||||
"download": "ダウンロード"
|
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "ダイアグラムを保存",
|
"title": "ダイアグラムを保存",
|
||||||
@@ -157,12 +151,10 @@
|
|||||||
"tpmLimit": "レート制限",
|
"tpmLimit": "レート制限",
|
||||||
"tpmMessage": "リクエストが多すぎます。しばらくお待ちください。",
|
"tpmMessage": "リクエストが多すぎます。しばらくお待ちください。",
|
||||||
"tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。",
|
"tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。",
|
||||||
"messageApi": "今日のデモ利用上限に達してしまったようです。楽しんでいただけて本当に嬉しいです。このデモはByteDance Doubaoのご厚意により提供されていますが、皆様に公平にご利用いただくため、少し制限を設けさせていただいております。",
|
"messageApi": "おっと — このデモの1日の API 制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。",
|
||||||
"messageToken": "今日のトークン利用上限に達してしまったようです。楽しんでいただけて本当に嬉しいです。このデモはByteDance Doubaoのご厚意により提供されていますが、皆様に公平にご利用いただくため、少し制限を設けさせていただいております。",
|
"messageToken": "おっと — このデモの1日のトークン制限に達しました!個人開発者として API コストをすべて負担しているため、持続可能性を保つためにこれらの制限を設定する必要があります。",
|
||||||
"tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。",
|
"tip": "<strong>ヒント:</strong>独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。",
|
||||||
"reset": "制限は明日リセットされます。ご理解ありがとうございます。",
|
"reset": "制限は明日リセットされます。ご理解ありがとうございます!",
|
||||||
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">こちらから登録</a>すると、各モデル(Doubao、DeepSeek、Kimi含む)で50万トークンを無料で取得できます。モデル設定でAPIキーを設定してください。",
|
|
||||||
"configModel": "APIキーを使用",
|
|
||||||
"selfHost": "セルフホスト",
|
"selfHost": "セルフホスト",
|
||||||
"sponsor": "スポンサー",
|
"sponsor": "スポンサー",
|
||||||
"learnMore": "詳細 →",
|
"learnMore": "詳細 →",
|
||||||
|
|||||||
@@ -98,13 +98,7 @@
|
|||||||
"minimal": "简约",
|
"minimal": "简约",
|
||||||
"sketch": "草图",
|
"sketch": "草图",
|
||||||
"closeProtection": "关闭确认",
|
"closeProtection": "关闭确认",
|
||||||
"closeProtectionDescription": "离开页面时显示确认。",
|
"closeProtectionDescription": "离开页面时显示确认。"
|
||||||
"diagramStyle": "图表样式",
|
|
||||||
"diagramStyleDescription": "切换简约与精致图表输出模式。",
|
|
||||||
"diagramActions": "图表操作",
|
|
||||||
"diagramActionsDescription": "管理图表历史记录和导出",
|
|
||||||
"history": "历史记录",
|
|
||||||
"download": "下载"
|
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "保存图表",
|
"title": "保存图表",
|
||||||
@@ -157,12 +151,10 @@
|
|||||||
"tpmLimit": "速率限制",
|
"tpmLimit": "速率限制",
|
||||||
"tpmMessage": "请求过多。请稍等片刻。",
|
"tpmMessage": "请求过多。请稍等片刻。",
|
||||||
"tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。",
|
"tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。",
|
||||||
"messageApi": "看来您今天的体验次数已达上限。非常高兴您玩得开心,虽然本项目由字节跳动豆包慷慨赞助,但为了确保大家都能公平使用,我们不得不对使用量做一点小小的限制。",
|
"messageApi": "糟糕 — 您已达到此演示的每日 API 限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。",
|
||||||
"messageToken": "看来您今天的 Token 用量已达上限。非常高兴您玩得开心,虽然本项目由字节跳动豆包慷慨赞助,但为了确保大家都能公平使用,我们不得不对使用量做一点小小的限制。",
|
"messageToken": "糟糕 — 您已达到此演示的每日令牌限制!作为一名独立开发者,我自己承担所有 API 费用,因此必须设置这些限制以保持可持续性。",
|
||||||
"tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。",
|
"tip": "<strong>提示:</strong>您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。",
|
||||||
"reset": "您的限制将在明天重置。感谢您的理解。",
|
"reset": "您的限制将在明天重置。感谢您的理解!",
|
||||||
"doubaoSponsorship": "<a href=\"{link}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-foreground\">点击此处注册</a>可获得每个模型 50 万免费 Token(包括豆包、DeepSeek 和 Kimi),然后在模型设置中配置您的 API Key。",
|
|
||||||
"configModel": "使用您的密钥",
|
|
||||||
"selfHost": "自托管",
|
"selfHost": "自托管",
|
||||||
"sponsor": "赞助",
|
"sponsor": "赞助",
|
||||||
"learnMore": "了解更多 →",
|
"learnMore": "了解更多 →",
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ edit_diagram uses ID-based operations to modify cells directly by their id attri
|
|||||||
**Operations:**
|
**Operations:**
|
||||||
- **update**: Replace an existing cell. Provide cell_id and new_xml.
|
- **update**: Replace an existing cell. Provide cell_id and new_xml.
|
||||||
- **add**: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
- **add**: Add a new cell. Provide cell_id (new unique id) and new_xml.
|
||||||
- **delete**: Remove a cell. **Cascade is automatic**: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id.
|
- **delete**: Remove a cell. Only cell_id is needed.
|
||||||
|
|
||||||
**Input Format:**
|
**Input Format:**
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
@@ -301,9 +301,9 @@ Add new shape:
|
|||||||
{"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"New Box\\" style=\\"rounded=1;fillColor=#dae8fc;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
{"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"New Box\\" style=\\"rounded=1;fillColor=#dae8fc;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Delete container (children & edges auto-deleted):
|
Delete cell:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{"operations": [{"operation": "delete", "cell_id": "2"}]}
|
{"operations": [{"operation": "delete", "cell_id": "5"}]}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Error Recovery:**
|
**Error Recovery:**
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ export type ProviderName =
|
|||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "deepseek"
|
| "deepseek"
|
||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
|
||||||
| "gateway"
|
| "gateway"
|
||||||
| "doubao"
|
|
||||||
|
|
||||||
// Individual model configuration
|
// Individual model configuration
|
||||||
export interface ModelConfig {
|
export interface ModelConfig {
|
||||||
@@ -80,15 +78,7 @@ export const PROVIDER_INFO: Record<
|
|||||||
label: "SiliconFlow",
|
label: "SiliconFlow",
|
||||||
defaultBaseUrl: "https://api.siliconflow.com/v1",
|
defaultBaseUrl: "https://api.siliconflow.com/v1",
|
||||||
},
|
},
|
||||||
sglang: {
|
|
||||||
label: "SGLang",
|
|
||||||
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
|
||||||
},
|
|
||||||
gateway: { label: "AI Gateway" },
|
gateway: { label: "AI Gateway" },
|
||||||
doubao: {
|
|
||||||
label: "Doubao (ByteDance)",
|
|
||||||
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suggested models per provider for quick add
|
// Suggested models per provider for quick add
|
||||||
@@ -208,10 +198,6 @@ export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
|
|||||||
"Qwen/Qwen2.5-7B-Instruct",
|
"Qwen/Qwen2.5-7B-Instruct",
|
||||||
"Qwen/Qwen2-VL-72B-Instruct",
|
"Qwen/Qwen2-VL-72B-Instruct",
|
||||||
],
|
],
|
||||||
sglang: [
|
|
||||||
// SGLang is OpenAI-compatible, models depend on deployment
|
|
||||||
"default",
|
|
||||||
],
|
|
||||||
gateway: [
|
gateway: [
|
||||||
"openai/gpt-4o",
|
"openai/gpt-4o",
|
||||||
"openai/gpt-4o-mini",
|
"openai/gpt-4o-mini",
|
||||||
@@ -219,15 +205,6 @@ export const SUGGESTED_MODELS: Record<ProviderName, string[]> = {
|
|||||||
"anthropic/claude-3-5-sonnet",
|
"anthropic/claude-3-5-sonnet",
|
||||||
"google/gemini-2.0-flash",
|
"google/gemini-2.0-flash",
|
||||||
],
|
],
|
||||||
doubao: [
|
|
||||||
// ByteDance Doubao models
|
|
||||||
"doubao-1.5-thinking-pro-250415",
|
|
||||||
"doubao-1.5-thinking-pro-m-250428",
|
|
||||||
"doubao-1.5-pro-32k-250115",
|
|
||||||
"doubao-1.5-pro-256k-250115",
|
|
||||||
"doubao-pro-32k-241215",
|
|
||||||
"doubao-pro-256k-241215",
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to generate UUID
|
// Helper to generate UUID
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export interface QuotaConfig {
|
|||||||
dailyRequestLimit: number
|
dailyRequestLimit: number
|
||||||
dailyTokenLimit: number
|
dailyTokenLimit: number
|
||||||
tpmLimit: number
|
tpmLimit: number
|
||||||
onConfigModel?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,8 +22,7 @@ export function useQuotaManager(config: QuotaConfig): {
|
|||||||
showTokenLimitToast: (used?: number, limit?: number) => void
|
showTokenLimitToast: (used?: number, limit?: number) => void
|
||||||
showTPMLimitToast: (limit?: number) => void
|
showTPMLimitToast: (limit?: number) => void
|
||||||
} {
|
} {
|
||||||
const { dailyRequestLimit, dailyTokenLimit, tpmLimit, onConfigModel } =
|
const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config
|
||||||
config
|
|
||||||
const dict = useDictionary()
|
const dict = useDictionary()
|
||||||
|
|
||||||
// Show quota limit toast (request-based)
|
// Show quota limit toast (request-based)
|
||||||
@@ -36,13 +34,12 @@ export function useQuotaManager(config: QuotaConfig): {
|
|||||||
used={used ?? dailyRequestLimit}
|
used={used ?? dailyRequestLimit}
|
||||||
limit={limit ?? dailyRequestLimit}
|
limit={limit ?? dailyRequestLimit}
|
||||||
onDismiss={() => toast.dismiss(t)}
|
onDismiss={() => toast.dismiss(t)}
|
||||||
onConfigModel={onConfigModel}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{ duration: 15000 },
|
{ duration: 15000 },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[dailyRequestLimit, onConfigModel],
|
[dailyRequestLimit],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Show token limit toast
|
// Show token limit toast
|
||||||
@@ -55,13 +52,12 @@ export function useQuotaManager(config: QuotaConfig): {
|
|||||||
used={used ?? dailyTokenLimit}
|
used={used ?? dailyTokenLimit}
|
||||||
limit={limit ?? dailyTokenLimit}
|
limit={limit ?? dailyTokenLimit}
|
||||||
onDismiss={() => toast.dismiss(t)}
|
onDismiss={() => toast.dismiss(t)}
|
||||||
onConfigModel={onConfigModel}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{ duration: 15000 },
|
{ duration: 15000 },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[dailyTokenLimit, onConfigModel],
|
[dailyTokenLimit],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Show TPM limit toast
|
// Show TPM limit toast
|
||||||
|
|||||||
77
lib/utils.ts
77
lib/utils.ts
@@ -633,77 +633,32 @@ export function applyDiagramOperations(
|
|||||||
// Add to map
|
// Add to map
|
||||||
cellMap.set(op.cell_id, importedNode)
|
cellMap.set(op.cell_id, importedNode)
|
||||||
} else if (op.operation === "delete") {
|
} else if (op.operation === "delete") {
|
||||||
// Protect root cells from deletion
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
if (op.cell_id === "0" || op.cell_id === "1") {
|
if (!existingCell) {
|
||||||
errors.push({
|
errors.push({
|
||||||
type: "delete",
|
type: "delete",
|
||||||
cellId: op.cell_id,
|
cellId: op.cell_id,
|
||||||
message: `Cannot delete root cell "${op.cell_id}"`,
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCell = cellMap.get(op.cell_id)
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
if (!existingCell) {
|
const referencingEdges = root.querySelectorAll(
|
||||||
// Cell not found - might have been cascade-deleted by a previous operation
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
// Skip silently instead of erroring (AI may redundantly list children/edges)
|
)
|
||||||
continue
|
if (referencingEdges.length > 0) {
|
||||||
}
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
// Cascade delete: collect all cells to delete (children + edges + self)
|
.join(", ")
|
||||||
const cellsToDelete = new Set<string>()
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
// Recursive function to find all descendants
|
|
||||||
const collectDescendants = (cellId: string) => {
|
|
||||||
if (cellsToDelete.has(cellId)) return
|
|
||||||
cellsToDelete.add(cellId)
|
|
||||||
|
|
||||||
// Find children (cells where parent === cellId)
|
|
||||||
const children = root.querySelectorAll(
|
|
||||||
`mxCell[parent="${cellId}"]`,
|
|
||||||
)
|
|
||||||
children.forEach((child) => {
|
|
||||||
const childId = child.getAttribute("id")
|
|
||||||
if (childId && childId !== "0" && childId !== "1") {
|
|
||||||
collectDescendants(childId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the target cell and all its descendants
|
|
||||||
collectDescendants(op.cell_id)
|
|
||||||
|
|
||||||
// Find edges referencing any of the cells to be deleted
|
|
||||||
// Also recursively collect children of those edges (e.g., edge labels)
|
|
||||||
for (const cellId of cellsToDelete) {
|
|
||||||
const referencingEdges = root.querySelectorAll(
|
|
||||||
`mxCell[source="${cellId}"], mxCell[target="${cellId}"]`,
|
|
||||||
)
|
|
||||||
referencingEdges.forEach((edge) => {
|
|
||||||
const edgeId = edge.getAttribute("id")
|
|
||||||
// Protect root cells from being added via edge references
|
|
||||||
if (edgeId && edgeId !== "0" && edgeId !== "1") {
|
|
||||||
// Recurse to collect edge's children (like labels)
|
|
||||||
collectDescendants(edgeId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log what will be deleted
|
|
||||||
if (cellsToDelete.size > 1) {
|
|
||||||
console.log(
|
|
||||||
`[applyDiagramOperations] Cascade delete "${op.cell_id}" → deleting ${cellsToDelete.size} cells: ${Array.from(cellsToDelete).join(", ")}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all collected cells
|
// Remove the node
|
||||||
for (const cellId of cellsToDelete) {
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
const cell = cellMap.get(cellId)
|
cellMap.delete(op.cell_id)
|
||||||
if (cell) {
|
|
||||||
cell.parentNode?.removeChild(cell)
|
|
||||||
cellMap.delete(cellId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3411
package-lock.json
generated
3411
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -21,11 +21,11 @@
|
|||||||
"electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/",
|
"electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/",
|
||||||
"electron:start": "npx cross-env NODE_ENV=development npx electron .",
|
"electron:start": "npx cross-env NODE_ENV=development npx electron .",
|
||||||
"electron:prepare": "node scripts/prepare-electron-build.mjs",
|
"electron:prepare": "node scripts/prepare-electron-build.mjs",
|
||||||
"dist": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml",
|
"dist": "npm run electron:build && npm run electron:prepare && npx electron-builder",
|
||||||
"dist:mac": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac",
|
"dist:mac": "npm run electron:build && npm run electron:prepare && npx electron-builder --mac",
|
||||||
"dist:win": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --win",
|
"dist:win": "npm run electron:build && npm run electron:prepare && npx electron-builder --win",
|
||||||
"dist:linux": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --linux",
|
"dist:linux": "npm run electron:build && npm run electron:prepare && npx electron-builder --linux",
|
||||||
"dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac --win --linux"
|
"dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --mac --win --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^4.0.1",
|
"@ai-sdk/amazon-bedrock": "^4.0.1",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"js-tiktoken": "^1.0.21",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"jsonrepair": "^3.13.1",
|
"jsonrepair": "^3.13.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.23.25",
|
"motion": "^12.23.25",
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/negotiator": "^0.6.4",
|
"@types/negotiator": "^0.6.4",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^20",
|
||||||
"@types/pako": "^2.0.3",
|
"@types/pako": "^2.0.3",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Use the standard MCP configuration with:
|
|||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `start_session` | Opens browser with real-time diagram preview |
|
| `start_session` | Opens browser with real-time diagram preview |
|
||||||
| `create_new_diagram` | Create a new diagram from XML (requires `xml` argument) |
|
| `display_diagram` | Create a new diagram from XML |
|
||||||
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells) |
|
||||||
| `get_diagram` | Get the current diagram XML |
|
| `get_diagram` | Get the current diagram XML |
|
||||||
| `export_diagram` | Save diagram to a `.drawio` file |
|
| `export_diagram` | Save diagram to a `.drawio` file |
|
||||||
|
|||||||
24
packages/mcp-server/package-lock.json
generated
24
packages/mcp-server/package-lock.json
generated
@@ -12,13 +12,13 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"linkedom": "^0.18.0",
|
"linkedom": "^0.18.0",
|
||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"zod": "^3.24.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"next-ai-drawio-mcp": "dist/index.js"
|
"next-ai-drawio-mcp": "dist/index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^20",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
@@ -520,13 +520,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.4",
|
"version": "20.19.27",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
@@ -1989,9 +1989,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2051,9 +2051,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-ai-drawio/mcp-server",
|
"name": "@next-ai-drawio/mcp-server",
|
||||||
"version": "0.1.10",
|
"version": "0.1.6",
|
||||||
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"linkedom": "^0.18.0",
|
"linkedom": "^0.18.0",
|
||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"zod": "^3.24.0"
|
"zod": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^20",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -182,77 +182,32 @@ export function applyDiagramOperations(
|
|||||||
// Add to map
|
// Add to map
|
||||||
cellMap.set(op.cell_id, importedNode)
|
cellMap.set(op.cell_id, importedNode)
|
||||||
} else if (op.operation === "delete") {
|
} else if (op.operation === "delete") {
|
||||||
// Protect root cells from deletion
|
const existingCell = cellMap.get(op.cell_id)
|
||||||
if (op.cell_id === "0" || op.cell_id === "1") {
|
if (!existingCell) {
|
||||||
errors.push({
|
errors.push({
|
||||||
type: "delete",
|
type: "delete",
|
||||||
cellId: op.cell_id,
|
cellId: op.cell_id,
|
||||||
message: `Cannot delete root cell "${op.cell_id}"`,
|
message: `Cell with id="${op.cell_id}" not found`,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCell = cellMap.get(op.cell_id)
|
// Check for edges referencing this cell (warning only, still delete)
|
||||||
if (!existingCell) {
|
const referencingEdges = root.querySelectorAll(
|
||||||
// Cell not found - might have been cascade-deleted by a previous operation
|
`mxCell[source="${op.cell_id}"], mxCell[target="${op.cell_id}"]`,
|
||||||
// Skip silently instead of erroring (AI may redundantly list children/edges)
|
)
|
||||||
continue
|
if (referencingEdges.length > 0) {
|
||||||
}
|
const edgeIds = Array.from(referencingEdges)
|
||||||
|
.map((e) => e.getAttribute("id"))
|
||||||
// Cascade delete: collect all cells to delete (children + edges + self)
|
.join(", ")
|
||||||
const cellsToDelete = new Set<string>()
|
console.warn(
|
||||||
|
`[applyDiagramOperations] Deleting cell "${op.cell_id}" which is referenced by edges: ${edgeIds}`,
|
||||||
// Recursive function to find all descendants
|
|
||||||
const collectDescendants = (cellId: string) => {
|
|
||||||
if (cellsToDelete.has(cellId)) return
|
|
||||||
cellsToDelete.add(cellId)
|
|
||||||
|
|
||||||
// Find children (cells where parent === cellId)
|
|
||||||
const children = root.querySelectorAll(
|
|
||||||
`mxCell[parent="${cellId}"]`,
|
|
||||||
)
|
|
||||||
children.forEach((child) => {
|
|
||||||
const childId = child.getAttribute("id")
|
|
||||||
if (childId && childId !== "0" && childId !== "1") {
|
|
||||||
collectDescendants(childId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the target cell and all its descendants
|
|
||||||
collectDescendants(op.cell_id)
|
|
||||||
|
|
||||||
// Find edges referencing any of the cells to be deleted
|
|
||||||
// Also recursively collect children of those edges (e.g., edge labels)
|
|
||||||
for (const cellId of cellsToDelete) {
|
|
||||||
const referencingEdges = root.querySelectorAll(
|
|
||||||
`mxCell[source="${cellId}"], mxCell[target="${cellId}"]`,
|
|
||||||
)
|
|
||||||
referencingEdges.forEach((edge) => {
|
|
||||||
const edgeId = edge.getAttribute("id")
|
|
||||||
// Protect root cells from being added via edge references
|
|
||||||
if (edgeId && edgeId !== "0" && edgeId !== "1") {
|
|
||||||
// Recurse to collect edge's children (like labels)
|
|
||||||
collectDescendants(edgeId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log what will be deleted
|
|
||||||
if (cellsToDelete.size > 1) {
|
|
||||||
console.log(
|
|
||||||
`[applyDiagramOperations] Cascade delete "${op.cell_id}" → deleting ${cellsToDelete.size} cells: ${Array.from(cellsToDelete).join(", ")}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all collected cells
|
// Remove the node
|
||||||
for (const cellId of cellsToDelete) {
|
existingCell.parentNode?.removeChild(existingCell)
|
||||||
const cell = cellMap.get(cellId)
|
cellMap.delete(op.cell_id)
|
||||||
if (cell) {
|
|
||||||
cell.parentNode?.removeChild(cell)
|
|
||||||
cellMap.delete(cellId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ server.prompt(
|
|||||||
|
|
||||||
## Creating a New Diagram
|
## Creating a New Diagram
|
||||||
1. Call start_session to open the browser preview
|
1. Call start_session to open the browser preview
|
||||||
2. Use create_new_diagram with complete mxGraphModel XML to create a new diagram
|
2. Use display_diagram with complete mxGraphModel XML to create a new diagram
|
||||||
|
|
||||||
## Adding Elements to Existing Diagram
|
## Adding Elements to Existing Diagram
|
||||||
1. Use edit_diagram with "add" operation
|
1. Use edit_diagram with "add" operation
|
||||||
@@ -91,7 +91,7 @@ server.prompt(
|
|||||||
3. For update, provide the cell_id and complete new mxCell XML
|
3. For update, provide the cell_id and complete new mxCell XML
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
- create_new_diagram REPLACES the entire diagram - only use for new diagrams
|
- display_diagram REPLACES the entire diagram - only use for new diagrams
|
||||||
- edit_diagram PRESERVES user's manual changes (fetches browser state first)
|
- edit_diagram PRESERVES user's manual changes (fetches browser state first)
|
||||||
- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`,
|
- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")`,
|
||||||
},
|
},
|
||||||
@@ -150,59 +150,19 @@ server.registerTool(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tool: create_new_diagram
|
// Tool: display_diagram
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"create_new_diagram",
|
"display_diagram",
|
||||||
{
|
{
|
||||||
description: `Create a NEW diagram from mxGraphModel XML. Use this when creating a diagram from scratch or replacing the current diagram entirely.
|
description:
|
||||||
|
"Display a NEW draw.io diagram from XML. REPLACES the entire diagram. " +
|
||||||
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
|
"Use this for creating new diagrams from scratch. " +
|
||||||
|
"To ADD elements to an existing diagram, use edit_diagram with 'add' operation instead. " +
|
||||||
When to use this tool:
|
"You should generate valid draw.io/mxGraph XML format.",
|
||||||
- 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:
|
|
||||||
- Small modifications to existing diagram
|
|
||||||
- Adding/removing individual elements
|
|
||||||
- Changing labels, colors, or positions
|
|
||||||
|
|
||||||
XML FORMAT - Full mxGraphModel structure:
|
|
||||||
<mxGraphModel>
|
|
||||||
<root>
|
|
||||||
<mxCell id="0"/>
|
|
||||||
<mxCell id="1" parent="0"/>
|
|
||||||
<mxCell id="2" value="Shape" style="rounded=1;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
</root>
|
|
||||||
</mxGraphModel>
|
|
||||||
|
|
||||||
LAYOUT CONSTRAINTS:
|
|
||||||
- Keep all elements within x=0-800, y=0-600 (single page viewport)
|
|
||||||
- Start from margins (x=40, y=40), keep elements grouped closely
|
|
||||||
- Use unique IDs starting from "2" (0 and 1 are reserved)
|
|
||||||
- Set parent="1" for top-level shapes
|
|
||||||
- Space shapes 150-200px apart for clear edge routing
|
|
||||||
|
|
||||||
EDGE ROUTING RULES:
|
|
||||||
- Never let multiple edges share the same path - use different exitY/entryY values
|
|
||||||
- For bidirectional connections (A↔B), use OPPOSITE sides
|
|
||||||
- Always specify exitX, exitY, entryX, entryY explicitly in edge style
|
|
||||||
- Route edges AROUND obstacles using waypoints (add 20-30px clearance)
|
|
||||||
- Use natural connection points based on flow (not corners)
|
|
||||||
|
|
||||||
COMMON STYLES:
|
|
||||||
- Shapes: rounded=1; fillColor=#hex; strokeColor=#hex
|
|
||||||
- Edges: endArrow=classic; edgeStyle=orthogonalEdgeStyle; curved=1
|
|
||||||
- Text: fontSize=14; fontStyle=1 (bold); align=center`,
|
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
xml: z
|
xml: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe("The draw.io XML to display (mxGraphModel format)"),
|
||||||
"REQUIRED: The complete mxGraphModel XML. Must always be provided.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ xml: inputXml }) => {
|
async ({ xml: inputXml }) => {
|
||||||
@@ -239,7 +199,7 @@ COMMON STYLES:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Setting diagram content, ${xml.length} chars`)
|
log.info(`Displaying diagram, ${xml.length} chars`)
|
||||||
|
|
||||||
// Sync from browser state first
|
// Sync from browser state first
|
||||||
const browserState = getState(currentSession.id)
|
const browserState = getState(currentSession.id)
|
||||||
@@ -266,20 +226,20 @@ COMMON STYLES:
|
|||||||
// Save AI result (no SVG yet - will be captured by browser)
|
// Save AI result (no SVG yet - will be captured by browser)
|
||||||
addHistory(currentSession.id, xml, "")
|
addHistory(currentSession.id, xml, "")
|
||||||
|
|
||||||
log.info(`Diagram content set successfully`)
|
log.info(`Diagram displayed successfully`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Diagram content set successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
|
text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : String(error)
|
error instanceof Error ? error.message : String(error)
|
||||||
log.error("create_new_diagram failed:", message)
|
log.error("display_diagram failed:", message)
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Error: ${message}` }],
|
content: [{ type: "text", text: `Error: ${message}` }],
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -380,7 +340,7 @@ server.registerTool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Error: No diagram to edit. Please create a diagram first with create_new_diagram.",
|
text: "Error: No diagram to edit. Please create a diagram first with display_diagram.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -514,7 +474,7 @@ server.registerTool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "No diagram exists yet. Use create_new_diagram to create one.",
|
text: "No diagram exists yet. Use display_diagram to create one.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Doubao</title><path d="M5.31 15.756c.172-3.75 1.883-5.999 2.549-6.739-3.26 2.058-5.425 5.658-6.358 8.308v1.12C1.501 21.513 4.226 24 7.59 24a6.59 6.59 0 002.2-.375c.353-.12.7-.248 1.039-.378.913-.899 1.65-1.91 2.243-2.992-4.877 2.431-7.974.072-7.763-4.5l.002.001z" fill="#1E37FC"></path><path d="M22.57 10.283c-1.212-.901-4.109-2.404-7.397-2.8.295 3.792.093 8.766-2.1 12.773a12.782 12.782 0 01-2.244 2.992c3.764-1.448 6.746-3.457 8.596-5.219 2.82-2.683 3.353-5.178 3.361-6.66a2.737 2.737 0 00-.216-1.084v-.002z" fill="#37E1BE"></path><path d="M14.303 1.867C12.955.7 11.248 0 9.39 0 7.532 0 5.883.677 4.545 1.807 2.791 3.29 1.627 5.557 1.5 8.125v9.201c.932-2.65 3.097-6.25 6.357-8.307.5-.318 1.025-.595 1.569-.829 1.883-.801 3.878-.932 5.746-.706-.222-2.83-.718-5.002-.87-5.617h.001z" fill="#A569FF"></path><path d="M17.305 4.961a199.47 199.47 0 01-1.08-1.094c-.202-.213-.398-.419-.586-.622l-1.333-1.378c.151.615.648 2.786.869 5.617 3.288.395 6.185 1.898 7.396 2.8-1.306-1.275-3.475-3.487-5.266-5.323z" fill="#1E37FC"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -22,7 +22,6 @@
|
|||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": ["electron/electron.d.ts"],
|
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user