mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
Compare commits
1 Commits
v0.4.0
...
fix/preven
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d0d4039d |
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
"extends": [
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"next/typescript"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
35
.github/CONTRIBUTING.md
vendored
35
.github/CONTRIBUTING.md
vendored
@@ -1,35 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/YOUR_USERNAME/next-ai-draw-io.git
|
|
||||||
cd next-ai-draw-io
|
|
||||||
npm install
|
|
||||||
cp env.example .env.local
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
We use [Biome](https://biomejs.dev/) for linting and formatting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run format # Format code
|
|
||||||
npm run lint # Check lint errors
|
|
||||||
npm run check # Run all checks (CI)
|
|
||||||
```
|
|
||||||
|
|
||||||
Pre-commit hooks via Husky will run Biome automatically on staged files.
|
|
||||||
|
|
||||||
For a better experience, install the [Biome VS Code extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) for real-time linting and format-on-save.
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
1. Create a feature branch
|
|
||||||
2. Make changes and ensure `npm run check` passes
|
|
||||||
3. Submit PR against `main` with a clear description
|
|
||||||
|
|
||||||
## Issues
|
|
||||||
|
|
||||||
Include steps to reproduce, expected vs actual behavior, and AI provider used.
|
|
||||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Report a bug to help us improve
|
|
||||||
title: '[Bug] '
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your thoughts.
|
|
||||||
|
|
||||||
## Bug Description
|
|
||||||
A brief description of the issue.
|
|
||||||
|
|
||||||
## Steps to Reproduce
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '...'
|
|
||||||
3. Scroll to '...'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
What you expected to happen.
|
|
||||||
|
|
||||||
## Actual Behavior
|
|
||||||
What actually happened.
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
If applicable, add screenshots to help explain the problem.
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
- OS: [e.g. Windows 11, macOS 14]
|
|
||||||
- Browser: [e.g. Chrome 120, Safari 17]
|
|
||||||
- Version: [e.g. 1.0.0]
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
Any other information about the problem.
|
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: true
|
|
||||||
contact_links:
|
|
||||||
- name: Discussions
|
|
||||||
url: https://github.com/DayuanJiang/next-ai-draw-io/discussions
|
|
||||||
about: Have questions or ideas? Feel free to start a discussion
|
|
||||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature Request
|
|
||||||
about: Suggest a new feature for this project
|
|
||||||
title: '[Feature] '
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
|
|
||||||
|
|
||||||
## Feature Description
|
|
||||||
A brief description of the feature you'd like.
|
|
||||||
|
|
||||||
## Problem Context
|
|
||||||
Is this related to a problem? Please describe.
|
|
||||||
e.g. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
How you'd like this feature to work.
|
|
||||||
|
|
||||||
## Alternatives Considered
|
|
||||||
Any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
Any other information or screenshots about the feature request.
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -40,9 +40,5 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
push-via-ec2.sh
|
push-via-ec2.sh
|
||||||
.claude/
|
.claude/settings.local.json
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
# Cloudflare
|
|
||||||
.dev.vars
|
|
||||||
.open-next/
|
|
||||||
.wrangler/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
npx lint-staged
|
|
||||||
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.biome": "explicit",
|
|
||||||
"source.organizeImports.biome": "explicit"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[javascriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,10 +22,6 @@ COPY . .
|
|||||||
# Disable Next.js telemetry during build
|
# Disable Next.js telemetry during build
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# Build-time argument for self-hosted draw.io URL
|
|
||||||
ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net
|
|
||||||
ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL}
|
|
||||||
|
|
||||||
# Build Next.js application (standalone mode)
|
# Build Next.js application (standalone mode)
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
141
README.md
141
README.md
@@ -4,44 +4,31 @@
|
|||||||
|
|
||||||
**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**
|
**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**
|
||||||
|
|
||||||
English | [中文](./docs/README_CN.md) | [日本語](./docs/README_JA.md)
|
English | [中文](./README_CN.md) | [日本語](./README_JA.md)
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nextjs.org/)
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://nextjs.org/)
|
|
||||||
[](https://react.dev/)
|
|
||||||
[](https://github.com/sponsors/DayuanJiang)
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[🚀 Live Demo](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands
|
||||||
|
- **Image-Based Diagram Replication**: Upload existing diagrams or images and have the AI replicate and enhance them automatically
|
||||||
|
- **Diagram History**: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing.
|
||||||
|
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
|
||||||
|
- **AWS Architecture Diagram Support**: Specialized support for generating AWS architecture diagrams
|
||||||
|
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
||||||
|
|
||||||
|
## **Examples**
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
- [Next AI Draw.io ](#next-ai-drawio-)
|
|
||||||
- [Table of Contents](#table-of-contents)
|
|
||||||
- [Examples](#examples)
|
|
||||||
- [Features](#features)
|
|
||||||
- [Getting Started](#getting-started)
|
|
||||||
- [Try it Online](#try-it-online)
|
|
||||||
- [Run with Docker (Recommended)](#run-with-docker-recommended)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Deployment](#deployment)
|
|
||||||
- [Multi-Provider Support](#multi-provider-support)
|
|
||||||
- [How It Works](#how-it-works)
|
|
||||||
- [Project Structure](#project-structure)
|
|
||||||
- [Support \& Contact](#support--contact)
|
|
||||||
- [Star History](#star-history)
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Here are some example prompts and their generated diagrams:
|
Here are some example prompts and their generated diagrams:
|
||||||
|
|
||||||
@@ -81,29 +68,31 @@ Here are some example prompts and their generated diagrams:
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Features
|
## How It Works
|
||||||
|
|
||||||
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands
|
The application uses the following technologies:
|
||||||
- **Image-Based Diagram Replication**: Upload existing diagrams or images and have the AI replicate and enhance them automatically
|
|
||||||
- **PDF & Text File Upload**: Upload PDF documents and text files to extract content and generate diagrams from existing documents
|
- **Next.js**: For the frontend framework and routing
|
||||||
- **AI Reasoning Display**: View the AI's thinking process for supported models (OpenAI o1/o3, Gemini, Claude, etc.)
|
- **Vercel AI SDK** (`ai` + `@ai-sdk/*`): For streaming AI responses and multi-provider support
|
||||||
- **Diagram History**: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing.
|
- **react-drawio**: For diagram representation and manipulation
|
||||||
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
|
|
||||||
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
||||||
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
|
||||||
|
## Multi-Provider Support
|
||||||
|
|
||||||
|
- AWS Bedrock (default)
|
||||||
|
- OpenAI / OpenAI-compatible APIs (via `OPENAI_BASE_URL`)
|
||||||
|
- Anthropic
|
||||||
|
- Google AI
|
||||||
|
- Azure OpenAI
|
||||||
|
- Ollama
|
||||||
|
- OpenRouter
|
||||||
|
- DeepSeek
|
||||||
|
|
||||||
|
Note that `claude-sonnet-4-5` has trained on draw.io diagrams with AWS logos, so if you want to create AWS architecture diagrams, this is the best choice.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Try it Online
|
|
||||||
|
|
||||||
No installation needed! Try the app directly on our demo site:
|
|
||||||
|
|
||||||
[](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.
|
|
||||||
|
|
||||||
### Run with Docker (Recommended)
|
### Run with Docker (Recommended)
|
||||||
|
|
||||||
If you just want to run it locally, the best way is to use Docker.
|
If you just want to run it locally, the best way is to use Docker.
|
||||||
@@ -120,20 +109,10 @@ docker run -d -p 3000:3000 \
|
|||||||
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use an env file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp env.example .env
|
|
||||||
# Edit .env with your configuration
|
|
||||||
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||||
|
|
||||||
Replace the environment variables with your preferred AI provider configuration. See [Multi-Provider Support](#multi-provider-support) for available options.
|
Replace the environment variables with your preferred AI provider configuration. See [Multi-Provider Support](#multi-provider-support) for available options.
|
||||||
|
|
||||||
> **Offline Deployment:** If `embed.diagrams.net` is blocked, see [Offline Deployment](./docs/offline-deployment.md) for configuration options.
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
@@ -147,6 +126,8 @@ cd next-ai-draw-io
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
# or
|
||||||
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Configure your AI provider:
|
3. Configure your AI provider:
|
||||||
@@ -159,15 +140,11 @@ 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 (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||||
- 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).
|
|
||||||
- `ACCESS_CODE_LIST`: Optional access password(s), can be comma-separated for multiple passwords.
|
|
||||||
|
|
||||||
> Warning: If you do not set `ACCESS_CODE_LIST`, anyone can access your deployed site directly, which may lead to rapid depletion of your token. It is recommended to set this option.
|
See the [Multi-Provider Support](#multi-provider-support) section above for provider-specific configuration examples.
|
||||||
|
|
||||||
See the [Provider Configuration Guide](./docs/ai-providers.md) for detailed setup instructions for each provider.
|
|
||||||
|
|
||||||
4. Run the development server:
|
4. Run the development server:
|
||||||
|
|
||||||
@@ -188,38 +165,6 @@ Or you can deploy by this button.
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
||||||
## Multi-Provider Support
|
|
||||||
|
|
||||||
- AWS Bedrock (default)
|
|
||||||
- OpenAI
|
|
||||||
- Anthropic
|
|
||||||
- Google AI
|
|
||||||
- Azure OpenAI
|
|
||||||
- Ollama
|
|
||||||
- OpenRouter
|
|
||||||
- DeepSeek
|
|
||||||
- SiliconFlow
|
|
||||||
|
|
||||||
All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
|
||||||
|
|
||||||
📖 **[Detailed Provider Configuration Guide](./docs/ai-providers.md)** - See setup instructions for each provider.
|
|
||||||
|
|
||||||
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
|
|
||||||
|
|
||||||
Note that `claude` series has trained on draw.io diagrams with cloud architecture logos like AWS, Azue, GCP. So if you want to create cloud architecture diagrams, this is the best choice.
|
|
||||||
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
The application uses the following technologies:
|
|
||||||
|
|
||||||
- **Next.js**: For the frontend framework and routing
|
|
||||||
- **Vercel AI SDK** (`ai` + `@ai-sdk/*`): For streaming AI responses and multi-provider support
|
|
||||||
- **react-drawio**: For diagram representation and manipulation
|
|
||||||
|
|
||||||
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -239,6 +184,14 @@ lib/ # Utility functions and helpers
|
|||||||
public/ # Static assets including example images
|
public/ # Static assets including example images
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] Allow the LLM to modify the XML instead of generating it from scratch everytime.
|
||||||
|
- [x] Improve the smoothness of shape streaming updates.
|
||||||
|
- [x] Add multiple AI provider support (OpenAI, Anthropic, Google, Azure, Ollama)
|
||||||
|
- [x] Solve the bug that generation will fail for session that longer than 60s.
|
||||||
|
- [ ] Add API config on the UI.
|
||||||
|
|
||||||
## Support & Contact
|
## Support & Contact
|
||||||
|
|
||||||
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!
|
||||||
|
|||||||
@@ -4,16 +4,14 @@
|
|||||||
|
|
||||||
**AI驱动的图表创建工具 - 对话、绘制、可视化**
|
**AI驱动的图表创建工具 - 对话、绘制、可视化**
|
||||||
|
|
||||||
[English](../README.md) | 中文 | [日本語](./README_JA.md)
|
[English](./README.md) | 中文 | [日本語](./README_JA.md)
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nextjs.org/)
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://nextjs.org/)
|
|
||||||
[](https://react.dev/)
|
|
||||||
[](https://github.com/sponsors/DayuanJiang)
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[🚀 在线演示](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,23 +19,16 @@
|
|||||||
|
|
||||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
## 目录
|
## 功能特性
|
||||||
- [Next AI Draw.io](#next-ai-drawio)
|
|
||||||
- [目录](#目录)
|
|
||||||
- [示例](#示例)
|
|
||||||
- [功能特性](#功能特性)
|
|
||||||
- [快速开始](#快速开始)
|
|
||||||
- [在线试用](#在线试用)
|
|
||||||
- [使用Docker运行(推荐)](#使用docker运行推荐)
|
|
||||||
- [安装](#安装)
|
|
||||||
- [部署](#部署)
|
|
||||||
- [多提供商支持](#多提供商支持)
|
|
||||||
- [工作原理](#工作原理)
|
|
||||||
- [项目结构](#项目结构)
|
|
||||||
- [支持与联系](#支持与联系)
|
|
||||||
- [Star历史](#star历史)
|
|
||||||
|
|
||||||
## 示例
|
- **LLM驱动的图表创建**:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
||||||
|
- **基于图像的图表复制**:上传现有图表或图像,让AI自动复制和增强
|
||||||
|
- **图表历史记录**:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本
|
||||||
|
- **交互式聊天界面**:与AI实时对话来完善您的图表
|
||||||
|
- **AWS架构图支持**:专门支持生成AWS架构图
|
||||||
|
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
|
||||||
|
## **示例**
|
||||||
|
|
||||||
以下是一些示例提示词及其生成的图表:
|
以下是一些示例提示词及其生成的图表:
|
||||||
|
|
||||||
@@ -47,59 +38,61 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
<td colspan="2" valign="top" align="center">
|
<td colspan="2" valign="top" align="center">
|
||||||
<strong>动画Transformer连接器</strong><br />
|
<strong>动画Transformer连接器</strong><br />
|
||||||
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
|
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
|
||||||
<img src="../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
|
<img src="./public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>GCP架构图</strong><br />
|
<strong>GCP架构图</strong><br />
|
||||||
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
<img src="../public/gcp_demo.svg" alt="GCP架构图" width="480" />
|
<img src="./public/gcp_demo.svg" alt="GCP架构图" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>AWS架构图</strong><br />
|
<strong>AWS架构图</strong><br />
|
||||||
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
<img src="../public/aws_demo.svg" alt="AWS架构图" width="480" />
|
<img src="./public/aws_demo.svg" alt="AWS架构图" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Azure架构图</strong><br />
|
<strong>Azure架构图</strong><br />
|
||||||
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
<img src="../public/azure_demo.svg" alt="Azure架构图" width="480" />
|
<img src="./public/azure_demo.svg" alt="Azure架构图" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>猫咪素描</strong><br />
|
<strong>猫咪素描</strong><br />
|
||||||
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
|
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
|
||||||
<img src="../public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
<img src="./public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 功能特性
|
## 工作原理
|
||||||
|
|
||||||
- **LLM驱动的图表创建**:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
本应用使用以下技术:
|
||||||
- **基于图像的图表复制**:上传现有图表或图像,让AI自动复制和增强
|
|
||||||
- **PDF和文本文件上传**:上传PDF文档和文本文件,提取内容并从现有文档生成图表
|
- **Next.js**:用于前端框架和路由
|
||||||
- **AI推理过程显示**:查看支持模型的AI思考过程(OpenAI o1/o3、Gemini、Claude等)
|
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):用于流式AI响应和多提供商支持
|
||||||
- **图表历史记录**:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本
|
- **react-drawio**:用于图表表示和操作
|
||||||
- **交互式聊天界面**:与AI实时对话来完善您的图表
|
|
||||||
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
||||||
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
|
||||||
|
## 多提供商支持
|
||||||
|
|
||||||
|
- AWS Bedrock(默认)
|
||||||
|
- OpenAI / OpenAI兼容API(通过 `OPENAI_BASE_URL`)
|
||||||
|
- Anthropic
|
||||||
|
- Google AI
|
||||||
|
- Azure OpenAI
|
||||||
|
- Ollama
|
||||||
|
- OpenRouter
|
||||||
|
- DeepSeek
|
||||||
|
|
||||||
|
注意:`claude-sonnet-4-5` 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 在线试用
|
|
||||||
|
|
||||||
无需安装!直接在我们的演示站点试用:
|
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
|
||||||
|
|
||||||
> 注意:由于访问量较大,演示站点目前使用 minimax-m2 模型。如需获得最佳效果,建议使用 Claude Sonnet 4.5 或 Claude Opus 4.5 自行部署。
|
|
||||||
|
|
||||||
> **使用自己的 API Key**:您可以使用自己的 API Key 来绕过演示站点的用量限制。点击聊天面板中的设置图标即可配置您的 Provider 和 API Key。您的 Key 仅保存在浏览器本地,不会被存储在服务器上。
|
|
||||||
|
|
||||||
### 使用Docker运行(推荐)
|
### 使用Docker运行(推荐)
|
||||||
|
|
||||||
如果您只想在本地运行,最好的方式是使用Docker。
|
如果您只想在本地运行,最好的方式是使用Docker。
|
||||||
@@ -116,20 +109,10 @@ docker run -d -p 3000:3000 \
|
|||||||
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
或者使用 env 文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp env.example .env
|
|
||||||
# 编辑 .env 填写您的配置
|
|
||||||
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
|
在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
|
||||||
|
|
||||||
请根据您首选的AI提供商配置替换环境变量。可用选项请参阅[多提供商支持](#多提供商支持)。
|
请根据您首选的AI提供商配置替换环境变量。可用选项请参阅[多提供商支持](#多提供商支持)。
|
||||||
|
|
||||||
> **离线部署:** 如果 `embed.diagrams.net` 被屏蔽,请参阅 [离线部署指南](./offline-deployment.md) 了解配置选项。
|
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
1. 克隆仓库:
|
1. 克隆仓库:
|
||||||
@@ -143,6 +126,8 @@ cd next-ai-draw-io
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
# 或
|
||||||
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 配置您的AI提供商:
|
3. 配置您的AI提供商:
|
||||||
@@ -155,15 +140,11 @@ cp env.example .env.local
|
|||||||
|
|
||||||
编辑 `.env.local` 并配置您选择的提供商:
|
编辑 `.env.local` 并配置您选择的提供商:
|
||||||
|
|
||||||
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||||
- 将 `AI_MODEL` 设置为您要使用的特定模型
|
- 将 `AI_MODEL` 设置为您要使用的特定模型
|
||||||
- 添加您的提供商所需的API密钥
|
- 添加您的提供商所需的API密钥
|
||||||
- `TEMPERATURE`:可选的温度设置(例如 `0` 表示确定性输出)。对于不支持此参数的模型(如推理模型),请不要设置。
|
|
||||||
- `ACCESS_CODE_LIST` 访问密码,可选,可以使用逗号隔开多个密码。
|
|
||||||
|
|
||||||
> 警告:如果不填写 `ACCESS_CODE_LIST`,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。
|
请参阅上面的[多提供商支持](#多提供商支持)部分了解特定提供商的配置示例。
|
||||||
|
|
||||||
详细设置说明请参阅[提供商配置指南](./ai-providers.md)。
|
|
||||||
|
|
||||||
4. 运行开发服务器:
|
4. 运行开发服务器:
|
||||||
|
|
||||||
@@ -184,38 +165,6 @@ npm run dev
|
|||||||
|
|
||||||
请确保在Vercel控制台中**设置环境变量**,就像您在本地 `.env.local` 文件中所做的那样。
|
请确保在Vercel控制台中**设置环境变量**,就像您在本地 `.env.local` 文件中所做的那样。
|
||||||
|
|
||||||
|
|
||||||
## 多提供商支持
|
|
||||||
|
|
||||||
- AWS Bedrock(默认)
|
|
||||||
- OpenAI
|
|
||||||
- Anthropic
|
|
||||||
- Google AI
|
|
||||||
- Azure OpenAI
|
|
||||||
- Ollama
|
|
||||||
- OpenRouter
|
|
||||||
- DeepSeek
|
|
||||||
- SiliconFlow
|
|
||||||
|
|
||||||
除AWS Bedrock和OpenRouter外,所有提供商都支持自定义端点。
|
|
||||||
|
|
||||||
📖 **[详细的提供商配置指南](./ai-providers.md)** - 查看各提供商的设置说明。
|
|
||||||
|
|
||||||
**模型要求**:此任务需要强大的模型能力,因为它涉及生成具有严格格式约束的长文本(draw.io XML)。推荐使用Claude Sonnet 4.5、GPT-4o、Gemini 2.0和DeepSeek V3/R1。
|
|
||||||
|
|
||||||
注意:`claude-sonnet-4-5` 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
|
||||||
|
|
||||||
|
|
||||||
## 工作原理
|
|
||||||
|
|
||||||
本应用使用以下技术:
|
|
||||||
|
|
||||||
- **Next.js**:用于前端框架和路由
|
|
||||||
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):用于流式AI响应和多提供商支持
|
|
||||||
- **react-drawio**:用于图表表示和操作
|
|
||||||
|
|
||||||
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -235,6 +184,14 @@ lib/ # 工具函数和辅助程序
|
|||||||
public/ # 静态资源包括示例图片
|
public/ # 静态资源包括示例图片
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
|
||||||
|
- [x] 允许LLM修改XML而不是每次从头生成
|
||||||
|
- [x] 提高形状流式更新的流畅度
|
||||||
|
- [x] 添加多AI提供商支持(OpenAI, Anthropic, Google, Azure, Ollama)
|
||||||
|
- [x] 解决超过60秒的会话生成失败的bug
|
||||||
|
- [ ] 在UI上添加API配置
|
||||||
|
|
||||||
## 支持与联系
|
## 支持与联系
|
||||||
|
|
||||||
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
||||||
@@ -4,16 +4,14 @@
|
|||||||
|
|
||||||
**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**
|
**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**
|
||||||
|
|
||||||
[English](../README.md) | [中文](./README_CN.md) | 日本語
|
[English](./README.md) | [中文](./README_CN.md) | 日本語
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nextjs.org/)
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://nextjs.org/)
|
|
||||||
[](https://react.dev/)
|
|
||||||
[](https://github.com/sponsors/DayuanJiang)
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
[🚀 ライブデモ](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,23 +19,16 @@ AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケ
|
|||||||
|
|
||||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
## 目次
|
## 機能
|
||||||
- [Next AI Draw.io](#next-ai-drawio)
|
|
||||||
- [目次](#目次)
|
|
||||||
- [例](#例)
|
|
||||||
- [機能](#機能)
|
|
||||||
- [はじめに](#はじめに)
|
|
||||||
- [オンラインで試す](#オンラインで試す)
|
|
||||||
- [Dockerで実行(推奨)](#dockerで実行推奨)
|
|
||||||
- [インストール](#インストール)
|
|
||||||
- [デプロイ](#デプロイ)
|
|
||||||
- [マルチプロバイダーサポート](#マルチプロバイダーサポート)
|
|
||||||
- [仕組み](#仕組み)
|
|
||||||
- [プロジェクト構造](#プロジェクト構造)
|
|
||||||
- [サポート&お問い合わせ](#サポートお問い合わせ)
|
|
||||||
- [スター履歴](#スター履歴)
|
|
||||||
|
|
||||||
## 例
|
- **LLM搭載のダイアグラム作成**:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
||||||
|
- **画像ベースのダイアグラム複製**:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
|
||||||
|
- **ダイアグラム履歴**:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
|
||||||
|
- **インタラクティブなチャットインターフェース**:AIとリアルタイムでコミュニケーションしてダイアグラムを改善
|
||||||
|
- **AWSアーキテクチャダイアグラムサポート**:AWSアーキテクチャダイアグラムの生成を専門的にサポート
|
||||||
|
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
|
||||||
|
## **例**
|
||||||
|
|
||||||
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
||||||
|
|
||||||
@@ -47,59 +38,61 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
|||||||
<td colspan="2" valign="top" align="center">
|
<td colspan="2" valign="top" align="center">
|
||||||
<strong>アニメーションTransformerコネクタ</strong><br />
|
<strong>アニメーションTransformerコネクタ</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
|
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
|
||||||
<img src="../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
|
<img src="./public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>GCPアーキテクチャ図</strong><br />
|
<strong>GCPアーキテクチャ図</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
<img src="../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
|
<img src="./public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>AWSアーキテクチャ図</strong><br />
|
<strong>AWSアーキテクチャ図</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
<img src="../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
|
<img src="./public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Azureアーキテクチャ図</strong><br />
|
<strong>Azureアーキテクチャ図</strong><br />
|
||||||
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
<img src="../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
|
<img src="./public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>猫のスケッチ</strong><br />
|
<strong>猫のスケッチ</strong><br />
|
||||||
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
|
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
|
||||||
<img src="../public/cat_demo.svg" alt="猫の絵" width="240" />
|
<img src="./public/cat_demo.svg" alt="猫の絵" width="240" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 機能
|
## 仕組み
|
||||||
|
|
||||||
- **LLM搭載のダイアグラム作成**:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
本アプリケーションは以下の技術を使用しています:
|
||||||
- **画像ベースのダイアグラム複製**:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
|
|
||||||
- **PDFとテキストファイルのアップロード**:PDFドキュメントやテキストファイルをアップロードして、既存のドキュメントからコンテンツを抽出し、ダイアグラムを生成
|
- **Next.js**:フロントエンドフレームワークとルーティング
|
||||||
- **AI推論プロセス表示**:サポートされているモデル(OpenAI o1/o3、Gemini、Claudeなど)のAIの思考プロセスを表示
|
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
||||||
- **ダイアグラム履歴**:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
|
- **react-drawio**:ダイアグラムの表現と操作
|
||||||
- **インタラクティブなチャットインターフェース**:AIとリアルタイムでコミュニケーションしてダイアグラムを改善
|
|
||||||
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
||||||
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
|
||||||
|
## マルチプロバイダーサポート
|
||||||
|
|
||||||
|
- AWS Bedrock(デフォルト)
|
||||||
|
- OpenAI / OpenAI互換API(`OPENAI_BASE_URL`経由)
|
||||||
|
- Anthropic
|
||||||
|
- Google AI
|
||||||
|
- Azure OpenAI
|
||||||
|
- Ollama
|
||||||
|
- OpenRouter
|
||||||
|
- DeepSeek
|
||||||
|
|
||||||
|
注:`claude-sonnet-4-5`はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
|
|
||||||
## はじめに
|
## はじめに
|
||||||
|
|
||||||
### オンラインで試す
|
|
||||||
|
|
||||||
インストール不要!デモサイトで直接お試しください:
|
|
||||||
|
|
||||||
[](https://next-ai-drawio.jiang.jp/)
|
|
||||||
|
|
||||||
> 注意:アクセス数が多いため、デモサイトでは現在 minimax-m2 モデルを使用しています。最高の結果を得るには、Claude Sonnet 4.5 または Claude Opus 4.5 でのセルフホスティングをお勧めします。
|
|
||||||
|
|
||||||
> **自分のAPIキーを使用**:自分のAPIキーを使用することで、デモサイトの利用制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。キーはブラウザのローカルに保存され、サーバーには保存されません。
|
|
||||||
|
|
||||||
### Dockerで実行(推奨)
|
### Dockerで実行(推奨)
|
||||||
|
|
||||||
ローカルで実行したいだけなら、Dockerを使用するのが最も簡単です。
|
ローカルで実行したいだけなら、Dockerを使用するのが最も簡単です。
|
||||||
@@ -116,20 +109,10 @@ docker run -d -p 3000:3000 \
|
|||||||
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
または env ファイルを使用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp env.example .env
|
|
||||||
# .env を編集して設定を入力
|
|
||||||
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
ブラウザで [http://localhost:3000](http://localhost:3000) を開いてください。
|
ブラウザで [http://localhost:3000](http://localhost:3000) を開いてください。
|
||||||
|
|
||||||
環境変数はお好みのAIプロバイダー設定に置き換えてください。利用可能なオプションについては[マルチプロバイダーサポート](#マルチプロバイダーサポート)を参照してください。
|
環境変数はお好みのAIプロバイダー設定に置き換えてください。利用可能なオプションについては[マルチプロバイダーサポート](#マルチプロバイダーサポート)を参照してください。
|
||||||
|
|
||||||
> **オフラインデプロイ:** `embed.diagrams.net` がブロックされている場合は、[オフラインデプロイガイド](./offline-deployment.md) で設定オプションをご確認ください。
|
|
||||||
|
|
||||||
### インストール
|
### インストール
|
||||||
|
|
||||||
1. リポジトリをクローン:
|
1. リポジトリをクローン:
|
||||||
@@ -143,6 +126,8 @@ cd next-ai-draw-io
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
# または
|
||||||
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. AIプロバイダーを設定:
|
3. AIプロバイダーを設定:
|
||||||
@@ -155,15 +140,11 @@ cp env.example .env.local
|
|||||||
|
|
||||||
`.env.local`を編集して選択したプロバイダーを設定:
|
`.env.local`を編集して選択したプロバイダーを設定:
|
||||||
|
|
||||||
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||||
- `AI_MODEL`を使用する特定のモデルに設定
|
- `AI_MODEL`を使用する特定のモデルに設定
|
||||||
- プロバイダーに必要なAPIキーを追加
|
- プロバイダーに必要なAPIキーを追加
|
||||||
- `TEMPERATURE`:オプションの温度設定(例:`0`で決定論的な出力)。温度をサポートしないモデル(推論モデルなど)では設定しないでください。
|
|
||||||
- `ACCESS_CODE_LIST` アクセスパスワード(オプション)。カンマ区切りで複数のパスワードを指定できます。
|
|
||||||
|
|
||||||
> 警告:`ACCESS_CODE_LIST`を設定しない場合、誰でもデプロイされたサイトに直接アクセスできるため、トークンが急速に消費される可能性があります。このオプションを設定することをお勧めします。
|
プロバイダー固有の設定例については、上記の[マルチプロバイダーサポート](#マルチプロバイダーサポート)セクションを参照してください。
|
||||||
|
|
||||||
詳細な設定手順については[プロバイダー設定ガイド](./ai-providers.md)を参照してください。
|
|
||||||
|
|
||||||
4. 開発サーバーを起動:
|
4. 開発サーバーを起動:
|
||||||
|
|
||||||
@@ -184,38 +165,6 @@ Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成
|
|||||||
|
|
||||||
ローカルの`.env.local`ファイルと同様に、Vercelダッシュボードで**環境変数を設定**してください。
|
ローカルの`.env.local`ファイルと同様に、Vercelダッシュボードで**環境変数を設定**してください。
|
||||||
|
|
||||||
|
|
||||||
## マルチプロバイダーサポート
|
|
||||||
|
|
||||||
- AWS Bedrock(デフォルト)
|
|
||||||
- OpenAI
|
|
||||||
- Anthropic
|
|
||||||
- Google AI
|
|
||||||
- Azure OpenAI
|
|
||||||
- Ollama
|
|
||||||
- OpenRouter
|
|
||||||
- DeepSeek
|
|
||||||
- SiliconFlow
|
|
||||||
|
|
||||||
AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタムエンドポイントをサポートしています。
|
|
||||||
|
|
||||||
📖 **[詳細なプロバイダー設定ガイド](./ai-providers.md)** - 各プロバイダーの設定手順をご覧ください。
|
|
||||||
|
|
||||||
**モデル要件**:このタスクは厳密なフォーマット制約(draw.io XML)を持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-4o、Gemini 2.0、DeepSeek V3/R1を推奨します。
|
|
||||||
|
|
||||||
注:`claude-sonnet-4-5`はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
|
||||||
|
|
||||||
|
|
||||||
## 仕組み
|
|
||||||
|
|
||||||
本アプリケーションは以下の技術を使用しています:
|
|
||||||
|
|
||||||
- **Next.js**:フロントエンドフレームワークとルーティング
|
|
||||||
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
|
||||||
- **react-drawio**:ダイアグラムの表現と操作
|
|
||||||
|
|
||||||
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
|
||||||
|
|
||||||
## プロジェクト構造
|
## プロジェクト構造
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -235,6 +184,14 @@ lib/ # ユーティリティ関数とヘルパー
|
|||||||
public/ # サンプル画像を含む静的アセット
|
public/ # サンプル画像を含む静的アセット
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [x] LLMが毎回ゼロから生成する代わりにXMLを修正できるようにする
|
||||||
|
- [x] シェイプストリーミング更新の滑らかさを改善
|
||||||
|
- [x] 複数のAIプロバイダーサポートを追加(OpenAI, Anthropic, Google, Azure, Ollama)
|
||||||
|
- [x] 60秒以上のセッションで生成が失敗するバグを解決
|
||||||
|
- [ ] UIにAPI設定を追加
|
||||||
|
|
||||||
## サポート&お問い合わせ
|
## サポート&お問い合わせ
|
||||||
|
|
||||||
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
||||||
22
amplify.yml
Normal file
22
amplify.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: 1
|
||||||
|
frontend:
|
||||||
|
phases:
|
||||||
|
preBuild:
|
||||||
|
commands:
|
||||||
|
- npm ci --cache .npm --prefer-offline
|
||||||
|
build:
|
||||||
|
commands:
|
||||||
|
# Write env vars to .env.production for Next.js SSR runtime
|
||||||
|
- env | grep -e AI_MODEL >> .env.production
|
||||||
|
- env | grep -e AI_PROVIDER >> .env.production
|
||||||
|
- env | grep -e OPENAI_API_KEY >> .env.production
|
||||||
|
- env | grep -e NEXT_PUBLIC_ >> .env.production
|
||||||
|
- npm run build
|
||||||
|
artifacts:
|
||||||
|
baseDirectory: .next
|
||||||
|
files:
|
||||||
|
- '**/*'
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .next/cache/**/*
|
||||||
|
- .npm/**/*
|
||||||
@@ -1,50 +1,29 @@
|
|||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image"
|
import Link from "next/link";
|
||||||
import Link from "next/link"
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { FaGithub } from "react-icons/fa"
|
import Image from "next/image";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "关于 - Next AI Draw.io",
|
title: "关于 - Next AI Draw.io",
|
||||||
description:
|
description: "AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
|
||||||
"AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
|
|
||||||
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
|
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
|
||||||
}
|
};
|
||||||
|
|
||||||
function formatNumber(num: number): string {
|
|
||||||
if (num >= 1000) {
|
|
||||||
return `${num / 1000}k`
|
|
||||||
}
|
|
||||||
return num.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AboutCN() {
|
export default function AboutCN() {
|
||||||
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
|
|
||||||
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
|
|
||||||
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link
|
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
||||||
href="/"
|
|
||||||
className="text-xl font-bold text-gray-900 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
Next AI Draw.io
|
Next AI Draw.io
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link
|
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
||||||
href="/"
|
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
|
||||||
>
|
|
||||||
编辑器
|
编辑器
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href="/about/cn" className="text-blue-600 font-semibold">
|
||||||
href="/about/cn"
|
|
||||||
className="text-blue-600 font-semibold"
|
|
||||||
>
|
|
||||||
关于
|
关于
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
@@ -66,154 +45,23 @@ export default function AboutCN() {
|
|||||||
<article className="prose prose-lg max-w-none">
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
||||||
Next AI Draw.io
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-gray-600 font-medium">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI驱动的图表创建工具 - 对话、绘制、可视化
|
AI驱动的图表创建工具 - 对话、绘制、可视化
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4 mt-4 text-sm">
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
<Link
|
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
|
||||||
href="/about"
|
|
||||||
className="text-gray-600 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
English
|
|
||||||
</Link>
|
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link
|
<Link href="/about/cn" className="text-blue-600 font-semibold">中文</Link>
|
||||||
href="/about/cn"
|
|
||||||
className="text-blue-600 font-semibold"
|
|
||||||
>
|
|
||||||
中文
|
|
||||||
</Link>
|
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link
|
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600">日本語</Link>
|
||||||
href="/about/ja"
|
|
||||||
className="text-gray-600 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
日本語
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-rose-400 opacity-20" />
|
<p className="text-amber-800">
|
||||||
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
本应用设计运行于 Claude Opus 4.5 以获得最佳性能。但由于流量超出预期,运行顶级模型的成本变得难以承受。为避免服务中断并控制成本,我已将后端切换至 Claude Haiku 4.5。
|
||||||
{/* Header */}
|
</p>
|
||||||
<div className="mb-4">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Story */}
|
|
||||||
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
|
||||||
<p>
|
|
||||||
大家对这个项目的热情太高了——看来大家都真的很喜欢画图!但这也带来了一个幸福的烦恼:我们经常触发出上游
|
|
||||||
AI 接口的频率限制
|
|
||||||
(TPS/TPM)。一旦超限,系统就会暂停,导致请求失败。
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
由于使用量过高,我已将模型从 Claude 更换为{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
minimax-m2
|
|
||||||
</span>
|
|
||||||
,以降低成本。
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
作为一个
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
独立开发者
|
|
||||||
</span>
|
|
||||||
,目前的 API
|
|
||||||
费用全是我自己在掏腰包(纯属为爱发电)。为了保证服务能细水长流,同时也为了避免我个人陷入财务危机,我还设置了以下临时用量限制:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Limits Cards */}
|
|
||||||
<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">
|
|
||||||
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
|
||||||
Token 用量
|
|
||||||
</div>
|
|
||||||
<div className="text-lg font-bold text-gray-900">
|
|
||||||
{formatNumber(tpmLimit)}
|
|
||||||
<span className="text-sm font-normal text-gray-600">
|
|
||||||
/分钟
|
|
||||||
</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="flex items-center gap-3 my-5">
|
|
||||||
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bring Your Own Key */}
|
|
||||||
<div className="text-center mb-5">
|
|
||||||
<h4 className="text-base font-bold text-gray-900 mb-2">
|
|
||||||
使用自己的 API Key
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
|
||||||
您可以使用自己的 API Key
|
|
||||||
来绕过这些限制。点击聊天面板中的设置图标即可配置您的
|
|
||||||
Provider 和 API Key。
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
|
||||||
您的 Key
|
|
||||||
仅保存在浏览器本地,不会被存储在服务器上。
|
|
||||||
</p>
|
|
||||||
</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>
|
||||||
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
@@ -221,167 +69,80 @@ export default function AboutCN() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">功能特性</h2>
|
||||||
功能特性
|
|
||||||
</h2>
|
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li>
|
<li><strong>LLM驱动的图表创建</strong>:利用大语言模型通过自然语言命令直接创建和操作draw.io图表</li>
|
||||||
<strong>LLM驱动的图表创建</strong>
|
<li><strong>基于图像的图表复制</strong>:上传现有图表或图像,让AI自动复制和增强</li>
|
||||||
:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
<li><strong>图表历史记录</strong>:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本</li>
|
||||||
</li>
|
<li><strong>交互式聊天界面</strong>:与AI实时对话来完善您的图表</li>
|
||||||
<li>
|
<li><strong>AWS架构图支持</strong>:专门支持生成AWS架构图</li>
|
||||||
<strong>基于图像的图表复制</strong>
|
<li><strong>动画连接器</strong>:在图表元素之间创建动态动画连接器,实现更好的可视化效果</li>
|
||||||
:上传现有图表或图像,让AI自动复制和增强
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>图表历史记录</strong>
|
|
||||||
:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>交互式聊天界面</strong>
|
|
||||||
:与AI实时对话来完善您的图表
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>AWS架构图支持</strong>
|
|
||||||
:专门支持生成AWS架构图
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>动画连接器</strong>
|
|
||||||
:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Examples */}
|
{/* Examples */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">示例</h2>
|
||||||
示例
|
<p className="text-gray-700 mb-6">以下是一些示例提示词及其生成的图表:</p>
|
||||||
</h2>
|
|
||||||
<p className="text-gray-700 mb-6">
|
|
||||||
以下是一些示例提示词及其生成的图表:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* Animated Transformer */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">动画Transformer连接器</h3>
|
||||||
动画Transformer连接器
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>提示词:</strong> 给我一个带有
|
<strong>提示词:</strong> 给我一个带有<strong>动画连接器</strong>的Transformer架构图。
|
||||||
<strong>动画连接器</strong>的Transformer架构图。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/animated_connectors.svg" alt="带动画连接器的Transformer架构" width={480} height={360} className="mx-auto" />
|
||||||
src="/animated_connectors.svg"
|
|
||||||
alt="带动画连接器的Transformer架构"
|
|
||||||
width={480}
|
|
||||||
height={360}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP架构图</h3>
|
||||||
GCP架构图
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用
|
<strong>提示词:</strong> 使用<strong>GCP图标</strong>生成一个GCP架构图。用户连接到托管在实例上的前端。
|
||||||
<strong>GCP图标</strong>
|
|
||||||
生成一个GCP架构图。用户连接到托管在实例上的前端。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/gcp_demo.svg" alt="GCP架构图" width={400} height={300} className="mx-auto" />
|
||||||
src="/gcp_demo.svg"
|
|
||||||
alt="GCP架构图"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS架构图</h3>
|
||||||
AWS架构图
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用
|
<strong>提示词:</strong> 使用<strong>AWS图标</strong>生成一个AWS架构图。用户连接到托管在实例上的前端。
|
||||||
<strong>AWS图标</strong>
|
|
||||||
生成一个AWS架构图。用户连接到托管在实例上的前端。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/aws_demo.svg" alt="AWS架构图" width={400} height={300} className="mx-auto" />
|
||||||
src="/aws_demo.svg"
|
|
||||||
alt="AWS架构图"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure架构图</h3>
|
||||||
Azure架构图
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用
|
<strong>提示词:</strong> 使用<strong>Azure图标</strong>生成一个Azure架构图。用户连接到托管在实例上的前端。
|
||||||
<strong>Azure图标</strong>
|
|
||||||
生成一个Azure架构图。用户连接到托管在实例上的前端。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/azure_demo.svg" alt="Azure架构图" width={400} height={300} className="mx-auto" />
|
||||||
src="/azure_demo.svg"
|
|
||||||
alt="Azure架构图"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">猫咪素描</h3>
|
||||||
猫咪素描
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong>{" "}
|
<strong>提示词:</strong> 给我画一只可爱的猫。
|
||||||
给我画一只可爱的猫。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/cat_demo.svg" alt="猫咪绘图" width={240} height={240} className="mx-auto" />
|
||||||
src="/cat_demo.svg"
|
|
||||||
alt="猫咪绘图"
|
|
||||||
width={240}
|
|
||||||
height={240}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">工作原理</h2>
|
||||||
工作原理
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-700 mb-4">本应用使用以下技术:</p>
|
<p className="text-gray-700 mb-4">本应用使用以下技术:</p>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li>
|
<li><strong>Next.js</strong>:用于前端框架和路由</li>
|
||||||
<strong>Next.js</strong>:用于前端框架和路由
|
<li><strong>Vercel AI SDK</strong>(<code>ai</code> + <code>@ai-sdk/*</code>):用于流式AI响应和多提供商支持</li>
|
||||||
</li>
|
<li><strong>react-drawio</strong>:用于图表表示和操作</li>
|
||||||
<li>
|
|
||||||
<strong>Vercel AI SDK</strong>(<code>ai</code> +{" "}
|
|
||||||
<code>@ai-sdk/*</code>
|
|
||||||
):用于流式AI响应和多提供商支持
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>react-drawio</strong>:用于图表表示和操作
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Multi-Provider Support */}
|
{/* Multi-Provider Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">多提供商支持</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>AWS Bedrock(默认)</li>
|
<li>AWS Bedrock(默认)</li>
|
||||||
<li>
|
<li>OpenAI / OpenAI兼容API(通过 <code>OPENAI_BASE_URL</code>)</li>
|
||||||
OpenAI / OpenAI兼容API(通过{" "}
|
|
||||||
<code>OPENAI_BASE_URL</code>)
|
|
||||||
</li>
|
|
||||||
<li>Anthropic</li>
|
<li>Anthropic</li>
|
||||||
<li>Google AI</li>
|
<li>Google AI</li>
|
||||||
<li>Azure OpenAI</li>
|
<li>Azure OpenAI</li>
|
||||||
@@ -390,15 +151,12 @@ export default function AboutCN() {
|
|||||||
<li>DeepSeek</li>
|
<li>DeepSeek</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> 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
||||||
已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<div className="flex items-center gap-4 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 className="text-2xl font-semibold text-gray-900">支持与联系</h2>
|
||||||
支持与联系
|
|
||||||
</h2>
|
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/DayuanJiang/button"
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
title="Sponsor DayuanJiang"
|
title="Sponsor DayuanJiang"
|
||||||
@@ -409,24 +167,14 @@ export default function AboutCN() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
如果您觉得这个项目有用,请考虑{" "}
|
如果您觉得这个项目有用,请考虑{" "}
|
||||||
<a
|
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
href="https://github.com/sponsors/DayuanJiang"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
赞助
|
赞助
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
来帮助托管在线演示站点!
|
来帮助托管在线演示站点!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mt-2">
|
<p className="text-gray-700 mt-2">
|
||||||
如需支持或咨询,请在{" "}
|
如需支持或咨询,请在{" "}
|
||||||
<a
|
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
GitHub仓库
|
GitHub仓库
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
上提交issue或联系:me[at]jiang.jp
|
上提交issue或联系:me[at]jiang.jp
|
||||||
@@ -453,5 +201,5 @@ export default function AboutCN() {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,29 @@
|
|||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image"
|
import Link from "next/link";
|
||||||
import Link from "next/link"
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { FaGithub } from "react-icons/fa"
|
import Image from "next/image";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "概要 - Next AI Draw.io",
|
title: "概要 - Next AI Draw.io",
|
||||||
description:
|
description: "AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
|
||||||
"AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
|
keywords: ["AIダイアグラム", "draw.io", "AWSアーキテクチャ", "GCPダイアグラム", "Azureダイアグラム", "LLM"],
|
||||||
keywords: [
|
};
|
||||||
"AIダイアグラム",
|
|
||||||
"draw.io",
|
|
||||||
"AWSアーキテクチャ",
|
|
||||||
"GCPダイアグラム",
|
|
||||||
"Azureダイアグラム",
|
|
||||||
"LLM",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNumber(num: number): string {
|
|
||||||
if (num >= 1000) {
|
|
||||||
return `${num / 1000}k`
|
|
||||||
}
|
|
||||||
return num.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AboutJA() {
|
export default function AboutJA() {
|
||||||
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
|
|
||||||
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
|
|
||||||
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link
|
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
||||||
href="/"
|
|
||||||
className="text-xl font-bold text-gray-900 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
Next AI Draw.io
|
Next AI Draw.io
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link
|
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
||||||
href="/"
|
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
|
||||||
>
|
|
||||||
エディタ
|
エディタ
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href="/about/ja" className="text-blue-600 font-semibold">
|
||||||
href="/about/ja"
|
|
||||||
className="text-blue-600 font-semibold"
|
|
||||||
>
|
|
||||||
概要
|
概要
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
@@ -73,152 +45,23 @@ export default function AboutJA() {
|
|||||||
<article className="prose prose-lg max-w-none">
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
||||||
Next AI Draw.io
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-gray-600 font-medium">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI搭載のダイアグラム作成ツール -
|
AI搭載のダイアグラム作成ツール - チャット、描画、可視化
|
||||||
チャット、描画、可視化
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4 mt-4 text-sm">
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
<Link
|
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
|
||||||
href="/about"
|
|
||||||
className="text-gray-600 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
English
|
|
||||||
</Link>
|
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link
|
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600">中文</Link>
|
||||||
href="/about/cn"
|
|
||||||
className="text-gray-600 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
中文
|
|
||||||
</Link>
|
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link
|
<Link href="/about/ja" className="text-blue-600 font-semibold">日本語</Link>
|
||||||
href="/about/ja"
|
|
||||||
className="text-blue-600 font-semibold"
|
|
||||||
>
|
|
||||||
日本語
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-rose-400 opacity-20" />
|
<p className="text-amber-800">
|
||||||
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
本アプリは最高のパフォーマンスを発揮するため、Claude Opus 4.5 で動作するよう設計されています。しかし、予想以上のトラフィックにより、最上位モデルの運用コストが負担となっています。サービスの中断を避け、コストを管理するため、バックエンドを Claude Haiku 4.5 に切り替えました。
|
||||||
{/* Header */}
|
</p>
|
||||||
<div className="mb-4">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Story */}
|
|
||||||
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
|
||||||
<p>
|
|
||||||
予想以上の反響をいただき、ありがとうございます!皆様にダイアグラム作成を楽しんでいただいているのは嬉しい限りですが、その熱量により
|
|
||||||
AI API のレート制限 (TPS/TPM)
|
|
||||||
に頻繁に引っかかってしまっています。制限に達するとシステムが一時停止し、エラーが発生してしまいます。
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
利用量の増加に伴い、コスト削減のためモデルを
|
|
||||||
Claude から{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
minimax-m2
|
|
||||||
</span>{" "}
|
|
||||||
に変更しました。
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
私は現在、
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
個人開発者
|
|
||||||
</span>
|
|
||||||
として API
|
|
||||||
費用を全額自腹で負担しています。サービスを継続し、かつ私自身が借金を背負わないようにするため(笑)、一時的に以下の利用制限も設けさせていただきました。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Limits Cards */}
|
|
||||||
<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">
|
|
||||||
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
|
||||||
トークン使用量
|
|
||||||
</div>
|
|
||||||
<div className="text-lg font-bold text-gray-900">
|
|
||||||
{formatNumber(tpmLimit)}
|
|
||||||
<span className="text-sm font-normal text-gray-600">
|
|
||||||
/分
|
|
||||||
</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="flex items-center gap-3 my-5">
|
|
||||||
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bring Your Own Key */}
|
|
||||||
<div className="text-center mb-5">
|
|
||||||
<h4 className="text-base font-bold text-gray-900 mb-2">
|
|
||||||
自分のAPIキーを使用
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
|
||||||
自分のAPIキーを使用することで、これらの制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
|
||||||
キーはブラウザのローカルに保存され、サーバーには保存されません。
|
|
||||||
</p>
|
|
||||||
</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>
|
||||||
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
@@ -226,176 +69,80 @@ export default function AboutJA() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">機能</h2>
|
||||||
機能
|
|
||||||
</h2>
|
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li>
|
<li><strong>LLM搭載のダイアグラム作成</strong>:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作</li>
|
||||||
<strong>LLM搭載のダイアグラム作成</strong>
|
<li><strong>画像ベースのダイアグラム複製</strong>:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化</li>
|
||||||
:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
<li><strong>ダイアグラム履歴</strong>:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能</li>
|
||||||
</li>
|
<li><strong>インタラクティブなチャットインターフェース</strong>:AIとリアルタイムでコミュニケーションしてダイアグラムを改善</li>
|
||||||
<li>
|
<li><strong>AWSアーキテクチャダイアグラムサポート</strong>:AWSアーキテクチャダイアグラムの生成を専門的にサポート</li>
|
||||||
<strong>画像ベースのダイアグラム複製</strong>
|
<li><strong>アニメーションコネクタ</strong>:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成</li>
|
||||||
:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>ダイアグラム履歴</strong>
|
|
||||||
:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>
|
|
||||||
インタラクティブなチャットインターフェース
|
|
||||||
</strong>
|
|
||||||
:AIとリアルタイムでコミュニケーションしてダイアグラムを改善
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>
|
|
||||||
AWSアーキテクチャダイアグラムサポート
|
|
||||||
</strong>
|
|
||||||
:AWSアーキテクチャダイアグラムの生成を専門的にサポート
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>アニメーションコネクタ</strong>
|
|
||||||
:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Examples */}
|
{/* Examples */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">例</h2>
|
||||||
例
|
<p className="text-gray-700 mb-6">以下はいくつかのプロンプト例と生成されたダイアグラムです:</p>
|
||||||
</h2>
|
|
||||||
<p className="text-gray-700 mb-6">
|
|
||||||
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* Animated Transformer */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">アニメーションTransformerコネクタ</h3>
|
||||||
アニメーションTransformerコネクタ
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>プロンプト:</strong> <strong>アニメーションコネクタ</strong>付きのTransformerアーキテクチャ図を作成してください。
|
||||||
<strong>アニメーションコネクタ</strong>
|
|
||||||
付きのTransformerアーキテクチャ図を作成してください。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width={480} height={360} className="mx-auto" />
|
||||||
src="/animated_connectors.svg"
|
|
||||||
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
|
|
||||||
width={480}
|
|
||||||
height={360}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCPアーキテクチャ図</h3>
|
||||||
GCPアーキテクチャ図
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>プロンプト:</strong> <strong>GCPアイコン</strong>を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
<strong>GCPアイコン</strong>
|
|
||||||
を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/gcp_demo.svg" alt="GCPアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
||||||
src="/gcp_demo.svg"
|
|
||||||
alt="GCPアーキテクチャ図"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWSアーキテクチャ図</h3>
|
||||||
AWSアーキテクチャ図
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>プロンプト:</strong> <strong>AWSアイコン</strong>を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
<strong>AWSアイコン</strong>
|
|
||||||
を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/aws_demo.svg" alt="AWSアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
||||||
src="/aws_demo.svg"
|
|
||||||
alt="AWSアーキテクチャ図"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azureアーキテクチャ図</h3>
|
||||||
Azureアーキテクチャ図
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>プロンプト:</strong> <strong>Azureアイコン</strong>を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
<strong>Azureアイコン</strong>
|
|
||||||
を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/azure_demo.svg" alt="Azureアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
||||||
src="/azure_demo.svg"
|
|
||||||
alt="Azureアーキテクチャ図"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">猫のスケッチ</h3>
|
||||||
猫のスケッチ
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong>{" "}
|
<strong>プロンプト:</strong> かわいい猫を描いてください。
|
||||||
かわいい猫を描いてください。
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/cat_demo.svg" alt="猫の絵" width={240} height={240} className="mx-auto" />
|
||||||
src="/cat_demo.svg"
|
|
||||||
alt="猫の絵"
|
|
||||||
width={240}
|
|
||||||
height={240}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">仕組み</h2>
|
||||||
仕組み
|
<p className="text-gray-700 mb-4">本アプリケーションは以下の技術を使用しています:</p>
|
||||||
</h2>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
本アプリケーションは以下の技術を使用しています:
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li>
|
<li><strong>Next.js</strong>:フロントエンドフレームワークとルーティング</li>
|
||||||
<strong>Next.js</strong>
|
<li><strong>Vercel AI SDK</strong>(<code>ai</code> + <code>@ai-sdk/*</code>):ストリーミングAIレスポンスとマルチプロバイダーサポート</li>
|
||||||
:フロントエンドフレームワークとルーティング
|
<li><strong>react-drawio</strong>:ダイアグラムの表現と操作</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Vercel AI SDK</strong>(<code>ai</code> +{" "}
|
|
||||||
<code>@ai-sdk/*</code>
|
|
||||||
):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>react-drawio</strong>
|
|
||||||
:ダイアグラムの表現と操作
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Multi-Provider Support */}
|
{/* Multi-Provider Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">マルチプロバイダーサポート</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>AWS Bedrock(デフォルト)</li>
|
<li>AWS Bedrock(デフォルト)</li>
|
||||||
<li>
|
<li>OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>経由)</li>
|
||||||
OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>
|
|
||||||
経由)
|
|
||||||
</li>
|
|
||||||
<li>Anthropic</li>
|
<li>Anthropic</li>
|
||||||
<li>Google AI</li>
|
<li>Google AI</li>
|
||||||
<li>Azure OpenAI</li>
|
<li>Azure OpenAI</li>
|
||||||
@@ -404,15 +151,12 @@ export default function AboutJA() {
|
|||||||
<li>DeepSeek</li>
|
<li>DeepSeek</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>はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<div className="flex items-center gap-4 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 className="text-2xl font-semibold text-gray-900">サポート&お問い合わせ</h2>
|
||||||
サポート&お問い合わせ
|
|
||||||
</h2>
|
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/DayuanJiang/button"
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
title="Sponsor DayuanJiang"
|
title="Sponsor DayuanJiang"
|
||||||
@@ -423,24 +167,14 @@ export default function AboutJA() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
||||||
<a
|
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
href="https://github.com/sponsors/DayuanJiang"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
スポンサー
|
スポンサー
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
をご検討ください!
|
をご検討ください!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mt-2">
|
<p className="text-gray-700 mt-2">
|
||||||
サポートやお問い合わせについては、{" "}
|
サポートやお問い合わせについては、{" "}
|
||||||
<a
|
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
GitHubリポジトリ
|
GitHubリポジトリ
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
でissueを開くか、ご連絡ください:me[at]jiang.jp
|
でissueを開くか、ご連絡ください:me[at]jiang.jp
|
||||||
@@ -462,11 +196,10 @@ export default function AboutJA() {
|
|||||||
<footer className="bg-white border-t border-gray-200 mt-16">
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<p className="text-center text-gray-600 text-sm">
|
<p className="text-center text-gray-600 text-sm">
|
||||||
Next AI Draw.io -
|
Next AI Draw.io - オープンソースAI搭載ダイアグラムジェネレーター
|
||||||
オープンソースAI搭載ダイアグラムジェネレーター
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,29 @@
|
|||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image"
|
import Link from "next/link";
|
||||||
import Link from "next/link"
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { FaGithub } from "react-icons/fa"
|
import Image from "next/image";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "About - Next AI Draw.io",
|
title: "About - Next AI Draw.io",
|
||||||
description:
|
description: "AI-Powered Diagram Creation Tool - Chat, Draw, Visualize. Create AWS, GCP, and Azure architecture diagrams with natural language.",
|
||||||
"AI-Powered Diagram Creation Tool - Chat, Draw, Visualize. Create AWS, GCP, and Azure architecture diagrams with natural language.",
|
keywords: ["AI diagram", "draw.io", "AWS architecture", "GCP diagram", "Azure diagram", "LLM"],
|
||||||
keywords: [
|
};
|
||||||
"AI diagram",
|
|
||||||
"draw.io",
|
|
||||||
"AWS architecture",
|
|
||||||
"GCP diagram",
|
|
||||||
"Azure diagram",
|
|
||||||
"LLM",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNumber(num: number): string {
|
|
||||||
if (num >= 1000) {
|
|
||||||
return `${num / 1000}k`
|
|
||||||
}
|
|
||||||
return num.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
|
|
||||||
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
|
|
||||||
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link
|
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
||||||
href="/"
|
|
||||||
className="text-xl font-bold text-gray-900 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
Next AI Draw.io
|
Next AI Draw.io
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link
|
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
||||||
href="/"
|
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
|
||||||
>
|
|
||||||
Editor
|
Editor
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href="/about" className="text-blue-600 font-semibold">
|
||||||
href="/about"
|
|
||||||
className="text-blue-600 font-semibold"
|
|
||||||
>
|
|
||||||
About
|
About
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
@@ -73,355 +45,105 @@ export default function About() {
|
|||||||
<article className="prose prose-lg max-w-none">
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
||||||
Next AI Draw.io
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-gray-600 font-medium">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI-Powered Diagram Creation Tool - Chat, Draw,
|
AI-Powered Diagram Creation Tool - Chat, Draw, Visualize
|
||||||
Visualize
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4 mt-4 text-sm">
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
<Link
|
<Link href="/about" className="text-blue-600 font-semibold">English</Link>
|
||||||
href="/about"
|
|
||||||
className="text-blue-600 font-semibold"
|
|
||||||
>
|
|
||||||
English
|
|
||||||
</Link>
|
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link
|
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600">中文</Link>
|
||||||
href="/about/cn"
|
|
||||||
className="text-gray-600 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
中文
|
|
||||||
</Link>
|
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link
|
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600">日本語</Link>
|
||||||
href="/about/ja"
|
|
||||||
className="text-gray-600 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
日本語
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-rose-400 opacity-20" />
|
<p className="text-amber-800">
|
||||||
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
This app is designed to run on Claude Opus 4.5 for best performance. However, due to higher-than-expected traffic, running the top-tier model has become cost-prohibitive. To avoid service interruptions and manage costs, I have switched the backend to Claude Haiku 4.5.
|
||||||
{/* Header */}
|
</p>
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
|
||||||
Model Change & Usage Limits{" "}
|
|
||||||
<span className="text-sm text-amber-600 font-medium italic font-normal">
|
|
||||||
(Or: Why My Wallet is Crying)
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Story */}
|
|
||||||
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
|
||||||
<p>
|
|
||||||
The response to this project has been
|
|
||||||
incredible—you all love making diagrams!
|
|
||||||
However, this enthusiasm means we are
|
|
||||||
frequently hitting the AI API rate limits
|
|
||||||
(TPS/TPM). When this happens, the system
|
|
||||||
pauses, leading to failed requests.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Due to the high usage, I have changed the
|
|
||||||
model from Claude to{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
minimax-m2
|
|
||||||
</span>
|
|
||||||
, which is more cost-effective.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
As an{" "}
|
|
||||||
<span className="font-semibold text-amber-700">
|
|
||||||
indie developer
|
|
||||||
</span>
|
|
||||||
, 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Limits Cards */}
|
|
||||||
<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">
|
|
||||||
<div className="text-xs font-medium text-amber-700 uppercase tracking-wide mb-1">
|
|
||||||
Token Usage
|
|
||||||
</div>
|
|
||||||
<div className="text-lg font-bold text-gray-900">
|
|
||||||
{formatNumber(tpmLimit)}
|
|
||||||
<span className="text-sm font-normal text-gray-600">
|
|
||||||
/min
|
|
||||||
</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="flex items-center gap-3 my-5">
|
|
||||||
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bring Your Own Key */}
|
|
||||||
<div className="text-center mb-5">
|
|
||||||
<h4 className="text-base font-bold text-gray-900 mb-2">
|
|
||||||
Bring Your Own API Key
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
|
||||||
You can use your own API key to bypass these
|
|
||||||
limits. Click the Settings icon in the chat
|
|
||||||
panel to configure your provider and API
|
|
||||||
key.
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
|
||||||
Your key is stored locally in your browser
|
|
||||||
and is never stored on the server.
|
|
||||||
</p>
|
|
||||||
</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>
|
||||||
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
A Next.js web application that integrates AI
|
A Next.js web application that integrates AI capabilities with draw.io diagrams.
|
||||||
capabilities with draw.io diagrams. Create, modify, and
|
Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
||||||
enhance diagrams through natural language commands and
|
|
||||||
AI-assisted visualization.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Features</h2>
|
||||||
Features
|
|
||||||
</h2>
|
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li>
|
<li><strong>LLM-Powered Diagram Creation</strong>: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands</li>
|
||||||
<strong>LLM-Powered Diagram Creation</strong>:
|
<li><strong>Image-Based Diagram Replication</strong>: Upload existing diagrams or images and have the AI replicate and enhance them automatically</li>
|
||||||
Leverage Large Language Models to create and
|
<li><strong>Diagram History</strong>: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing</li>
|
||||||
manipulate draw.io diagrams directly through natural
|
<li><strong>Interactive Chat Interface</strong>: Communicate with AI to refine your diagrams in real-time</li>
|
||||||
language commands
|
<li><strong>AWS Architecture Diagram Support</strong>: Specialized support for generating AWS architecture diagrams</li>
|
||||||
</li>
|
<li><strong>Animated Connectors</strong>: Create dynamic and animated connectors between diagram elements for better visualization</li>
|
||||||
<li>
|
|
||||||
<strong>Image-Based Diagram Replication</strong>:
|
|
||||||
Upload existing diagrams or images and have the AI
|
|
||||||
replicate and enhance them automatically
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Diagram History</strong>: Comprehensive
|
|
||||||
version control that tracks all changes, allowing
|
|
||||||
you to view and restore previous versions of your
|
|
||||||
diagrams before the AI editing
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Interactive Chat Interface</strong>:
|
|
||||||
Communicate with AI to refine your diagrams in
|
|
||||||
real-time
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>AWS Architecture Diagram Support</strong>:
|
|
||||||
Specialized support for generating AWS architecture
|
|
||||||
diagrams
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Animated Connectors</strong>: Create dynamic
|
|
||||||
and animated connectors between diagram elements for
|
|
||||||
better visualization
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Examples */}
|
{/* Examples */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Examples</h2>
|
||||||
Examples
|
<p className="text-gray-700 mb-6">Here are some example prompts and their generated diagrams:</p>
|
||||||
</h2>
|
|
||||||
<p className="text-gray-700 mb-6">
|
|
||||||
Here are some example prompts and their generated
|
|
||||||
diagrams:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* Animated Transformer */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Animated Transformer Connectors</h3>
|
||||||
Animated Transformer Connectors
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>Prompt:</strong> Give me an{" "}
|
<strong>Prompt:</strong> Give me an <strong>animated connector</strong> diagram of transformer's architecture.
|
||||||
<strong>animated connector</strong> diagram of
|
|
||||||
transformer's architecture.
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/animated_connectors.svg" alt="Transformer Architecture with Animated Connectors" width={480} height={360} className="mx-auto" />
|
||||||
src="/animated_connectors.svg"
|
|
||||||
alt="Transformer Architecture with Animated Connectors"
|
|
||||||
width={480}
|
|
||||||
height={360}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP Architecture Diagram</h3>
|
||||||
GCP Architecture Diagram
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate a GCP
|
<strong>Prompt:</strong> Generate a GCP architecture diagram with <strong>GCP icons</strong>. Users connect to a frontend hosted on an instance.
|
||||||
architecture diagram with{" "}
|
|
||||||
<strong>GCP icons</strong>. Users connect to
|
|
||||||
a frontend hosted on an instance.
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/gcp_demo.svg" alt="GCP Architecture Diagram" width={400} height={300} className="mx-auto" />
|
||||||
src="/gcp_demo.svg"
|
|
||||||
alt="GCP Architecture Diagram"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS Architecture Diagram</h3>
|
||||||
AWS Architecture Diagram
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate an AWS
|
<strong>Prompt:</strong> Generate an AWS architecture diagram with <strong>AWS icons</strong>. Users connect to a frontend hosted on an instance.
|
||||||
architecture diagram with{" "}
|
|
||||||
<strong>AWS icons</strong>. Users connect to
|
|
||||||
a frontend hosted on an instance.
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/aws_demo.svg" alt="AWS Architecture Diagram" width={400} height={300} className="mx-auto" />
|
||||||
src="/aws_demo.svg"
|
|
||||||
alt="AWS Architecture Diagram"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure Architecture Diagram</h3>
|
||||||
Azure Architecture Diagram
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate an Azure
|
<strong>Prompt:</strong> Generate an Azure architecture diagram with <strong>Azure icons</strong>. Users connect to a frontend hosted on an instance.
|
||||||
architecture diagram with{" "}
|
|
||||||
<strong>Azure icons</strong>. Users connect
|
|
||||||
to a frontend hosted on an instance.
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/azure_demo.svg" alt="Azure Architecture Diagram" width={400} height={300} className="mx-auto" />
|
||||||
src="/azure_demo.svg"
|
|
||||||
alt="Azure Architecture Diagram"
|
|
||||||
width={400}
|
|
||||||
height={300}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Cat Sketch</h3>
|
||||||
Cat Sketch
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Draw a cute cat for
|
<strong>Prompt:</strong> Draw a cute cat for me.
|
||||||
me.
|
|
||||||
</p>
|
</p>
|
||||||
<Image
|
<Image src="/cat_demo.svg" alt="Cat Drawing" width={240} height={240} className="mx-auto" />
|
||||||
src="/cat_demo.svg"
|
|
||||||
alt="Cat Drawing"
|
|
||||||
width={240}
|
|
||||||
height={240}
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">How It Works</h2>
|
||||||
How It Works
|
<p className="text-gray-700 mb-4">The application uses the following technologies:</p>
|
||||||
</h2>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
The application uses the following technologies:
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li>
|
<li><strong>Next.js</strong>: For the frontend framework and routing</li>
|
||||||
<strong>Next.js</strong>: For the frontend framework
|
<li><strong>Vercel AI SDK</strong> (<code>ai</code> + <code>@ai-sdk/*</code>): For streaming AI responses and multi-provider support</li>
|
||||||
and routing
|
<li><strong>react-drawio</strong>: For diagram representation and manipulation</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Vercel AI SDK</strong> (<code>ai</code> +{" "}
|
|
||||||
<code>@ai-sdk/*</code>): For streaming AI responses
|
|
||||||
and multi-provider support
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>react-drawio</strong>: For diagram
|
|
||||||
representation and manipulation
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
Diagrams are represented as XML that can be rendered in
|
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
||||||
draw.io. The AI processes your commands and generates or
|
|
||||||
modifies this XML accordingly.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Multi-Provider Support */}
|
{/* Multi-Provider Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Multi-Provider Support</h2>
|
||||||
Multi-Provider Support
|
|
||||||
</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>AWS Bedrock (default)</li>
|
<li>AWS Bedrock (default)</li>
|
||||||
<li>
|
<li>OpenAI / OpenAI-compatible APIs (via <code>OPENAI_BASE_URL</code>)</li>
|
||||||
OpenAI / OpenAI-compatible APIs (via{" "}
|
|
||||||
<code>OPENAI_BASE_URL</code>)
|
|
||||||
</li>
|
|
||||||
<li>Anthropic</li>
|
<li>Anthropic</li>
|
||||||
<li>Google AI</li>
|
<li>Google AI</li>
|
||||||
<li>Azure OpenAI</li>
|
<li>Azure OpenAI</li>
|
||||||
@@ -430,17 +152,12 @@ export default function About() {
|
|||||||
<li>DeepSeek</li>
|
<li>DeepSeek</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 draw.io diagrams with AWS logos, so if you want to create AWS architecture diagrams, this is the best choice.
|
||||||
draw.io diagrams with AWS logos, so if you want to
|
|
||||||
create AWS architecture diagrams, this is the best
|
|
||||||
choice.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<div className="flex items-center gap-4 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 className="text-2xl font-semibold text-gray-900">Support & Contact</h2>
|
||||||
Support & Contact
|
|
||||||
</h2>
|
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/DayuanJiang/button"
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
title="Sponsor DayuanJiang"
|
title="Sponsor DayuanJiang"
|
||||||
@@ -451,24 +168,14 @@ export default function About() {
|
|||||||
</div>
|
</div>
|
||||||
<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 href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
href="https://github.com/sponsors/DayuanJiang"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
sponsoring
|
sponsoring
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
to help host the live demo site!
|
to help host the live demo site!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mt-2">
|
<p className="text-gray-700 mt-2">
|
||||||
For support or inquiries, please open an issue on the{" "}
|
For support or inquiries, please open an issue on the{" "}
|
||||||
<a
|
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
GitHub repository
|
GitHub repository
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
or contact: me[at]jiang.jp
|
or contact: me[at]jiang.jp
|
||||||
@@ -490,11 +197,10 @@ export default function About() {
|
|||||||
<footer className="bg-white border-t border-gray-200 mt-16">
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<p className="text-center text-gray-600 text-sm">
|
<p className="text-center text-gray-600 text-sm">
|
||||||
Next AI Draw.io - Open Source AI-Powered Diagram
|
Next AI Draw.io - Open Source AI-Powered Diagram Generator
|
||||||
Generator
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,402 +1,186 @@
|
|||||||
import {
|
import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai';
|
||||||
APICallError,
|
import { getAIModel } from '@/lib/ai-providers';
|
||||||
convertToModelMessages,
|
import { findCachedResponse } from '@/lib/cached-responses';
|
||||||
createUIMessageStream,
|
import { getSystemPrompt } from '@/lib/system-prompts';
|
||||||
createUIMessageStreamResponse,
|
import { z } from "zod";
|
||||||
LoadAPIKeyError,
|
|
||||||
stepCountIs,
|
|
||||||
streamText,
|
|
||||||
} from "ai"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { getAIModel, supportsPromptCaching } from "@/lib/ai-providers"
|
|
||||||
import { findCachedResponse } from "@/lib/cached-responses"
|
|
||||||
import {
|
|
||||||
getTelemetryConfig,
|
|
||||||
setTraceInput,
|
|
||||||
setTraceOutput,
|
|
||||||
wrapWithObserve,
|
|
||||||
} from "@/lib/langfuse"
|
|
||||||
import { getSystemPrompt } from "@/lib/system-prompts"
|
|
||||||
|
|
||||||
export const maxDuration = 300
|
export const maxDuration = 300;
|
||||||
|
|
||||||
// File upload limits (must match client-side)
|
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
||||||
const MAX_FILES = 5
|
|
||||||
|
|
||||||
// Helper function to validate file parts in messages
|
|
||||||
function validateFileParts(messages: any[]): {
|
|
||||||
valid: boolean
|
|
||||||
error?: string
|
|
||||||
} {
|
|
||||||
const lastMessage = messages[messages.length - 1]
|
|
||||||
const fileParts =
|
|
||||||
lastMessage?.parts?.filter((p: any) => p.type === "file") || []
|
|
||||||
|
|
||||||
if (fileParts.length > MAX_FILES) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error: `Too many files. Maximum ${MAX_FILES} allowed.`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const filePart of fileParts) {
|
|
||||||
// Data URLs format: data:image/png;base64,<data>
|
|
||||||
// Base64 increases size by ~33%, so we check the decoded size
|
|
||||||
if (filePart.url?.startsWith("data:")) {
|
|
||||||
const base64Data = filePart.url.split(",")[1]
|
|
||||||
if (base64Data) {
|
|
||||||
const sizeInBytes = Math.ceil((base64Data.length * 3) / 4)
|
|
||||||
if (sizeInBytes > MAX_FILE_SIZE) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error: `File exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if diagram is minimal/empty
|
// Helper function to check if diagram is minimal/empty
|
||||||
function isMinimalDiagram(xml: string): boolean {
|
function isMinimalDiagram(xml: string): boolean {
|
||||||
const stripped = xml.replace(/\s/g, "")
|
const stripped = xml.replace(/\s/g, '');
|
||||||
return !stripped.includes('id="2"')
|
return !stripped.includes('id="2"');
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to replace historical tool call XML with placeholders
|
|
||||||
// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
|
|
||||||
function replaceHistoricalToolInputs(messages: any[]): any[] {
|
|
||||||
return messages.map((msg) => {
|
|
||||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
const replacedContent = msg.content.map((part: any) => {
|
|
||||||
if (part.type === "tool-call") {
|
|
||||||
const toolName = part.toolName
|
|
||||||
if (
|
|
||||||
toolName === "display_diagram" ||
|
|
||||||
toolName === "edit_diagram"
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
input: {
|
|
||||||
placeholder:
|
|
||||||
"[XML content replaced - see current diagram XML in system context]",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return part
|
|
||||||
})
|
|
||||||
return { ...msg, content: replacedContent }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to fix tool call inputs for Bedrock API
|
|
||||||
// Bedrock requires toolUse.input to be a JSON object, not a string
|
|
||||||
function fixToolCallInputs(messages: any[]): any[] {
|
|
||||||
return messages.map((msg) => {
|
|
||||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
const fixedContent = msg.content.map((part: any) => {
|
|
||||||
if (part.type === "tool-call") {
|
|
||||||
if (typeof part.input === "string") {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(part.input)
|
|
||||||
return { ...part, input: parsed }
|
|
||||||
} catch {
|
|
||||||
// If parsing fails, wrap the string in an object
|
|
||||||
return { ...part, input: { rawInput: part.input } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Input is already an object, but verify it's not null/undefined
|
|
||||||
if (part.input === null || part.input === undefined) {
|
|
||||||
return { ...part, input: {} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return part
|
|
||||||
})
|
|
||||||
return { ...msg, content: fixedContent }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to create cached stream response
|
// Helper function to create cached stream response
|
||||||
function createCachedStreamResponse(xml: string): Response {
|
function createCachedStreamResponse(xml: string): Response {
|
||||||
const toolCallId = `cached-${Date.now()}`
|
const toolCallId = `cached-${Date.now()}`;
|
||||||
|
|
||||||
const stream = createUIMessageStream({
|
const stream = createUIMessageStream({
|
||||||
execute: async ({ writer }) => {
|
execute: async ({ writer }) => {
|
||||||
writer.write({ type: "start" })
|
writer.write({ type: 'start' });
|
||||||
writer.write({
|
writer.write({ type: 'tool-input-start', toolCallId, toolName: 'display_diagram' });
|
||||||
type: "tool-input-start",
|
writer.write({ type: 'tool-input-delta', toolCallId, inputTextDelta: xml });
|
||||||
toolCallId,
|
writer.write({ type: 'tool-input-available', toolCallId, toolName: 'display_diagram', input: { xml } });
|
||||||
toolName: "display_diagram",
|
writer.write({ type: 'finish' });
|
||||||
})
|
},
|
||||||
writer.write({
|
});
|
||||||
type: "tool-input-delta",
|
|
||||||
toolCallId,
|
|
||||||
inputTextDelta: xml,
|
|
||||||
})
|
|
||||||
writer.write({
|
|
||||||
type: "tool-input-available",
|
|
||||||
toolCallId,
|
|
||||||
toolName: "display_diagram",
|
|
||||||
input: { xml },
|
|
||||||
})
|
|
||||||
writer.write({ type: "finish" })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return createUIMessageStreamResponse({ stream })
|
return createUIMessageStreamResponse({ stream });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner handler function
|
// Inner handler function
|
||||||
async function handleChatRequest(req: Request): Promise<Response> {
|
async function handleChatRequest(req: Request): Promise<Response> {
|
||||||
// Check for access code
|
const { messages, xml } = await req.json();
|
||||||
const accessCodes =
|
|
||||||
process.env.ACCESS_CODE_LIST?.split(",")
|
// === CACHE CHECK START ===
|
||||||
.map((code) => code.trim())
|
const isFirstMessage = messages.length === 1;
|
||||||
.filter(Boolean) || []
|
const isEmptyDiagram = !xml || xml.trim() === '' || isMinimalDiagram(xml);
|
||||||
if (accessCodes.length > 0) {
|
|
||||||
const accessCodeHeader = req.headers.get("x-access-code")
|
if (isFirstMessage && isEmptyDiagram) {
|
||||||
if (!accessCodeHeader || !accessCodes.includes(accessCodeHeader)) {
|
const lastMessage = messages[0];
|
||||||
return Response.json(
|
const textPart = lastMessage.parts?.find((p: any) => p.type === 'text');
|
||||||
{
|
const filePart = lastMessage.parts?.find((p: any) => p.type === 'file');
|
||||||
error: "Invalid or missing access code. Please configure it in Settings.",
|
|
||||||
},
|
const cached = findCachedResponse(textPart?.text || '', !!filePart);
|
||||||
{ status: 401 },
|
|
||||||
)
|
if (cached) {
|
||||||
}
|
console.log('[Cache] Returning cached response for:', textPart?.text);
|
||||||
|
return createCachedStreamResponse(cached.xml);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// === CACHE CHECK END ===
|
||||||
|
|
||||||
const { messages, xml, previousXml, sessionId } = await req.json()
|
// Get AI model from environment configuration
|
||||||
|
const { model, providerOptions, headers, modelId } = getAIModel();
|
||||||
|
|
||||||
// Get user IP for Langfuse tracking
|
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
||||||
const forwardedFor = req.headers.get("x-forwarded-for")
|
const systemMessage = getSystemPrompt(modelId);
|
||||||
const userId = forwardedFor?.split(",")[0]?.trim() || "anonymous"
|
|
||||||
|
|
||||||
// Validate sessionId for Langfuse (must be string, max 200 chars)
|
const lastMessage = messages[messages.length - 1];
|
||||||
const validSessionId =
|
|
||||||
sessionId && typeof sessionId === "string" && sessionId.length <= 200
|
|
||||||
? sessionId
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// Extract user input text for Langfuse trace
|
// Extract text from the last message parts
|
||||||
const currentMessage = messages[messages.length - 1]
|
const lastMessageText = lastMessage.parts?.find((part: any) => part.type === 'text')?.text || '';
|
||||||
const userInputText =
|
|
||||||
currentMessage?.parts?.find((p: any) => p.type === "text")?.text || ""
|
|
||||||
|
|
||||||
// Update Langfuse trace with input, session, and user
|
// Extract file parts (images) from the last message
|
||||||
setTraceInput({
|
const fileParts = lastMessage.parts?.filter((part: any) => part.type === 'file') || [];
|
||||||
input: userInputText,
|
|
||||||
sessionId: validSessionId,
|
|
||||||
userId: userId,
|
|
||||||
})
|
|
||||||
|
|
||||||
// === FILE VALIDATION START ===
|
// User input only - XML is now in a separate cached system message
|
||||||
const fileValidation = validateFileParts(messages)
|
const formattedUserInput = `User input:
|
||||||
if (!fileValidation.valid) {
|
|
||||||
return Response.json({ error: fileValidation.error }, { status: 400 })
|
|
||||||
}
|
|
||||||
// === FILE VALIDATION END ===
|
|
||||||
|
|
||||||
// === CACHE CHECK START ===
|
|
||||||
const isFirstMessage = messages.length === 1
|
|
||||||
const isEmptyDiagram = !xml || xml.trim() === "" || isMinimalDiagram(xml)
|
|
||||||
|
|
||||||
if (isFirstMessage && isEmptyDiagram) {
|
|
||||||
const lastMessage = messages[0]
|
|
||||||
const textPart = lastMessage.parts?.find((p: any) => p.type === "text")
|
|
||||||
const filePart = lastMessage.parts?.find((p: any) => p.type === "file")
|
|
||||||
|
|
||||||
const cached = findCachedResponse(textPart?.text || "", !!filePart)
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
return createCachedStreamResponse(cached.xml)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// === CACHE CHECK END ===
|
|
||||||
|
|
||||||
// Read client AI provider overrides from headers
|
|
||||||
const clientOverrides = {
|
|
||||||
provider: req.headers.get("x-ai-provider"),
|
|
||||||
baseUrl: req.headers.get("x-ai-base-url"),
|
|
||||||
apiKey: req.headers.get("x-ai-api-key"),
|
|
||||||
modelId: req.headers.get("x-ai-model"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get AI model with optional client overrides
|
|
||||||
const { model, providerOptions, headers, modelId } =
|
|
||||||
getAIModel(clientOverrides)
|
|
||||||
|
|
||||||
// Check if model supports prompt caching
|
|
||||||
const shouldCache = supportsPromptCaching(modelId)
|
|
||||||
console.log(
|
|
||||||
`[Prompt Caching] ${shouldCache ? "ENABLED" : "DISABLED"} for model: ${modelId}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
|
||||||
const systemMessage = getSystemPrompt(modelId)
|
|
||||||
|
|
||||||
const lastMessage = messages[messages.length - 1]
|
|
||||||
|
|
||||||
// Extract text from the last message parts
|
|
||||||
const lastMessageText =
|
|
||||||
lastMessage.parts?.find((part: any) => part.type === "text")?.text || ""
|
|
||||||
|
|
||||||
// Extract file parts (images) from the last message
|
|
||||||
const fileParts =
|
|
||||||
lastMessage.parts?.filter((part: any) => part.type === "file") || []
|
|
||||||
|
|
||||||
// User input only - XML is now in a separate cached system message
|
|
||||||
const formattedUserInput = `User input:
|
|
||||||
"""md
|
"""md
|
||||||
${lastMessageText}
|
${lastMessageText}
|
||||||
"""`
|
"""`;
|
||||||
|
|
||||||
// Convert UIMessages to ModelMessages and add system message
|
// Convert UIMessages to ModelMessages and add system message
|
||||||
const modelMessages = convertToModelMessages(messages)
|
const modelMessages = convertToModelMessages(messages);
|
||||||
|
|
||||||
// Fix tool call inputs for Bedrock API (requires JSON objects, not strings)
|
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
||||||
const fixedMessages = fixToolCallInputs(modelMessages)
|
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
||||||
|
let enhancedMessages = modelMessages.filter((msg: any) =>
|
||||||
|
msg.content && Array.isArray(msg.content) && msg.content.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
// Replace historical tool call XML with placeholders to reduce tokens and avoid confusion
|
// Update the last message with user input only (XML moved to separate cached system message)
|
||||||
const placeholderMessages = replaceHistoricalToolInputs(fixedMessages)
|
if (enhancedMessages.length >= 1) {
|
||||||
|
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1];
|
||||||
|
if (lastModelMessage.role === 'user') {
|
||||||
|
// Build content array with user input text and file parts
|
||||||
|
const contentParts: any[] = [
|
||||||
|
{ type: 'text', text: formattedUserInput }
|
||||||
|
];
|
||||||
|
|
||||||
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
// Add image parts back
|
||||||
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
for (const filePart of fileParts) {
|
||||||
let enhancedMessages = placeholderMessages.filter(
|
contentParts.push({
|
||||||
(msg: any) =>
|
type: 'image',
|
||||||
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
image: filePart.url,
|
||||||
)
|
mimeType: filePart.mediaType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update the last message with user input only (XML moved to separate cached system message)
|
enhancedMessages = [
|
||||||
if (enhancedMessages.length >= 1) {
|
...enhancedMessages.slice(0, -1),
|
||||||
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
|
{ ...lastModelMessage, content: contentParts }
|
||||||
if (lastModelMessage.role === "user") {
|
];
|
||||||
// Build content array with user input text and file parts
|
|
||||||
const contentParts: any[] = [
|
|
||||||
{ type: "text", text: formattedUserInput },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Add image parts back
|
|
||||||
for (const filePart of fileParts) {
|
|
||||||
contentParts.push({
|
|
||||||
type: "image",
|
|
||||||
image: filePart.url,
|
|
||||||
mimeType: filePart.mediaType,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
enhancedMessages = [
|
|
||||||
...enhancedMessages.slice(0, -1),
|
|
||||||
{ ...lastModelMessage, content: contentParts },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add cache point to the last assistant message in conversation history
|
// Add cache point to the last assistant message in conversation history
|
||||||
// This caches the entire conversation prefix for subsequent requests
|
// This caches the entire conversation prefix for subsequent requests
|
||||||
// Strategy: system (cached) + history with last assistant (cached) + new user message
|
// Strategy: system (cached) + history with last assistant (cached) + new user message
|
||||||
if (shouldCache && enhancedMessages.length >= 2) {
|
if (enhancedMessages.length >= 2) {
|
||||||
// Find the last assistant message (should be second-to-last, before current user message)
|
// Find the last assistant message (should be second-to-last, before current user message)
|
||||||
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
|
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
|
||||||
if (enhancedMessages[i].role === "assistant") {
|
if (enhancedMessages[i].role === 'assistant') {
|
||||||
enhancedMessages[i] = {
|
enhancedMessages[i] = {
|
||||||
...enhancedMessages[i],
|
...enhancedMessages[i],
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
bedrock: { cachePoint: { type: "default" } },
|
bedrock: { cachePoint: { type: 'default' } },
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
break // Only cache the last assistant message
|
break; // Only cache the last assistant message
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// System messages with multiple cache breakpoints for optimal caching:
|
// System messages with multiple cache breakpoints for optimal caching:
|
||||||
// - Breakpoint 1: Static instructions (~1500 tokens) - rarely changes
|
// - Breakpoint 1: Static instructions (~1500 tokens) - rarely changes
|
||||||
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
|
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
|
||||||
// This allows: if only user message changes, both system caches are reused
|
// This allows: if only user message changes, both system caches are reused
|
||||||
// if XML changes, instruction cache is still reused
|
// if XML changes, instruction cache is still reused
|
||||||
const systemMessages = [
|
const systemMessages = [
|
||||||
// Cache breakpoint 1: Instructions (rarely change)
|
// Cache breakpoint 1: Instructions (rarely change)
|
||||||
{
|
{
|
||||||
role: "system" as const,
|
role: 'system' as const,
|
||||||
content: systemMessage,
|
content: systemMessage,
|
||||||
...(shouldCache && {
|
providerOptions: {
|
||||||
providerOptions: {
|
bedrock: { cachePoint: { type: 'default' } },
|
||||||
bedrock: { cachePoint: { type: "default" } },
|
},
|
||||||
},
|
},
|
||||||
}),
|
// Cache breakpoint 2: Current diagram XML context
|
||||||
},
|
{
|
||||||
// Cache breakpoint 2: Previous and Current diagram XML context
|
role: 'system' as const,
|
||||||
{
|
content: `Current diagram XML:\n"""xml\n${xml || ''}\n"""\nWhen using edit_diagram, COPY search patterns exactly from this XML - attribute order matters!`,
|
||||||
role: "system" as const,
|
providerOptions: {
|
||||||
content: `${previousXml ? `Previous diagram XML (before user's last message):\n"""xml\n${previousXml}\n"""\n\n` : ""}Current diagram XML (AUTHORITATIVE - the source of truth):\n"""xml\n${xml || ""}\n"""\n\nIMPORTANT: The "Current diagram XML" is the SINGLE SOURCE OF TRUTH for what's on the canvas right now. The user can manually add, delete, or modify shapes directly in draw.io. Always count and describe elements based on the CURRENT XML, not on what you previously generated. If both previous and current XML are shown, compare them to understand what the user changed. When using edit_diagram, COPY search patterns exactly from the CURRENT XML - attribute order matters!`,
|
bedrock: { cachePoint: { type: 'default' } },
|
||||||
...(shouldCache && {
|
},
|
||||||
providerOptions: {
|
},
|
||||||
bedrock: { cachePoint: { type: "default" } },
|
];
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const allMessages = [...systemMessages, ...enhancedMessages]
|
const allMessages = [...systemMessages, ...enhancedMessages];
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model,
|
model,
|
||||||
stopWhen: stepCountIs(5),
|
messages: allMessages,
|
||||||
messages: allMessages,
|
...(providerOptions && { providerOptions }),
|
||||||
...(providerOptions && { providerOptions }), // This now includes all reasoning configs
|
...(headers && { headers }),
|
||||||
...(headers && { headers }),
|
onFinish: ({ usage, providerMetadata, finishReason, text, toolCalls }) => {
|
||||||
// Langfuse telemetry config (returns undefined if not configured)
|
// Detect potential mid-stream failures (e.g., Bedrock 503 ServiceUnavailableException)
|
||||||
...(getTelemetryConfig({ sessionId: validSessionId, userId }) && {
|
// When this happens, usage is empty and providerMetadata is undefined
|
||||||
experimental_telemetry: getTelemetryConfig({
|
const hasUsage = usage && Object.keys(usage).length > 0;
|
||||||
sessionId: validSessionId,
|
if (!hasUsage) {
|
||||||
userId,
|
console.error('[Stream Error] Empty usage detected - possible Bedrock 503 or mid-stream failure');
|
||||||
}),
|
console.error('[Stream Error] finishReason:', finishReason);
|
||||||
}),
|
console.error('[Stream Error] text received:', text?.substring(0, 200) || '(none)');
|
||||||
// Repair malformed tool calls (model sometimes generates invalid JSON with unescaped quotes)
|
console.error('[Stream Error] toolCalls:', toolCalls?.length || 0);
|
||||||
experimental_repairToolCall: async ({ toolCall }) => {
|
// Log the user's last message for debugging
|
||||||
// The toolCall.input contains the raw JSON string that failed to parse
|
const lastUserMsg = enhancedMessages.filter(m => m.role === 'user').pop();
|
||||||
const rawJson =
|
if (lastUserMsg) {
|
||||||
typeof toolCall.input === "string" ? toolCall.input : null
|
const content = lastUserMsg.content;
|
||||||
|
const preview = Array.isArray(content)
|
||||||
if (rawJson) {
|
? (content.find((c) => c.type === 'text') as { type: 'text'; text: string } | undefined)?.text?.substring(0, 100)
|
||||||
try {
|
: String(content).substring(0, 100);
|
||||||
// Fix unescaped quotes: x="520" should be x=\"520\"
|
console.error('[Stream Error] Last user message preview:', preview);
|
||||||
const fixed = rawJson.replace(
|
}
|
||||||
/([a-zA-Z])="(\d+)"/g,
|
} else {
|
||||||
'$1=\\"$2\\"',
|
console.log('[Cache] Full providerMetadata:', JSON.stringify(providerMetadata, null, 2));
|
||||||
)
|
console.log('[Cache] Usage:', JSON.stringify(usage, null, 2));
|
||||||
const parsed = JSON.parse(fixed)
|
}
|
||||||
return {
|
},
|
||||||
type: "tool-call" as const,
|
tools: {
|
||||||
toolCallId: toolCall.toolCallId,
|
// Client-side tool that will be executed on the client
|
||||||
toolName: toolCall.toolName,
|
display_diagram: {
|
||||||
input: JSON.stringify(parsed),
|
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Repair failed, return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
onFinish: ({ text, usage }) => {
|
|
||||||
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
|
|
||||||
setTraceOutput(text, {
|
|
||||||
promptTokens: usage?.inputTokens,
|
|
||||||
completionTokens: usage?.outputTokens,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
tools: {
|
|
||||||
// Client-side tool that will be executed on the client
|
|
||||||
display_diagram: {
|
|
||||||
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
|
|
||||||
|
|
||||||
VALIDATION RULES (XML will be rejected if violated):
|
VALIDATION RULES (XML will be rejected if violated):
|
||||||
1. All mxCell elements must be DIRECT children of <root> - never nested
|
1. All mxCell elements must be DIRECT children of <root> - never nested
|
||||||
@@ -431,147 +215,79 @@ Notes:
|
|||||||
- For AWS diagrams, use **AWS 2025 icons**.
|
- For AWS diagrams, use **AWS 2025 icons**.
|
||||||
- For animated connectors, add "flowAnimation=1" to edge style.
|
- For animated connectors, add "flowAnimation=1" to edge style.
|
||||||
`,
|
`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
xml: z
|
xml: z.string().describe("XML string to be displayed on draw.io")
|
||||||
.string()
|
})
|
||||||
.describe("XML string to be displayed on draw.io"),
|
},
|
||||||
}),
|
edit_diagram: {
|
||||||
},
|
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
||||||
edit_diagram: {
|
|
||||||
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
|
||||||
CRITICAL: Copy-paste the EXACT search pattern from the "Current diagram XML" in system context. Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly.
|
CRITICAL: Copy-paste the EXACT search pattern from the "Current diagram XML" in system context. Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly.
|
||||||
IMPORTANT: Keep edits concise:
|
IMPORTANT: Keep edits concise:
|
||||||
- COPY the exact mxCell line from the current XML (attribute order matters!)
|
- COPY the exact mxCell line from the current XML (attribute order matters!)
|
||||||
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
|
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
|
||||||
- Break large changes into multiple smaller edits
|
- Break large changes into multiple smaller edits
|
||||||
- Each search must contain complete lines (never truncate mid-line)
|
- Each search must contain complete lines (never truncate mid-line)
|
||||||
- First match only - be specific enough to target the right element
|
- First match only - be specific enough to target the right element`,
|
||||||
|
inputSchema: z.object({
|
||||||
|
edits: z.array(z.object({
|
||||||
|
search: z.string().describe("EXACT lines copied from current XML (preserve attribute order!)"),
|
||||||
|
replace: z.string().describe("Replacement lines")
|
||||||
|
})).describe("Array of search/replace pairs to apply sequentially")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
temperature: 0,
|
||||||
|
});
|
||||||
|
|
||||||
⚠️ JSON ESCAPING: Every " inside string values MUST be escaped as \\". Example: x=\\"100\\" y=\\"200\\" - BOTH quotes need backslashes!`,
|
// Error handler function to provide detailed error messages
|
||||||
inputSchema: z.object({
|
function errorHandler(error: unknown) {
|
||||||
edits: z
|
if (error == null) {
|
||||||
.array(
|
return 'unknown error';
|
||||||
z.object({
|
|
||||||
search: z
|
|
||||||
.string()
|
|
||||||
.describe(
|
|
||||||
"EXACT lines copied from current XML (preserve attribute order!)",
|
|
||||||
),
|
|
||||||
replace: z
|
|
||||||
.string()
|
|
||||||
.describe("Replacement lines"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.describe(
|
|
||||||
"Array of search/replace pairs to apply sequentially",
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(process.env.TEMPERATURE !== undefined && {
|
|
||||||
temperature: parseFloat(process.env.TEMPERATURE),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
return result.toUIMessageStreamResponse({
|
|
||||||
sendReasoning: true,
|
|
||||||
messageMetadata: ({ part }) => {
|
|
||||||
if (part.type === "finish") {
|
|
||||||
const usage = (part as any).totalUsage
|
|
||||||
if (!usage) {
|
|
||||||
console.warn(
|
|
||||||
"[messageMetadata] No usage data in finish part",
|
|
||||||
)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
// Total input = non-cached + cached (these are separate counts)
|
|
||||||
// Note: cacheWriteInputTokens is not available on finish part
|
|
||||||
const totalInputTokens =
|
|
||||||
(usage.inputTokens ?? 0) + (usage.cachedInputTokens ?? 0)
|
|
||||||
return {
|
|
||||||
inputTokens: totalInputTokens,
|
|
||||||
outputTokens: usage.outputTokens ?? 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to categorize errors and return appropriate response
|
|
||||||
function handleError(error: unknown): Response {
|
|
||||||
console.error("Error in chat route:", error)
|
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === "development"
|
|
||||||
|
|
||||||
// Check for specific AI SDK error types
|
|
||||||
if (APICallError.isInstance(error)) {
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
error: error.message,
|
|
||||||
...(isDev && {
|
|
||||||
details: error.responseBody,
|
|
||||||
stack: error.stack,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ status: error.statusCode || 500 },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LoadAPIKeyError.isInstance(error)) {
|
const errorString = typeof error === 'string'
|
||||||
return Response.json(
|
? error
|
||||||
{
|
: error instanceof Error
|
||||||
error: "Authentication failed. Please check your API key.",
|
? error.message
|
||||||
...(isDev && {
|
: JSON.stringify(error);
|
||||||
stack: error.stack,
|
|
||||||
}),
|
// Check for Bedrock service errors (503, throttling, etc.)
|
||||||
},
|
if (errorString.includes('ServiceUnavailable') ||
|
||||||
{ status: 401 },
|
errorString.includes('503') ||
|
||||||
)
|
errorString.includes('temporarily unavailable')) {
|
||||||
|
console.error('[Bedrock Error] ServiceUnavailableException:', errorString);
|
||||||
|
return 'The AI service is temporarily unavailable. Please try again in a few seconds.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for other errors with safety filter
|
// Check for throttling errors
|
||||||
const message =
|
if (errorString.includes('ThrottlingException') ||
|
||||||
error instanceof Error ? error.message : "An unexpected error occurred"
|
errorString.includes('rate limit') ||
|
||||||
const status = (error as any)?.statusCode || (error as any)?.status || 500
|
errorString.includes('too many requests') ||
|
||||||
|
errorString.includes('429')) {
|
||||||
// Prevent leaking API keys, tokens, or other sensitive data
|
console.error('[Bedrock Error] ThrottlingException:', errorString);
|
||||||
const lowerMessage = message.toLowerCase()
|
return 'Too many requests. Please wait a moment and try again.';
|
||||||
const safeMessage =
|
|
||||||
lowerMessage.includes("key") ||
|
|
||||||
lowerMessage.includes("token") ||
|
|
||||||
lowerMessage.includes("sig") ||
|
|
||||||
lowerMessage.includes("signature") ||
|
|
||||||
lowerMessage.includes("secret") ||
|
|
||||||
lowerMessage.includes("password") ||
|
|
||||||
lowerMessage.includes("credential")
|
|
||||||
? "Authentication failed. Please check your credentials."
|
|
||||||
: message
|
|
||||||
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
error: safeMessage,
|
|
||||||
...(isDev && {
|
|
||||||
details: message,
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ status },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap handler with error handling
|
|
||||||
async function safeHandler(req: Request): Promise<Response> {
|
|
||||||
try {
|
|
||||||
return await handleChatRequest(req)
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap with Langfuse observe (if configured)
|
// Check for image not supported error (e.g., DeepSeek models)
|
||||||
const observedHandler = wrapWithObserve(safeHandler)
|
if (errorString.includes('image_url') ||
|
||||||
|
errorString.includes('unknown variant') ||
|
||||||
|
(errorString.includes('image') && errorString.includes('not supported'))) {
|
||||||
|
return 'This model does not support image inputs. Please remove the image and try again, or switch to a vision-capable model.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toUIMessageStreamResponse({
|
||||||
|
onError: errorHandler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
return observedHandler(req)
|
try {
|
||||||
|
return await handleChatRequest(req);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in chat route:', error);
|
||||||
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json({
|
|
||||||
accessCodeRequired: !!process.env.ACCESS_CODE_LIST,
|
|
||||||
dailyRequestLimit: Number(process.env.DAILY_REQUEST_LIMIT) || 0,
|
|
||||||
dailyTokenLimit: Number(process.env.DAILY_TOKEN_LIMIT) || 0,
|
|
||||||
tpmLimit: Number(process.env.TPM_LIMIT) || 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { randomUUID } from "crypto"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { getLangfuseClient } from "@/lib/langfuse"
|
|
||||||
|
|
||||||
const feedbackSchema = z.object({
|
|
||||||
messageId: z.string().min(1).max(200),
|
|
||||||
feedback: z.enum(["good", "bad"]),
|
|
||||||
sessionId: z.string().min(1).max(200).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
const langfuse = getLangfuseClient()
|
|
||||||
if (!langfuse) {
|
|
||||||
return Response.json({ success: true, logged: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = feedbackSchema.parse(await req.json())
|
|
||||||
} catch {
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: "Invalid input" },
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { messageId, feedback, sessionId } = data
|
|
||||||
|
|
||||||
// Get user IP for tracking
|
|
||||||
const forwardedFor = req.headers.get("x-forwarded-for")
|
|
||||||
const userId = forwardedFor?.split(",")[0]?.trim() || "anonymous"
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the most recent chat trace for this session to attach the score to
|
|
||||||
const tracesResponse = await langfuse.api.trace.list({
|
|
||||||
sessionId,
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const traces = tracesResponse.data || []
|
|
||||||
const latestTrace = traces[0]
|
|
||||||
|
|
||||||
if (!latestTrace) {
|
|
||||||
// No trace found for this session - create a standalone feedback trace
|
|
||||||
const traceId = randomUUID()
|
|
||||||
const timestamp = new Date().toISOString()
|
|
||||||
|
|
||||||
await langfuse.api.ingestion.batch({
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: "trace-create",
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: traceId,
|
|
||||||
name: "user-feedback",
|
|
||||||
sessionId,
|
|
||||||
userId,
|
|
||||||
input: { messageId, feedback },
|
|
||||||
metadata: {
|
|
||||||
source: "feedback-button",
|
|
||||||
note: "standalone - no chat trace found",
|
|
||||||
},
|
|
||||||
timestamp,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "score-create",
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: randomUUID(),
|
|
||||||
traceId,
|
|
||||||
name: "user-feedback",
|
|
||||||
value: feedback === "good" ? 1 : 0,
|
|
||||||
comment: `User gave ${feedback} feedback`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Attach score to the existing chat trace
|
|
||||||
const timestamp = new Date().toISOString()
|
|
||||||
|
|
||||||
await langfuse.api.ingestion.batch({
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: "score-create",
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: randomUUID(),
|
|
||||||
traceId: latestTrace.id,
|
|
||||||
name: "user-feedback",
|
|
||||||
value: feedback === "good" ? 1 : 0,
|
|
||||||
comment: `User gave ${feedback} feedback`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ success: true, logged: true })
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Langfuse feedback error:", error)
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: "Failed to log feedback" },
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { randomUUID } from "crypto"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { getLangfuseClient } from "@/lib/langfuse"
|
|
||||||
|
|
||||||
const saveSchema = z.object({
|
|
||||||
filename: z.string().min(1).max(255),
|
|
||||||
format: z.enum(["drawio", "png", "svg"]),
|
|
||||||
sessionId: z.string().min(1).max(200).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
const langfuse = getLangfuseClient()
|
|
||||||
if (!langfuse) {
|
|
||||||
return Response.json({ success: true, logged: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = saveSchema.parse(await req.json())
|
|
||||||
} catch {
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: "Invalid input" },
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { filename, format, sessionId } = data
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timestamp = new Date().toISOString()
|
|
||||||
|
|
||||||
// Find the most recent chat trace for this session to attach the save flag
|
|
||||||
const tracesResponse = await langfuse.api.trace.list({
|
|
||||||
sessionId,
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const traces = tracesResponse.data || []
|
|
||||||
const latestTrace = traces[0]
|
|
||||||
|
|
||||||
if (latestTrace) {
|
|
||||||
// Add a score to the existing trace to flag that user saved
|
|
||||||
await langfuse.api.ingestion.batch({
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: "score-create",
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: randomUUID(),
|
|
||||||
traceId: latestTrace.id,
|
|
||||||
name: "diagram-saved",
|
|
||||||
value: 1,
|
|
||||||
comment: `User saved diagram as ${filename}.${format}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// If no trace found, skip logging (user hasn't chatted yet)
|
|
||||||
|
|
||||||
return Response.json({ success: true, logged: !!latestTrace })
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Langfuse save error:", error)
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: "Failed to log save" },
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export async function POST(req: Request) {
|
|
||||||
const accessCodes =
|
|
||||||
process.env.ACCESS_CODE_LIST?.split(",")
|
|
||||||
.map((code) => code.trim())
|
|
||||||
.filter(Boolean) || []
|
|
||||||
|
|
||||||
// If no access codes configured, verification always passes
|
|
||||||
if (accessCodes.length === 0) {
|
|
||||||
return Response.json({
|
|
||||||
valid: true,
|
|
||||||
message: "No access code required",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessCodeHeader = req.headers.get("x-access-code")
|
|
||||||
|
|
||||||
if (!accessCodeHeader) {
|
|
||||||
return Response.json(
|
|
||||||
{ valid: false, message: "Access code is required" },
|
|
||||||
{ status: 401 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accessCodes.includes(accessCodeHeader)) {
|
|
||||||
return Response.json(
|
|
||||||
{ valid: false, message: "Invalid access code" },
|
|
||||||
{ status: 401 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ valid: true, message: "Access code is valid" })
|
|
||||||
}
|
|
||||||
353
app/globals.css
353
app/globals.css
@@ -1,259 +1,248 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@plugin "tailwindcss-animate";
|
@plugin "tailwindcss-animate";
|
||||||
@plugin "@tailwindcss/typography";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-mono: var(--font-mono);
|
--font-mono: var(--font-mono);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--color-sidebar: var(--sidebar);
|
--color-sidebar: var(--sidebar);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-2: var(--chart-2);
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
/* Clean Light Modern Palette */
|
/* Clean Light Modern Palette */
|
||||||
--background: oklch(0.985 0.002 240);
|
--background: oklch(0.985 0.002 240);
|
||||||
--foreground: oklch(0.23 0.02 260);
|
--foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.23 0.02 260);
|
--card-foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.23 0.02 260);
|
--popover-foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
/* Dark primary - slightly lighter */
|
/* Dark primary - slightly lighter */
|
||||||
--primary: oklch(0.35 0.01 260);
|
--primary: oklch(0.35 0.01 260);
|
||||||
--primary-foreground: oklch(0.99 0 0);
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
|
|
||||||
/* Warm gray secondary */
|
/* Warm gray secondary */
|
||||||
--secondary: oklch(0.96 0.005 260);
|
--secondary: oklch(0.96 0.005 260);
|
||||||
--secondary-foreground: oklch(0.35 0.02 260);
|
--secondary-foreground: oklch(0.35 0.02 260);
|
||||||
|
|
||||||
/* Light muted tones */
|
/* Light muted tones */
|
||||||
--muted: oklch(0.965 0.005 260);
|
--muted: oklch(0.965 0.005 260);
|
||||||
--muted-foreground: oklch(0.5 0.02 260);
|
--muted-foreground: oklch(0.50 0.02 260);
|
||||||
|
|
||||||
/* Soft lavender accent */
|
/* Soft lavender accent */
|
||||||
--accent: oklch(0.94 0.03 280);
|
--accent: oklch(0.94 0.03 280);
|
||||||
--accent-foreground: oklch(0.35 0.08 270);
|
--accent-foreground: oklch(0.35 0.08 270);
|
||||||
|
|
||||||
/* Coral destructive */
|
/* Coral destructive */
|
||||||
--destructive: oklch(0.6 0.2 25);
|
--destructive: oklch(0.60 0.20 25);
|
||||||
|
|
||||||
/* Subtle borders */
|
/* Subtle borders */
|
||||||
--border: oklch(0.92 0.01 260);
|
--border: oklch(0.92 0.01 260);
|
||||||
--input: oklch(0.94 0.01 260);
|
--input: oklch(0.94 0.01 260);
|
||||||
--ring: oklch(0.25 0.01 260);
|
--ring: oklch(0.25 0.01 260);
|
||||||
|
|
||||||
/* Chart colors - harmonious palette */
|
/* Chart colors - harmonious palette */
|
||||||
--chart-1: oklch(0.55 0.18 265);
|
--chart-1: oklch(0.55 0.18 265);
|
||||||
--chart-2: oklch(0.65 0.15 170);
|
--chart-2: oklch(0.65 0.15 170);
|
||||||
--chart-3: oklch(0.7 0.18 45);
|
--chart-3: oklch(0.70 0.18 45);
|
||||||
--chart-4: oklch(0.6 0.2 330);
|
--chart-4: oklch(0.60 0.20 330);
|
||||||
--chart-5: oklch(0.5 0.15 200);
|
--chart-5: oklch(0.50 0.15 200);
|
||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
--sidebar: oklch(0.99 0.002 260);
|
--sidebar: oklch(0.99 0.002 260);
|
||||||
--sidebar-foreground: oklch(0.23 0.02 260);
|
--sidebar-foreground: oklch(0.23 0.02 260);
|
||||||
--sidebar-primary: oklch(0.55 0.18 265);
|
--sidebar-primary: oklch(0.55 0.18 265);
|
||||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||||
--sidebar-accent: oklch(0.96 0.02 270);
|
--sidebar-accent: oklch(0.96 0.02 270);
|
||||||
--sidebar-accent-foreground: oklch(0.35 0.05 265);
|
--sidebar-accent-foreground: oklch(0.35 0.05 265);
|
||||||
--sidebar-border: oklch(0.93 0.01 260);
|
--sidebar-border: oklch(0.93 0.01 260);
|
||||||
--sidebar-ring: oklch(0.55 0.18 265);
|
--sidebar-ring: oklch(0.55 0.18 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.15 0.015 260);
|
--background: oklch(0.15 0.015 260);
|
||||||
--foreground: oklch(0.95 0.01 260);
|
--foreground: oklch(0.95 0.01 260);
|
||||||
|
|
||||||
--card: oklch(0.2 0.015 260);
|
--card: oklch(0.20 0.015 260);
|
||||||
--card-foreground: oklch(0.95 0.01 260);
|
--card-foreground: oklch(0.95 0.01 260);
|
||||||
|
|
||||||
--popover: oklch(0.2 0.015 260);
|
--popover: oklch(0.20 0.015 260);
|
||||||
--popover-foreground: oklch(0.95 0.01 260);
|
--popover-foreground: oklch(0.95 0.01 260);
|
||||||
|
|
||||||
--primary: oklch(0.7 0.16 265);
|
--primary: oklch(0.70 0.16 265);
|
||||||
--primary-foreground: oklch(0.15 0.02 260);
|
--primary-foreground: oklch(0.15 0.02 260);
|
||||||
|
|
||||||
--secondary: oklch(0.25 0.015 260);
|
--secondary: oklch(0.25 0.015 260);
|
||||||
--secondary-foreground: oklch(0.9 0.01 260);
|
--secondary-foreground: oklch(0.90 0.01 260);
|
||||||
|
|
||||||
--muted: oklch(0.25 0.015 260);
|
--muted: oklch(0.25 0.015 260);
|
||||||
--muted-foreground: oklch(0.65 0.02 260);
|
--muted-foreground: oklch(0.65 0.02 260);
|
||||||
|
|
||||||
--accent: oklch(0.3 0.04 280);
|
--accent: oklch(0.30 0.04 280);
|
||||||
--accent-foreground: oklch(0.9 0.03 270);
|
--accent-foreground: oklch(0.90 0.03 270);
|
||||||
|
|
||||||
--destructive: oklch(0.65 0.22 25);
|
--destructive: oklch(0.65 0.22 25);
|
||||||
|
|
||||||
--border: oklch(0.28 0.015 260);
|
--border: oklch(0.28 0.015 260);
|
||||||
--input: oklch(0.25 0.015 260);
|
--input: oklch(0.25 0.015 260);
|
||||||
--ring: oklch(0.7 0.16 265);
|
--ring: oklch(0.70 0.16 265);
|
||||||
|
|
||||||
--chart-1: oklch(0.7 0.16 265);
|
--chart-1: oklch(0.70 0.16 265);
|
||||||
--chart-2: oklch(0.7 0.13 170);
|
--chart-2: oklch(0.70 0.13 170);
|
||||||
--chart-3: oklch(0.75 0.16 45);
|
--chart-3: oklch(0.75 0.16 45);
|
||||||
--chart-4: oklch(0.7 0.18 330);
|
--chart-4: oklch(0.70 0.18 330);
|
||||||
--chart-5: oklch(0.6 0.13 200);
|
--chart-5: oklch(0.60 0.13 200);
|
||||||
|
|
||||||
--sidebar: oklch(0.18 0.015 260);
|
--sidebar: oklch(0.18 0.015 260);
|
||||||
--sidebar-foreground: oklch(0.95 0.01 260);
|
--sidebar-foreground: oklch(0.95 0.01 260);
|
||||||
--sidebar-primary: oklch(0.7 0.16 265);
|
--sidebar-primary: oklch(0.70 0.16 265);
|
||||||
--sidebar-primary-foreground: oklch(0.15 0.02 260);
|
--sidebar-primary-foreground: oklch(0.15 0.02 260);
|
||||||
--sidebar-accent: oklch(0.25 0.03 270);
|
--sidebar-accent: oklch(0.25 0.03 270);
|
||||||
--sidebar-accent-foreground: oklch(0.9 0.02 265);
|
--sidebar-accent-foreground: oklch(0.90 0.02 265);
|
||||||
--sidebar-border: oklch(0.28 0.015 260);
|
--sidebar-border: oklch(0.28 0.015 260);
|
||||||
--sidebar-ring: oklch(0.7 0.16 265);
|
--sidebar-ring: oklch(0.70 0.16 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground font-sans;
|
@apply bg-background text-foreground font-sans;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix for Radix ScrollArea viewport horizontal overflow */
|
|
||||||
[data-slot="scroll-area-viewport"] > div {
|
|
||||||
display: block !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.scrollbar-thin {
|
.scrollbar-thin {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: oklch(0.85 0.01 260) transparent;
|
scrollbar-color: oklch(0.85 0.01 260) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar {
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-track {
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
background-color: oklch(0.85 0.01 260);
|
background-color: oklch(0.85 0.01 260);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: oklch(0.75 0.01 260);
|
background-color: oklch(0.75 0.01 260);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth page transitions */
|
/* Smooth page transitions */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(8px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.3s ease-out forwards;
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-in-right {
|
.animate-slide-in-right {
|
||||||
animation: slideInRight 0.3s ease-out forwards;
|
animation: slideInRight 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message bubble animations */
|
/* Message bubble animations */
|
||||||
@keyframes messageIn {
|
@keyframes messageIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(12px) scale(0.98);
|
transform: translateY(12px) scale(0.98);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-message-in {
|
.animate-message-in {
|
||||||
animation: messageIn 0.25s ease-out forwards;
|
animation: messageIn 0.25s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle floating shadow for cards */
|
/* Subtle floating shadow for cards */
|
||||||
.shadow-soft {
|
.shadow-soft {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 2px oklch(0.23 0.02 260 / 0.04),
|
0 1px 2px oklch(0.23 0.02 260 / 0.04),
|
||||||
0 4px 12px oklch(0.23 0.02 260 / 0.06),
|
0 4px 12px oklch(0.23 0.02 260 / 0.06),
|
||||||
0 8px 24px oklch(0.23 0.02 260 / 0.04);
|
0 8px 24px oklch(0.23 0.02 260 / 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-soft-lg {
|
.shadow-soft-lg {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 4px oklch(0.23 0.02 260 / 0.04),
|
0 2px 4px oklch(0.23 0.02 260 / 0.04),
|
||||||
0 8px 20px oklch(0.23 0.02 260 / 0.08),
|
0 8px 20px oklch(0.23 0.02 260 / 0.08),
|
||||||
0 16px 40px oklch(0.23 0.02 260 / 0.06);
|
0 16px 40px oklch(0.23 0.02 260 / 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient text utility */
|
/* Gradient text utility */
|
||||||
.text-gradient-primary {
|
.text-gradient-primary {
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg, oklch(0.55 0.18 265), oklch(0.60 0.20 290));
|
||||||
135deg,
|
-webkit-background-clip: text;
|
||||||
oklch(0.55 0.18 265),
|
-webkit-text-fill-color: transparent;
|
||||||
oklch(0.6 0.2 290)
|
background-clip: text;
|
||||||
);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,34 @@
|
|||||||
import { GoogleAnalytics } from "@next/third-parties/google"
|
import type { Metadata } from "next";
|
||||||
import { Analytics } from "@vercel/analytics/react"
|
import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
|
||||||
import type { Metadata, Viewport } from "next"
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||||
import { DiagramProvider } from "@/contexts/diagram-context"
|
import { DiagramProvider } from "@/contexts/diagram-context";
|
||||||
|
|
||||||
import "./globals.css"
|
import "./globals.css";
|
||||||
|
|
||||||
const plusJakarta = Plus_Jakarta_Sans({
|
const plusJakarta = Plus_Jakarta_Sans({
|
||||||
variable: "--font-sans",
|
variable: "--font-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500", "600", "700"],
|
weight: ["400", "500", "600", "700"],
|
||||||
})
|
});
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
variable: "--font-mono",
|
variable: "--font-mono",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500"],
|
weight: ["400", "500"],
|
||||||
})
|
});
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
width: "device-width",
|
|
||||||
initialScale: 1,
|
|
||||||
maximumScale: 1,
|
|
||||||
userScalable: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Next AI Draw.io - AI-Powered Diagram Generator",
|
title: "Next AI Draw.io - AI-Powered Diagram Generator",
|
||||||
description:
|
description: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
||||||
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
keywords: ["AI diagram generator", "AWS architecture", "flowchart creator", "draw.io", "AI drawing tool", "technical diagrams", "diagram automation", "free diagram generator", "online diagram maker"],
|
||||||
keywords: [
|
|
||||||
"AI diagram generator",
|
|
||||||
"AWS architecture",
|
|
||||||
"flowchart creator",
|
|
||||||
"draw.io",
|
|
||||||
"AI drawing tool",
|
|
||||||
"technical diagrams",
|
|
||||||
"diagram automation",
|
|
||||||
"free diagram generator",
|
|
||||||
"online diagram maker",
|
|
||||||
],
|
|
||||||
authors: [{ name: "Next AI Draw.io" }],
|
authors: [{ name: "Next AI Draw.io" }],
|
||||||
creator: "Next AI Draw.io",
|
creator: "Next AI Draw.io",
|
||||||
publisher: "Next AI Draw.io",
|
publisher: "Next AI Draw.io",
|
||||||
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
title: "Next AI Draw.io - AI Diagram Generator",
|
||||||
description:
|
description: "Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
|
||||||
"Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
|
|
||||||
type: "website",
|
type: "website",
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
siteName: "Next AI Draw.io",
|
siteName: "Next AI Draw.io",
|
||||||
@@ -64,8 +45,7 @@ export const metadata: Metadata = {
|
|||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
title: "Next AI Draw.io - AI Diagram Generator",
|
||||||
description:
|
description: "Create professional diagrams with AI assistance. Free, no login required.",
|
||||||
"Create professional diagrams with AI assistance. Free, no login required.",
|
|
||||||
images: ["/architecture.png"],
|
images: ["/architecture.png"],
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
@@ -82,31 +62,30 @@ export const metadata: Metadata = {
|
|||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon.ico",
|
icon: "/favicon.ico",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
"@context": "https://schema.org",
|
'@context': 'https://schema.org',
|
||||||
"@type": "SoftwareApplication",
|
'@type': 'SoftwareApplication',
|
||||||
name: "Next AI Draw.io",
|
name: 'Next AI Draw.io',
|
||||||
applicationCategory: "DesignApplication",
|
applicationCategory: 'DesignApplication',
|
||||||
operatingSystem: "Web Browser",
|
operatingSystem: 'Web Browser',
|
||||||
description:
|
description: 'AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.',
|
||||||
"AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.",
|
url: 'https://next-ai-drawio.jiang.jp',
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
|
||||||
offers: {
|
offers: {
|
||||||
"@type": "Offer",
|
'@type': 'Offer',
|
||||||
price: "0",
|
price: '0',
|
||||||
priceCurrency: "USD",
|
priceCurrency: 'USD',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
@@ -117,11 +96,12 @@ export default function RootLayout({
|
|||||||
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<DiagramProvider>{children}</DiagramProvider>
|
<DiagramProvider>{children}</DiagramProvider>
|
||||||
|
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
{process.env.NEXT_PUBLIC_GA_ID && (
|
{process.env.NEXT_PUBLIC_GA_ID && (
|
||||||
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
||||||
)}
|
)}
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
251
app/page.tsx
251
app/page.tsx
@@ -1,202 +1,97 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import { useEffect, useRef, useState } from "react"
|
import React, { useState, useEffect } from "react";
|
||||||
import { DrawIoEmbed } from "react-drawio"
|
import { DrawIoEmbed } from "react-drawio";
|
||||||
import type { ImperativePanelHandle } from "react-resizable-panels"
|
import ChatPanel from "@/components/chat-panel";
|
||||||
import ChatPanel from "@/components/chat-panel"
|
import { useDiagram } from "@/contexts/diagram-context";
|
||||||
import { STORAGE_CLOSE_PROTECTION_KEY } from "@/components/settings-dialog"
|
import { Monitor } from "lucide-react";
|
||||||
import {
|
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
} from "@/components/ui/resizable"
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
|
||||||
|
|
||||||
const drawioBaseUrl =
|
|
||||||
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
|
const { drawioRef, handleDiagramExport } = useDiagram();
|
||||||
useDiagram()
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isChatVisible, setIsChatVisible] = useState(true);
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
|
||||||
const [darkMode, setDarkMode] = useState(false)
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
|
||||||
const [closeProtection, setCloseProtection] = useState(false)
|
|
||||||
|
|
||||||
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
|
||||||
|
|
||||||
// Load preferences from localStorage after mount
|
|
||||||
useEffect(() => {
|
|
||||||
const savedUi = localStorage.getItem("drawio-theme")
|
|
||||||
if (savedUi === "min" || savedUi === "sketch") {
|
|
||||||
setDrawioUi(savedUi)
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
|
||||||
if (savedDarkMode !== null) {
|
|
||||||
// Use saved preference
|
|
||||||
const isDark = savedDarkMode === "true"
|
|
||||||
setDarkMode(isDark)
|
|
||||||
document.documentElement.classList.toggle("dark", isDark)
|
|
||||||
} else {
|
|
||||||
// First visit: match browser preference
|
|
||||||
const prefersDark = window.matchMedia(
|
|
||||||
"(prefers-color-scheme: dark)",
|
|
||||||
).matches
|
|
||||||
setDarkMode(prefersDark)
|
|
||||||
document.documentElement.classList.toggle("dark", prefersDark)
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedCloseProtection = localStorage.getItem(
|
|
||||||
STORAGE_CLOSE_PROTECTION_KEY,
|
|
||||||
)
|
|
||||||
if (savedCloseProtection === "true") {
|
|
||||||
setCloseProtection(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoaded(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
|
||||||
const newValue = !darkMode
|
|
||||||
setDarkMode(newValue)
|
|
||||||
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
|
||||||
document.documentElement.classList.toggle("dark", newValue)
|
|
||||||
// Reset so onDrawioLoad fires again after remount
|
|
||||||
resetDrawioReady()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check mobile
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
setIsMobile(window.innerWidth < 768)
|
setIsMobile(window.innerWidth < 768);
|
||||||
}
|
};
|
||||||
|
|
||||||
checkMobile()
|
checkMobile();
|
||||||
window.addEventListener("resize", checkMobile)
|
window.addEventListener("resize", checkMobile);
|
||||||
return () => window.removeEventListener("resize", checkMobile)
|
return () => window.removeEventListener("resize", checkMobile);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const toggleChatPanel = () => {
|
|
||||||
const panel = chatPanelRef.current
|
|
||||||
if (panel) {
|
|
||||||
if (panel.isCollapsed()) {
|
|
||||||
panel.expand()
|
|
||||||
setIsChatVisible(true)
|
|
||||||
} else {
|
|
||||||
panel.collapse()
|
|
||||||
setIsChatVisible(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard shortcut for toggling chat panel
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
toggleChatPanel()
|
setIsChatVisible((prev) => !prev);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// Show confirmation dialog when user tries to leave the page
|
// Show confirmation dialog when user tries to leave the page
|
||||||
|
// This helps prevent accidental navigation from browser back gestures
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!closeProtection) return
|
|
||||||
|
|
||||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
return ""
|
return '';
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload)
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
return () =>
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload)
|
}, []);
|
||||||
}, [closeProtection])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-background relative overflow-hidden">
|
<div className="flex h-screen bg-background relative overflow-hidden">
|
||||||
<ResizablePanelGroup
|
{/* Mobile warning overlay */}
|
||||||
id="main-panel-group"
|
{isMobile && (
|
||||||
key={isMobile ? "mobile" : "desktop"}
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background">
|
||||||
direction={isMobile ? "vertical" : "horizontal"}
|
<div className="text-center p-8 max-w-sm mx-auto animate-fade-in">
|
||||||
className="h-full"
|
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-6">
|
||||||
>
|
<Monitor className="w-8 h-8 text-primary" />
|
||||||
{/* Draw.io Canvas */}
|
|
||||||
<ResizablePanel
|
|
||||||
id="drawio-panel"
|
|
||||||
defaultSize={isMobile ? 50 : 67}
|
|
||||||
minSize={20}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`h-full relative ${
|
|
||||||
isMobile ? "p-1" : "p-2"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
|
|
||||||
{isLoaded ? (
|
|
||||||
<DrawIoEmbed
|
|
||||||
key={`${drawioUi}-${darkMode}`}
|
|
||||||
ref={drawioRef}
|
|
||||||
onExport={handleDiagramExport}
|
|
||||||
onLoad={onDrawioLoad}
|
|
||||||
baseUrl={drawioBaseUrl}
|
|
||||||
urlParameters={{
|
|
||||||
ui: drawioUi,
|
|
||||||
spin: true,
|
|
||||||
libraries: false,
|
|
||||||
saveAndExit: false,
|
|
||||||
noExitBtn: true,
|
|
||||||
dark: darkMode,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full flex items-center justify-center bg-background">
|
|
||||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="text-xl font-semibold text-foreground mb-3">
|
||||||
|
Desktop Required
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
This application works best on desktop or laptop devices. Please open it on a larger screen for the full experience.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ResizableHandle withHandle />
|
{/* Draw.io Canvas */}
|
||||||
|
<div
|
||||||
|
className={`${isChatVisible ? 'w-2/3' : 'w-full'} h-full relative transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-2 rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
||||||
|
<DrawIoEmbed
|
||||||
|
ref={drawioRef}
|
||||||
|
onExport={handleDiagramExport}
|
||||||
|
urlParameters={{
|
||||||
|
spin: true,
|
||||||
|
libraries: false,
|
||||||
|
saveAndExit: false,
|
||||||
|
noExitBtn: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Chat Panel */}
|
{/* Chat Panel */}
|
||||||
<ResizablePanel
|
<div
|
||||||
id="chat-panel"
|
className={`${isChatVisible ? 'w-1/3' : 'w-12'} h-full transition-all duration-300 ease-out`}
|
||||||
ref={chatPanelRef}
|
>
|
||||||
defaultSize={isMobile ? 50 : 33}
|
<div className="h-full py-2 pr-2">
|
||||||
minSize={isMobile ? 20 : 15}
|
<ChatPanel
|
||||||
maxSize={isMobile ? 80 : 50}
|
isVisible={isChatVisible}
|
||||||
collapsible={!isMobile}
|
onToggleVisibility={() => setIsChatVisible(!isChatVisible)}
|
||||||
collapsedSize={isMobile ? 0 : 3}
|
/>
|
||||||
onCollapse={() => setIsChatVisible(false)}
|
</div>
|
||||||
onExpand={() => setIsChatVisible(true)}
|
</div>
|
||||||
>
|
|
||||||
<div className={`h-full ${isMobile ? "p-1" : "py-2 pr-2"}`}>
|
|
||||||
<ChatPanel
|
|
||||||
isVisible={isChatVisible}
|
|
||||||
onToggleVisibility={toggleChatPanel}
|
|
||||||
drawioUi={drawioUi}
|
|
||||||
onToggleDrawioUi={() => {
|
|
||||||
const newUi =
|
|
||||||
drawioUi === "min" ? "sketch" : "min"
|
|
||||||
localStorage.setItem("drawio-theme", newUi)
|
|
||||||
setDrawioUi(newUi)
|
|
||||||
resetDrawioReady()
|
|
||||||
}}
|
|
||||||
darkMode={darkMode}
|
|
||||||
onToggleDarkMode={toggleDarkMode}
|
|
||||||
isMobile={isMobile}
|
|
||||||
onCloseProtectionChange={setCloseProtection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
|
||||||
</ResizablePanelGroup>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { MetadataRoute } from "next"
|
import { MetadataRoute } from 'next'
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
userAgent: "*",
|
userAgent: '*',
|
||||||
allow: "/",
|
allow: '/',
|
||||||
disallow: "/api/",
|
disallow: '/api/',
|
||||||
},
|
},
|
||||||
sitemap: "https://next-ai-drawio.jiang.jp/sitemap.xml",
|
sitemap: 'https://next-ai-drawio.jiang.jp/sitemap.xml',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { MetadataRoute } from "next"
|
import { MetadataRoute } from 'next'
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
url: 'https://next-ai-drawio.jiang.jp',
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "weekly",
|
changeFrequency: 'weekly',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "https://next-ai-drawio.jiang.jp/about",
|
url: 'https://next-ai-drawio.jiang.jp/about',
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "monthly",
|
changeFrequency: 'monthly',
|
||||||
priority: 0.8,
|
priority: 0.8,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
83
biome.json
83
biome.json
@@ -1,83 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 4
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"complexity": {
|
|
||||||
"noImportantStyles": "off"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "off",
|
|
||||||
"noArrayIndexKey": "off",
|
|
||||||
"noImplicitAnyLet": "off",
|
|
||||||
"noAssignInExpressions": "off"
|
|
||||||
},
|
|
||||||
"a11y": {
|
|
||||||
"useButtonType": "off",
|
|
||||||
"noAutofocus": "off",
|
|
||||||
"noStaticElementInteractions": "off",
|
|
||||||
"useKeyWithClickEvents": "off",
|
|
||||||
"noLabelWithoutControl": "off",
|
|
||||||
"noNoninteractiveTabindex": "off"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"useExhaustiveDependencies": "off"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"useNodejsImportProtocol": "off",
|
|
||||||
"useTemplate": "off"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"noDangerouslySetInnerHtml": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "double",
|
|
||||||
"semicolons": "asNeeded"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"css": {
|
|
||||||
"parser": {
|
|
||||||
"cssModules": true,
|
|
||||||
"tailwindDirectives": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assist": {
|
|
||||||
"enabled": true,
|
|
||||||
"actions": {
|
|
||||||
"source": {
|
|
||||||
"organizeImports": "on"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"includes": ["components/ui/**"],
|
|
||||||
"formatter": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"assist": {
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "app/globals.css",
|
"css": "app/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
|
||||||
import { BrainIcon, ChevronDownIcon } from "lucide-react"
|
|
||||||
import type { ComponentProps, ReactNode } from "react"
|
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react"
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Shimmer } from "./shimmer"
|
|
||||||
|
|
||||||
type ReasoningContextValue = {
|
|
||||||
isStreaming: boolean
|
|
||||||
isOpen: boolean
|
|
||||||
setIsOpen: (open: boolean) => void
|
|
||||||
duration: number | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReasoningContext = createContext<ReasoningContextValue | null>(null)
|
|
||||||
|
|
||||||
export const useReasoning = () => {
|
|
||||||
const context = useContext(ReasoningContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("Reasoning components must be used within Reasoning")
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|
||||||
isStreaming?: boolean
|
|
||||||
open?: boolean
|
|
||||||
defaultOpen?: boolean
|
|
||||||
onOpenChange?: (open: boolean) => void
|
|
||||||
duration?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 1000
|
|
||||||
const MS_IN_S = 1000
|
|
||||||
|
|
||||||
export const Reasoning = memo(
|
|
||||||
({
|
|
||||||
className,
|
|
||||||
isStreaming = false,
|
|
||||||
open,
|
|
||||||
defaultOpen = true,
|
|
||||||
onOpenChange,
|
|
||||||
duration: durationProp,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ReasoningProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useControllableState({
|
|
||||||
prop: open,
|
|
||||||
defaultProp: defaultOpen,
|
|
||||||
onChange: onOpenChange,
|
|
||||||
})
|
|
||||||
const [duration, setDuration] = useControllableState({
|
|
||||||
prop: durationProp,
|
|
||||||
defaultProp: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [hasAutoClosed, setHasAutoClosed] = useState(false)
|
|
||||||
const [startTime, setStartTime] = useState<number | null>(null)
|
|
||||||
|
|
||||||
// Track duration when streaming starts and ends
|
|
||||||
useEffect(() => {
|
|
||||||
if (isStreaming) {
|
|
||||||
if (startTime === null) {
|
|
||||||
setStartTime(Date.now())
|
|
||||||
}
|
|
||||||
} else if (startTime !== null) {
|
|
||||||
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
|
|
||||||
setStartTime(null)
|
|
||||||
}
|
|
||||||
}, [isStreaming, startTime, setDuration])
|
|
||||||
|
|
||||||
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
|
||||||
useEffect(() => {
|
|
||||||
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
|
||||||
// Add a small delay before closing to allow user to see the content
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setIsOpen(false)
|
|
||||||
setHasAutoClosed(true)
|
|
||||||
}, AUTO_CLOSE_DELAY)
|
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed])
|
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean) => {
|
|
||||||
setIsOpen(newOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReasoningContext.Provider
|
|
||||||
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
|
||||||
>
|
|
||||||
<Collapsible
|
|
||||||
className={cn("not-prose mb-4", className)}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
open={isOpen}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Collapsible>
|
|
||||||
</ReasoningContext.Provider>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export type ReasoningTriggerProps = ComponentProps<
|
|
||||||
typeof CollapsibleTrigger
|
|
||||||
> & {
|
|
||||||
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
|
||||||
if (isStreaming || duration === 0) {
|
|
||||||
return <Shimmer duration={1}>Thinking...</Shimmer>
|
|
||||||
}
|
|
||||||
if (duration === undefined) {
|
|
||||||
return <p>Thought for a few seconds</p>
|
|
||||||
}
|
|
||||||
return <p>Thought for {duration} seconds</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReasoningTrigger = memo(
|
|
||||||
({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
getThinkingMessage = defaultGetThinkingMessage,
|
|
||||||
...props
|
|
||||||
}: ReasoningTriggerProps) => {
|
|
||||||
const { isStreaming, isOpen, duration } = useReasoning()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapsibleTrigger
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<BrainIcon className="size-4" />
|
|
||||||
{getThinkingMessage(isStreaming, duration)}
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn(
|
|
||||||
"size-4 transition-transform",
|
|
||||||
isOpen ? "rotate-180" : "rotate-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export type ReasoningContentProps = ComponentProps<
|
|
||||||
typeof CollapsibleContent
|
|
||||||
> & {
|
|
||||||
children: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReasoningContent = memo(
|
|
||||||
({ className, children, ...props }: ReasoningContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"mt-4 text-sm",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="whitespace-pre-wrap">{children}</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Reasoning.displayName = "Reasoning"
|
|
||||||
ReasoningTrigger.displayName = "ReasoningTrigger"
|
|
||||||
ReasoningContent.displayName = "ReasoningContent"
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { motion } from "motion/react"
|
|
||||||
import {
|
|
||||||
type CSSProperties,
|
|
||||||
type ElementType,
|
|
||||||
type JSX,
|
|
||||||
memo,
|
|
||||||
useMemo,
|
|
||||||
} from "react"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
export type TextShimmerProps = {
|
|
||||||
children: string
|
|
||||||
as?: ElementType
|
|
||||||
className?: string
|
|
||||||
duration?: number
|
|
||||||
spread?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShimmerComponent = ({
|
|
||||||
children,
|
|
||||||
as: Component = "p",
|
|
||||||
className,
|
|
||||||
duration = 2,
|
|
||||||
spread = 2,
|
|
||||||
}: TextShimmerProps) => {
|
|
||||||
const MotionComponent = motion.create(
|
|
||||||
Component as keyof JSX.IntrinsicElements,
|
|
||||||
)
|
|
||||||
|
|
||||||
const dynamicSpread = useMemo(
|
|
||||||
() => (children?.length ?? 0) * spread,
|
|
||||||
[children, spread],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MotionComponent
|
|
||||||
animate={{ backgroundPosition: "0% center" }}
|
|
||||||
className={cn(
|
|
||||||
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
|
||||||
"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
initial={{ backgroundPosition: "100% center" }}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--spread": `${dynamicSpread}px`,
|
|
||||||
backgroundImage:
|
|
||||||
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
transition={{
|
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
duration,
|
|
||||||
ease: "linear",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MotionComponent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Shimmer = memo(ShimmerComponent)
|
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import React from "react";
|
||||||
import type React from "react"
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { Button, type buttonVariants } from "@/components/ui/button"
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip";
|
||||||
|
import { type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
interface ButtonWithTooltipProps
|
interface ButtonWithTooltipProps
|
||||||
extends React.ComponentProps<"button">,
|
extends React.ComponentProps<"button">,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
tooltipContent: string
|
tooltipContent: string;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonWithTooltip({
|
export function ButtonWithTooltip({
|
||||||
@@ -27,10 +27,8 @@ export function ButtonWithTooltip({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button {...buttonProps}>{children}</Button>
|
<Button {...buttonProps}>{children}</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs text-wrap">
|
<TooltipContent className="max-w-xs text-wrap">{tooltipContent}</TooltipContent>
|
||||||
{tooltipContent}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +1,71 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Cloud, FileText, GitBranch, Palette, Zap } from "lucide-react"
|
import { Zap, Cloud, GitBranch, Palette } from "lucide-react";
|
||||||
|
|
||||||
interface ExampleCardProps {
|
interface ExampleCardProps {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode;
|
||||||
title: string
|
title: string;
|
||||||
description: string
|
description: string;
|
||||||
onClick: () => void
|
onClick: () => void;
|
||||||
isNew?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExampleCard({
|
function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) {
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
onClick,
|
|
||||||
isNew,
|
|
||||||
}: ExampleCardProps) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`group w-full text-left p-4 rounded-xl border bg-card hover:bg-accent/50 hover:border-primary/30 transition-all duration-200 hover:shadow-sm ${
|
className="group w-full text-left p-4 rounded-xl border border-border/60 bg-card hover:bg-accent/50 hover:border-primary/30 transition-all duration-200 hover:shadow-sm"
|
||||||
isNew
|
|
||||||
? "border-primary/40 ring-1 ring-primary/20"
|
|
||||||
: "border-border/60"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div
|
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary/15 transition-colors">
|
||||||
className={`w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors ${
|
|
||||||
isNew
|
|
||||||
? "bg-primary/20 group-hover:bg-primary/25"
|
|
||||||
: "bg-primary/10 group-hover:bg-primary/15"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<h3 className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
||||||
<h3 className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
{title}
|
||||||
{title}
|
</h3>
|
||||||
</h3>
|
|
||||||
{isNew && (
|
|
||||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-primary text-primary-foreground rounded">
|
|
||||||
NEW
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExamplePanel({
|
export default function ExamplePanel({
|
||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
}: {
|
}: {
|
||||||
setInput: (input: string) => void
|
setInput: (input: string) => void;
|
||||||
setFiles: (files: File[]) => void
|
setFiles: (files: File[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleReplicateFlowchart = async () => {
|
const handleReplicateFlowchart = async () => {
|
||||||
setInput("Replicate this flowchart.")
|
setInput("Replicate this flowchart.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/example.png")
|
const response = await fetch("/example.png");
|
||||||
const blob = await response.blob()
|
const blob = await response.blob();
|
||||||
const file = new File([blob], "example.png", { type: "image/png" })
|
const file = new File([blob], "example.png", { type: "image/png" });
|
||||||
setFiles([file])
|
setFiles([file]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading example image:", error)
|
console.error("Error loading example image:", error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleReplicateArchitecture = async () => {
|
const handleReplicateArchitecture = async () => {
|
||||||
setInput("Replicate this in aws style")
|
setInput("Replicate this in aws style");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/architecture.png")
|
const response = await fetch("/architecture.png");
|
||||||
const blob = await response.blob()
|
const blob = await response.blob();
|
||||||
const file = new File([blob], "architecture.png", {
|
const file = new File([blob], "architecture.png", {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
})
|
});
|
||||||
setFiles([file])
|
setFiles([file]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading architecture image:", error)
|
console.error("Error loading architecture image:", error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePdfExample = async () => {
|
|
||||||
setInput("Summarize this paper as a diagram")
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/chain-of-thought.txt")
|
|
||||||
const blob = await response.blob()
|
|
||||||
const file = new File([blob], "chain-of-thought.txt", {
|
|
||||||
type: "text/plain",
|
|
||||||
})
|
|
||||||
setFiles([file])
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading text file:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 px-2 animate-fade-in">
|
<div className="py-6 px-2 animate-fade-in">
|
||||||
@@ -114,8 +75,7 @@ export default function ExamplePanel({
|
|||||||
Create diagrams with AI
|
Create diagrams with AI
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
||||||
Describe what you want to create or upload an image to
|
Describe what you want to create or upload an image to replicate
|
||||||
replicate
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -126,22 +86,12 @@ export default function ExamplePanel({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<ExampleCard
|
|
||||||
icon={<FileText className="w-4 h-4 text-primary" />}
|
|
||||||
title="Paper to Diagram"
|
|
||||||
description="Upload .pdf, .txt, .md, .json, .csv, .py, .js, .ts and more"
|
|
||||||
onClick={handlePdfExample}
|
|
||||||
isNew
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ExampleCard
|
<ExampleCard
|
||||||
icon={<Zap className="w-4 h-4 text-primary" />}
|
icon={<Zap className="w-4 h-4 text-primary" />}
|
||||||
title="Animated Diagram"
|
title="Animated Diagram"
|
||||||
description="Draw a transformer architecture with animated connectors"
|
description="Draw a transformer architecture with animated connectors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInput(
|
setInput("Give me a **animated connector** diagram of transformer's architecture")
|
||||||
"Give me a **animated connector** diagram of transformer's architecture",
|
|
||||||
)
|
|
||||||
setFiles([])
|
setFiles([])
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -176,5 +126,5 @@ export default function ExamplePanel({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +1,35 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
|
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { ResetWarningModal } from "@/components/reset-warning-modal";
|
||||||
|
import { SaveDialog } from "@/components/save-dialog";
|
||||||
import {
|
import {
|
||||||
Download,
|
|
||||||
History,
|
|
||||||
Image as ImageIcon,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Send,
|
Send,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react"
|
Image as ImageIcon,
|
||||||
import type React from "react"
|
History,
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
Download,
|
||||||
import { toast } from "sonner"
|
Paperclip,
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
} from "lucide-react";
|
||||||
import { ErrorToast } from "@/components/error-toast"
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
||||||
import { HistoryDialog } from "@/components/history-dialog"
|
import { FilePreviewList } from "./file-preview-list";
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
import { useDiagram } from "@/contexts/diagram-context";
|
||||||
import { SaveDialog } from "@/components/save-dialog"
|
import { HistoryDialog } from "@/components/history-dialog";
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
|
||||||
import { FilePreviewList } from "./file-preview-list"
|
|
||||||
|
|
||||||
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
||||||
const MAX_FILES = 5
|
|
||||||
|
|
||||||
function isValidFileType(file: File): boolean {
|
|
||||||
return file.type.startsWith("image/") || isPdfFile(file) || isTextFile(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFileSize(bytes: number): string {
|
|
||||||
const mb = bytes / 1024 / 1024
|
|
||||||
if (mb < 0.01) return `${(bytes / 1024).toFixed(0)}KB`
|
|
||||||
return `${mb.toFixed(2)}MB`
|
|
||||||
}
|
|
||||||
|
|
||||||
function showErrorToast(message: React.ReactNode) {
|
|
||||||
toast.custom(
|
|
||||||
(t) => (
|
|
||||||
<ErrorToast message={message} onDismiss={() => toast.dismiss(t)} />
|
|
||||||
),
|
|
||||||
{ duration: 5000 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidationResult {
|
|
||||||
validFiles: File[]
|
|
||||||
errors: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateFiles(
|
|
||||||
newFiles: File[],
|
|
||||||
existingCount: number,
|
|
||||||
): ValidationResult {
|
|
||||||
const errors: string[] = []
|
|
||||||
const validFiles: File[] = []
|
|
||||||
|
|
||||||
const availableSlots = MAX_FILES - existingCount
|
|
||||||
|
|
||||||
if (availableSlots <= 0) {
|
|
||||||
errors.push(`Maximum ${MAX_FILES} files allowed`)
|
|
||||||
return { validFiles, errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of newFiles) {
|
|
||||||
if (validFiles.length >= availableSlots) {
|
|
||||||
errors.push(`Only ${availableSlots} more file(s) allowed`)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (!isValidFileType(file)) {
|
|
||||||
errors.push(`"${file.name}" is not a supported file type`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter)
|
|
||||||
const isExtractedFile = isPdfFile(file) || isTextFile(file)
|
|
||||||
if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) {
|
|
||||||
const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024
|
|
||||||
errors.push(
|
|
||||||
`"${file.name}" is ${formatFileSize(file.size)} (exceeds ${maxSizeMB}MB)`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
validFiles.push(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { validFiles, errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
function showValidationErrors(errors: string[]) {
|
|
||||||
if (errors.length === 0) return
|
|
||||||
|
|
||||||
if (errors.length === 1) {
|
|
||||||
showErrorToast(
|
|
||||||
<span className="text-muted-foreground">{errors[0]}</span>,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showErrorToast(
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-medium">
|
|
||||||
{errors.length} files rejected:
|
|
||||||
</span>
|
|
||||||
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
|
||||||
{errors.slice(0, 3).map((err) => (
|
|
||||||
<li key={err}>{err}</li>
|
|
||||||
))}
|
|
||||||
{errors.length > 3 && (
|
|
||||||
<li>...and {errors.length - 3} more</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
input: string
|
input: string;
|
||||||
status: "submitted" | "streaming" | "ready" | "error"
|
status: "submitted" | "streaming" | "ready" | "error";
|
||||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
onClearChat: () => void
|
onClearChat: () => void;
|
||||||
files?: File[]
|
files?: File[];
|
||||||
onFileChange?: (files: File[]) => void
|
onFileChange?: (files: File[]) => void;
|
||||||
pdfData?: Map<
|
showHistory?: boolean;
|
||||||
File,
|
onToggleHistory?: (show: boolean) => void;
|
||||||
{ text: string; charCount: number; isExtracting: boolean }
|
error?: Error | null;
|
||||||
>
|
|
||||||
showHistory?: boolean
|
|
||||||
onToggleHistory?: (show: boolean) => void
|
|
||||||
sessionId?: string
|
|
||||||
error?: Error | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -139,147 +40,128 @@ export function ChatInput({
|
|||||||
onClearChat,
|
onClearChat,
|
||||||
files = [],
|
files = [],
|
||||||
onFileChange = () => {},
|
onFileChange = () => {},
|
||||||
pdfData = new Map(),
|
|
||||||
showHistory = false,
|
showHistory = false,
|
||||||
onToggleHistory = () => {},
|
onToggleHistory = () => {},
|
||||||
sessionId,
|
|
||||||
error = null,
|
error = null,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
const { diagramHistory, saveDiagramToFile } = 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 [showSaveDialog, setShowSaveDialog] = 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
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled);
|
||||||
|
}, [status, isDisabled]);
|
||||||
|
|
||||||
const adjustTextareaHeight = useCallback(() => {
|
const adjustTextareaHeight = useCallback(() => {
|
||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current;
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.style.height = "auto"
|
textarea.style.height = "auto";
|
||||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`
|
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// Handle programmatic input changes (e.g., setInput("") after form submission)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight();
|
||||||
}, [input, adjustTextareaHeight])
|
}, [input, adjustTextareaHeight]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
onChange(e)
|
|
||||||
adjustTextareaHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
const form = e.currentTarget.closest("form")
|
const form = e.currentTarget.closest("form");
|
||||||
if (form && input.trim() && !isDisabled) {
|
if (form && input.trim() && !isDisabled) {
|
||||||
form.requestSubmit()
|
form.requestSubmit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
if (isDisabled) return
|
if (isDisabled) return;
|
||||||
|
|
||||||
const items = e.clipboardData.items
|
const items = e.clipboardData.items;
|
||||||
const imageItems = Array.from(items).filter((item) =>
|
const imageItems = Array.from(items).filter((item) =>
|
||||||
item.type.startsWith("image/"),
|
item.type.startsWith("image/")
|
||||||
)
|
);
|
||||||
|
|
||||||
if (imageItems.length > 0) {
|
if (imageItems.length > 0) {
|
||||||
const imageFiles = (
|
const imageFiles = await Promise.all(
|
||||||
await Promise.all(
|
imageItems.map(async (item) => {
|
||||||
imageItems.map(async (item, index) => {
|
const file = item.getAsFile();
|
||||||
const file = item.getAsFile()
|
if (!file) return null;
|
||||||
if (!file) return null
|
return new File(
|
||||||
return new File(
|
[file],
|
||||||
[file],
|
`pasted-image-${Date.now()}.${file.type.split("/")[1]}`,
|
||||||
`pasted-image-${Date.now()}-${index}.${file.type.split("/")[1]}`,
|
{
|
||||||
{ type: file.type },
|
type: file.type,
|
||||||
)
|
}
|
||||||
}),
|
);
|
||||||
)
|
})
|
||||||
).filter((f): f is File => f !== null)
|
);
|
||||||
|
|
||||||
const { validFiles, errors } = validateFiles(
|
const validFiles = imageFiles.filter(
|
||||||
imageFiles,
|
(file): file is File => file !== null
|
||||||
files.length,
|
);
|
||||||
)
|
|
||||||
showValidationErrors(errors)
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles])
|
onFileChange([...files, ...validFiles]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newFiles = Array.from(e.target.files || [])
|
const newFiles = Array.from(e.target.files || []);
|
||||||
const { validFiles, errors } = validateFiles(newFiles, files.length)
|
onFileChange([...files, ...newFiles]);
|
||||||
showValidationErrors(errors)
|
};
|
||||||
if (validFiles.length > 0) {
|
|
||||||
onFileChange([...files, ...validFiles])
|
|
||||||
}
|
|
||||||
// Reset input so same file can be selected again
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveFile = (fileToRemove: File) => {
|
const handleRemoveFile = (fileToRemove: File) => {
|
||||||
onFileChange(files.filter((file) => file !== fileToRemove))
|
onFileChange(files.filter((file) => file !== fileToRemove));
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = ""
|
fileInputRef.current.value = "";
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
fileInputRef.current?.click()
|
fileInputRef.current?.click();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
setIsDragging(true)
|
setIsDragging(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
setIsDragging(false)
|
setIsDragging(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
setIsDragging(false)
|
setIsDragging(false);
|
||||||
|
|
||||||
if (isDisabled) return
|
if (isDisabled) return;
|
||||||
|
|
||||||
const droppedFiles = e.dataTransfer.files
|
const droppedFiles = e.dataTransfer.files;
|
||||||
const supportedFiles = Array.from(droppedFiles).filter((file) =>
|
|
||||||
isValidFileType(file),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { validFiles, errors } = validateFiles(
|
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
||||||
supportedFiles,
|
file.type.startsWith("image/")
|
||||||
files.length,
|
);
|
||||||
)
|
|
||||||
showValidationErrors(errors)
|
if (imageFiles.length > 0) {
|
||||||
if (validFiles.length > 0) {
|
onFileChange([...files, ...imageFiles]);
|
||||||
onFileChange([...files, ...validFiles])
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onClearChat()
|
onClearChat();
|
||||||
setShowClearDialog(false)
|
setShowClearDialog(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -296,11 +178,7 @@ export function ChatInput({
|
|||||||
{/* File previews */}
|
{/* File previews */}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<FilePreviewList
|
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} />
|
||||||
files={files}
|
|
||||||
onRemoveFile={handleRemoveFile}
|
|
||||||
pdfData={pdfData}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -309,10 +187,10 @@ export function ChatInput({
|
|||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={handleChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder="Describe your diagram or upload a file..."
|
placeholder="Describe your diagram or paste an image..."
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
aria-label="Chat input"
|
aria-label="Chat input"
|
||||||
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
|
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
|
||||||
@@ -374,12 +252,8 @@ export function ChatInput({
|
|||||||
<SaveDialog
|
<SaveDialog
|
||||||
open={showSaveDialog}
|
open={showSaveDialog}
|
||||||
onOpenChange={setShowSaveDialog}
|
onOpenChange={setShowSaveDialog}
|
||||||
onSave={(filename, format) =>
|
onSave={(filename, format) => saveDiagramToFile(filename, format)}
|
||||||
saveDiagramToFile(filename, format, sessionId)
|
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
|
||||||
}
|
|
||||||
defaultFilename={`diagram-${new Date()
|
|
||||||
.toISOString()
|
|
||||||
.slice(0, 10)}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
@@ -388,7 +262,7 @@ export function ChatInput({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={triggerFileInput}
|
onClick={triggerFileInput}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
tooltipContent="Upload file (image, PDF, text)"
|
tooltipContent="Upload image"
|
||||||
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"
|
||||||
>
|
>
|
||||||
<ImageIcon className="h-4 w-4" />
|
<ImageIcon className="h-4 w-4" />
|
||||||
@@ -399,7 +273,7 @@ export function ChatInput({
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
|
accept="image/*"
|
||||||
multiple
|
multiple
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
@@ -411,9 +285,7 @@ export function ChatInput({
|
|||||||
disabled={isDisabled || !input.trim()}
|
disabled={isDisabled || !input.trim()}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||||
aria-label={
|
aria-label={isDisabled ? "Sending..." : "Send message"}
|
||||||
isDisabled ? "Sending..." : "Send message"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
@@ -427,6 +299,7 @@ export function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,22 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Highlight, themes } from "prism-react-renderer"
|
import { Highlight, themes } from "prism-react-renderer";
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
code: string
|
code: string;
|
||||||
language?: "xml" | "json"
|
language?: "xml" | "json";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden w-full">
|
<div className="overflow-hidden w-full">
|
||||||
<Highlight theme={themes.github} code={code} language={language}>
|
<Highlight theme={themes.github} code={code} language={language}>
|
||||||
{({
|
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
||||||
className: _className,
|
|
||||||
style,
|
|
||||||
tokens,
|
|
||||||
getLineProps,
|
|
||||||
getTokenProps,
|
|
||||||
}) => (
|
|
||||||
<pre
|
<pre
|
||||||
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
|
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
fontFamily:
|
fontFamily: "var(--font-mono), ui-monospace, monospace",
|
||||||
"var(--font-mono), ui-monospace, monospace",
|
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@@ -32,16 +25,9 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tokens.map((line, i) => (
|
{tokens.map((line, i) => (
|
||||||
<div
|
<div key={i} {...getLineProps({ line })} style={{ wordBreak: "break-all" }}>
|
||||||
key={i}
|
|
||||||
{...getLineProps({ line })}
|
|
||||||
style={{ wordBreak: "break-all" }}
|
|
||||||
>
|
|
||||||
{line.map((token, key) => (
|
{line.map((token, key) => (
|
||||||
<span
|
<span key={key} {...getTokenProps({ token })} />
|
||||||
key={key}
|
|
||||||
{...getTokenProps({ token })}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -49,5 +35,5 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
|||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import type React from "react"
|
|
||||||
|
|
||||||
interface ErrorToastProps {
|
|
||||||
message: React.ReactNode
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === "Enter" || e.key === " " || e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
aria-live="polite"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={onDismiss}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm cursor-pointer hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-destructive/10 flex-shrink-0">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-destructive"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-foreground">{message}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,142 +1,49 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { FileCode, FileText, Loader2, X } from "lucide-react"
|
import React, { useEffect, useState } from "react";
|
||||||
import Image from "next/image"
|
import Image from "next/image";
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { X } from "lucide-react";
|
||||||
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
|
||||||
|
|
||||||
function formatCharCount(count: number): string {
|
|
||||||
if (count >= 1000) {
|
|
||||||
return `${(count / 1000).toFixed(1)}k`
|
|
||||||
}
|
|
||||||
return String(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilePreviewListProps {
|
interface FilePreviewListProps {
|
||||||
files: File[]
|
files: File[];
|
||||||
onRemoveFile: (fileToRemove: File) => void
|
onRemoveFile: (fileToRemove: File) => void;
|
||||||
pdfData?: Map<
|
|
||||||
File,
|
|
||||||
{ text: string; charCount: number; isExtracting: boolean }
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilePreviewList({
|
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
||||||
files,
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
onRemoveFile,
|
|
||||||
pdfData = new Map(),
|
|
||||||
}: FilePreviewListProps) {
|
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
|
||||||
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
|
||||||
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
|
||||||
|
|
||||||
// Create and cleanup object URLs when files change
|
// Cleanup object URLs on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentUrls = imageUrlsRef.current
|
const objectUrls = files
|
||||||
const newUrls = new Map<File, string>()
|
.filter((file) => file.type.startsWith("image/"))
|
||||||
|
.map((file) => URL.createObjectURL(file));
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
// Reuse existing URL if file is already tracked
|
|
||||||
const existingUrl = currentUrls.get(file)
|
|
||||||
if (existingUrl) {
|
|
||||||
newUrls.set(file, existingUrl)
|
|
||||||
} else {
|
|
||||||
newUrls.set(file, URL.createObjectURL(file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Revoke URLs for files that are no longer in the list
|
|
||||||
currentUrls.forEach((url, file) => {
|
|
||||||
if (!newUrls.has(file)) {
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
imageUrlsRef.current = newUrls
|
|
||||||
setImageUrls(newUrls)
|
|
||||||
}, [files])
|
|
||||||
|
|
||||||
// Cleanup all URLs on unmount only
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
return () => {
|
||||||
imageUrlsRef.current.forEach((url) => {
|
objectUrls.forEach(URL.revokeObjectURL);
|
||||||
URL.revokeObjectURL(url)
|
};
|
||||||
})
|
}, [files]);
|
||||||
// Clear the ref so StrictMode remount creates fresh URLs
|
|
||||||
imageUrlsRef.current = new Map()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Clear selected image if its URL was revoked
|
if (files.length === 0) return null;
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
selectedImage &&
|
|
||||||
!Array.from(imageUrls.values()).includes(selectedImage)
|
|
||||||
) {
|
|
||||||
setSelectedImage(null)
|
|
||||||
}
|
|
||||||
}, [imageUrls, selectedImage])
|
|
||||||
|
|
||||||
if (files.length === 0) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
||||||
{files.map((file, index) => {
|
{files.map((file, index) => {
|
||||||
const imageUrl = imageUrls.get(file) || null
|
const imageUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null;
|
||||||
const pdfInfo = pdfData.get(file)
|
|
||||||
return (
|
return (
|
||||||
<div key={file.name + index} className="relative group">
|
<div key={file.name + index} className="relative group">
|
||||||
<div
|
<div
|
||||||
className={`w-20 h-20 border rounded-md overflow-hidden bg-muted ${
|
className="w-20 h-20 border rounded-md overflow-hidden bg-muted cursor-pointer"
|
||||||
file.type.startsWith("image/") && imageUrl
|
onClick={() => imageUrl && setSelectedImage(imageUrl)}
|
||||||
? "cursor-pointer"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() =>
|
|
||||||
file.type.startsWith("image/") &&
|
|
||||||
imageUrl &&
|
|
||||||
setSelectedImage(imageUrl)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{file.type.startsWith("image/") && imageUrl ? (
|
{file.type.startsWith("image/") ? (
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl!}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
width={80}
|
width={80}
|
||||||
height={80}
|
height={80}
|
||||||
className="object-cover w-full h-full"
|
className="object-cover w-full h-full"
|
||||||
unoptimized
|
|
||||||
/>
|
/>
|
||||||
) : isPdfFile(file) || isTextFile(file) ? (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full p-1">
|
|
||||||
{pdfInfo?.isExtracting ? (
|
|
||||||
<Loader2 className="h-6 w-6 text-blue-500 mb-1 animate-spin" />
|
|
||||||
) : isPdfFile(file) ? (
|
|
||||||
<FileText className="h-6 w-6 text-red-500 mb-1" />
|
|
||||||
) : (
|
|
||||||
<FileCode className="h-6 w-6 text-blue-500 mb-1" />
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-center truncate w-full px-1">
|
|
||||||
{file.name.length > 10
|
|
||||||
? `${file.name.slice(0, 7)}...`
|
|
||||||
: file.name}
|
|
||||||
</span>
|
|
||||||
{pdfInfo?.isExtracting ? (
|
|
||||||
<span className="text-[10px] text-muted-foreground">
|
|
||||||
Reading...
|
|
||||||
</span>
|
|
||||||
) : pdfInfo?.charCount ? (
|
|
||||||
<span className="text-[10px] text-green-600 font-medium">
|
|
||||||
{formatCharCount(
|
|
||||||
pdfInfo.charCount,
|
|
||||||
)}{" "}
|
|
||||||
chars
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-xs text-center p-1">
|
<div className="flex items-center justify-center h-full text-xs text-center p-1">
|
||||||
{file.name}
|
{file.name}
|
||||||
@@ -152,7 +59,7 @@ export function FilePreviewList({
|
|||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -177,11 +84,10 @@ export function FilePreviewList({
|
|||||||
height={900}
|
height={900}
|
||||||
className="object-contain max-w-full max-h-[90vh] w-auto h-auto"
|
className="object-contain max-w-full max-h-[90vh] w-auto h-auto"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
unoptimized
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image"
|
import { useState } from "react";
|
||||||
import { useState } from "react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -10,33 +8,34 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useDiagram } from "@/contexts/diagram-context";
|
||||||
|
|
||||||
interface HistoryDialogProps {
|
interface HistoryDialogProps {
|
||||||
showHistory: boolean
|
showHistory: boolean;
|
||||||
onToggleHistory: (show: boolean) => void
|
onToggleHistory: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryDialog({
|
export function HistoryDialog({
|
||||||
showHistory,
|
showHistory,
|
||||||
onToggleHistory,
|
onToggleHistory,
|
||||||
}: HistoryDialogProps) {
|
}: HistoryDialogProps) {
|
||||||
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
|
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram();
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSelectedIndex(null)
|
setSelectedIndex(null);
|
||||||
onToggleHistory(false)
|
onToggleHistory(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleConfirmRestore = () => {
|
const handleConfirmRestore = () => {
|
||||||
if (selectedIndex !== null) {
|
if (selectedIndex !== null) {
|
||||||
// Skip validation for trusted history snapshots
|
onDisplayChart(diagramHistory[selectedIndex].xml);
|
||||||
onDisplayChart(diagramHistory[selectedIndex].xml, true)
|
handleClose();
|
||||||
handleClose()
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
||||||
@@ -101,12 +100,15 @@ export function HistoryDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Coffee, X } from "lucide-react"
|
|
||||||
import Link from "next/link"
|
|
||||||
import type React from "react"
|
|
||||||
import { FaGithub } from "react-icons/fa"
|
|
||||||
|
|
||||||
interface QuotaLimitToastProps {
|
|
||||||
type?: "request" | "token"
|
|
||||||
used: number
|
|
||||||
limit: number
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QuotaLimitToast({
|
|
||||||
type = "request",
|
|
||||||
used,
|
|
||||||
limit,
|
|
||||||
onDismiss,
|
|
||||||
}: QuotaLimitToastProps) {
|
|
||||||
const isTokenLimit = type === "token"
|
|
||||||
const formatNumber = (n: number) =>
|
|
||||||
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toString()
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
aria-live="polite"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
className="relative w-[400px] overflow-hidden rounded-xl border border-border/50 bg-card p-5 shadow-soft animate-message-in"
|
|
||||||
>
|
|
||||||
{/* Close button */}
|
|
||||||
<button
|
|
||||||
onClick={onDismiss}
|
|
||||||
className="absolute right-3 top-3 p-1.5 rounded-full text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
|
||||||
aria-label="Dismiss"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Title row with icon */}
|
|
||||||
<div className="flex items-center gap-2.5 mb-3 pr-6">
|
|
||||||
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-accent flex items-center justify-center">
|
|
||||||
<Coffee
|
|
||||||
className="w-4 h-4 text-accent-foreground"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h3 className="font-semibold text-foreground text-sm">
|
|
||||||
{isTokenLimit
|
|
||||||
? "Daily Token Limit Reached"
|
|
||||||
: "Daily Quota Reached"}
|
|
||||||
</h3>
|
|
||||||
<span className="px-2 py-0.5 text-xs font-medium rounded-md bg-muted text-muted-foreground">
|
|
||||||
{isTokenLimit
|
|
||||||
? `${formatNumber(used)}/${formatNumber(limit)} tokens`
|
|
||||||
: `${used}/${limit}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Message */}
|
|
||||||
<div className="text-sm text-muted-foreground leading-relaxed mb-4 space-y-2">
|
|
||||||
<p>
|
|
||||||
Oops — you've reached the daily{" "}
|
|
||||||
{isTokenLimit ? "token" : "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.{" "}
|
|
||||||
<Link
|
|
||||||
href="/about"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 text-amber-600 font-medium hover:text-amber-700 hover:underline"
|
|
||||||
>
|
|
||||||
Learn more →
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Tip:</strong> You can use your own API key (click
|
|
||||||
the Settings icon) or self-host the project to bypass these
|
|
||||||
limits.
|
|
||||||
</p>
|
|
||||||
<p>Your limit resets tomorrow. Thanks for understanding!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action buttons */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
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" />
|
|
||||||
Self-host
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/sponsors/DayuanJiang"
|
|
||||||
target="_blank"
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Coffee className="w-3.5 h-3.5" />
|
|
||||||
Sponsor
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
interface ResetWarningModalProps {
|
interface ResetWarningModalProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void;
|
||||||
onClear: () => void
|
onClear: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResetWarningModal({
|
export function ResetWarningModal({
|
||||||
@@ -44,5 +44,5 @@ export function ResetWarningModal({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,36 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
DialogFooter,
|
||||||
import { Input } from "@/components/ui/input"
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
export type ExportFormat = "drawio" | "png" | "svg"
|
export type ExportFormat = "drawio" | "png" | "svg";
|
||||||
|
|
||||||
const FORMAT_OPTIONS: {
|
const FORMAT_OPTIONS: { value: ExportFormat; label: string; extension: string }[] = [
|
||||||
value: ExportFormat
|
|
||||||
label: string
|
|
||||||
extension: string
|
|
||||||
}[] = [
|
|
||||||
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
|
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
|
||||||
{ value: "png", label: "PNG Image", extension: ".png" },
|
{ value: "png", label: "PNG Image", extension: ".png" },
|
||||||
{ value: "svg", label: "SVG Image", extension: ".svg" },
|
{ value: "svg", label: "SVG Image", extension: ".svg" },
|
||||||
]
|
];
|
||||||
|
|
||||||
interface SaveDialogProps {
|
interface SaveDialogProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void;
|
||||||
onSave: (filename: string, format: ExportFormat) => void
|
onSave: (filename: string, format: ExportFormat) => void;
|
||||||
defaultFilename: string
|
defaultFilename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SaveDialog({
|
export function SaveDialog({
|
||||||
@@ -43,29 +39,29 @@ export function SaveDialog({
|
|||||||
onSave,
|
onSave,
|
||||||
defaultFilename,
|
defaultFilename,
|
||||||
}: SaveDialogProps) {
|
}: SaveDialogProps) {
|
||||||
const [filename, setFilename] = useState(defaultFilename)
|
const [filename, setFilename] = useState(defaultFilename);
|
||||||
const [format, setFormat] = useState<ExportFormat>("drawio")
|
const [format, setFormat] = useState<ExportFormat>("drawio");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setFilename(defaultFilename)
|
setFilename(defaultFilename);
|
||||||
}
|
}
|
||||||
}, [open, defaultFilename])
|
}, [open, defaultFilename]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const finalFilename = filename.trim() || defaultFilename
|
const finalFilename = filename.trim() || defaultFilename;
|
||||||
onSave(finalFilename, format)
|
onSave(finalFilename, format);
|
||||||
onOpenChange(false)
|
onOpenChange(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
handleSave()
|
handleSave();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format)
|
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -76,19 +72,13 @@ export function SaveDialog({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Format</label>
|
<label className="text-sm font-medium">Format</label>
|
||||||
<Select
|
<Select value={format} onValueChange={(v) => setFormat(v as ExportFormat)}>
|
||||||
value={format}
|
|
||||||
onValueChange={(v) => setFormat(v as ExportFormat)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FORMAT_OPTIONS.map((opt) => (
|
{FORMAT_OPTIONS.map((opt) => (
|
||||||
<SelectItem
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
key={opt.value}
|
|
||||||
value={opt.value}
|
|
||||||
>
|
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -114,15 +104,12 @@ export function SaveDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave}>Save</Button>
|
<Button onClick={handleSave}>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,436 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Moon, Sun } from "lucide-react"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { Switch } from "@/components/ui/switch"
|
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
onCloseProtectionChange?: (enabled: boolean) => void
|
|
||||||
drawioUi: "min" | "sketch"
|
|
||||||
onToggleDrawioUi: () => void
|
|
||||||
darkMode: boolean
|
|
||||||
onToggleDarkMode: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
|
||||||
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
|
||||||
const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required"
|
|
||||||
export const STORAGE_AI_PROVIDER_KEY = "next-ai-draw-io-ai-provider"
|
|
||||||
export const STORAGE_AI_BASE_URL_KEY = "next-ai-draw-io-ai-base-url"
|
|
||||||
export const STORAGE_AI_API_KEY_KEY = "next-ai-draw-io-ai-api-key"
|
|
||||||
export const STORAGE_AI_MODEL_KEY = "next-ai-draw-io-ai-model"
|
|
||||||
|
|
||||||
function getStoredAccessCodeRequired(): boolean | null {
|
|
||||||
if (typeof window === "undefined") return null
|
|
||||||
const stored = localStorage.getItem(STORAGE_ACCESS_CODE_REQUIRED_KEY)
|
|
||||||
if (stored === null) return null
|
|
||||||
return stored === "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
onCloseProtectionChange,
|
|
||||||
drawioUi,
|
|
||||||
onToggleDrawioUi,
|
|
||||||
darkMode,
|
|
||||||
onToggleDarkMode,
|
|
||||||
}: SettingsDialogProps) {
|
|
||||||
const [accessCode, setAccessCode] = useState("")
|
|
||||||
const [closeProtection, setCloseProtection] = useState(true)
|
|
||||||
const [isVerifying, setIsVerifying] = useState(false)
|
|
||||||
const [error, setError] = useState("")
|
|
||||||
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
|
||||||
() => getStoredAccessCodeRequired() ?? false,
|
|
||||||
)
|
|
||||||
const [provider, setProvider] = useState("")
|
|
||||||
const [baseUrl, setBaseUrl] = useState("")
|
|
||||||
const [apiKey, setApiKey] = useState("")
|
|
||||||
const [modelId, setModelId] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Only fetch if not cached in localStorage
|
|
||||||
if (getStoredAccessCodeRequired() !== null) return
|
|
||||||
|
|
||||||
fetch("/api/config")
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
const required = data?.accessCodeRequired === true
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_ACCESS_CODE_REQUIRED_KEY,
|
|
||||||
String(required),
|
|
||||||
)
|
|
||||||
setAccessCodeRequired(required)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Don't cache on error - allow retry on next mount
|
|
||||||
setAccessCodeRequired(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
const storedCode =
|
|
||||||
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
|
||||||
setAccessCode(storedCode)
|
|
||||||
|
|
||||||
const storedCloseProtection = localStorage.getItem(
|
|
||||||
STORAGE_CLOSE_PROTECTION_KEY,
|
|
||||||
)
|
|
||||||
// Default to true if not set
|
|
||||||
setCloseProtection(storedCloseProtection !== "false")
|
|
||||||
|
|
||||||
// Load AI provider settings
|
|
||||||
setProvider(localStorage.getItem(STORAGE_AI_PROVIDER_KEY) || "")
|
|
||||||
setBaseUrl(localStorage.getItem(STORAGE_AI_BASE_URL_KEY) || "")
|
|
||||||
setApiKey(localStorage.getItem(STORAGE_AI_API_KEY_KEY) || "")
|
|
||||||
setModelId(localStorage.getItem(STORAGE_AI_MODEL_KEY) || "")
|
|
||||||
|
|
||||||
setError("")
|
|
||||||
}
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!accessCodeRequired) return
|
|
||||||
|
|
||||||
setError("")
|
|
||||||
setIsVerifying(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/verify-access-code", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"x-access-code": accessCode.trim(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!data.valid) {
|
|
||||||
setError(data.message || "Invalid access code")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
|
||||||
onOpenChange(false)
|
|
||||||
} catch {
|
|
||||||
setError("Failed to verify access code")
|
|
||||||
} finally {
|
|
||||||
setIsVerifying(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Settings</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Configure your application settings.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-2">
|
|
||||||
{accessCodeRequired && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="access-code">Access Code</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="access-code"
|
|
||||||
type="password"
|
|
||||||
value={accessCode}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAccessCode(e.target.value)
|
|
||||||
}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Enter access code"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isVerifying || !accessCode.trim()}
|
|
||||||
>
|
|
||||||
{isVerifying ? "..." : "Save"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Required to use this application.
|
|
||||||
</p>
|
|
||||||
{error && (
|
|
||||||
<p className="text-[0.8rem] text-destructive">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>AI Provider Settings</Label>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Use your own API key to bypass usage limits. Your
|
|
||||||
key is stored locally in your browser and is never
|
|
||||||
stored on the server.
|
|
||||||
</p>
|
|
||||||
<div className="space-y-3 pt-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="ai-provider">Provider</Label>
|
|
||||||
<Select
|
|
||||||
value={provider || "default"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const actualValue =
|
|
||||||
value === "default" ? "" : value
|
|
||||||
setProvider(actualValue)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_AI_PROVIDER_KEY,
|
|
||||||
actualValue,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="ai-provider">
|
|
||||||
<SelectValue placeholder="Use Server Default" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="default">
|
|
||||||
Use Server Default
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="openai">
|
|
||||||
OpenAI
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="anthropic">
|
|
||||||
Anthropic
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="google">
|
|
||||||
Google
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="azure">
|
|
||||||
Azure OpenAI
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="openrouter">
|
|
||||||
OpenRouter
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="deepseek">
|
|
||||||
DeepSeek
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="siliconflow">
|
|
||||||
SiliconFlow
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{provider && provider !== "default" && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="ai-model">
|
|
||||||
Model ID
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="ai-model"
|
|
||||||
value={modelId}
|
|
||||||
onChange={(e) => {
|
|
||||||
setModelId(e.target.value)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_AI_MODEL_KEY,
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
placeholder={
|
|
||||||
provider === "openai"
|
|
||||||
? "e.g., gpt-4o"
|
|
||||||
: provider === "anthropic"
|
|
||||||
? "e.g., claude-sonnet-4-5"
|
|
||||||
: provider === "google"
|
|
||||||
? "e.g., gemini-2.0-flash-exp"
|
|
||||||
: provider ===
|
|
||||||
"deepseek"
|
|
||||||
? "e.g., deepseek-chat"
|
|
||||||
: "Model ID"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="ai-api-key">
|
|
||||||
API Key
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="ai-api-key"
|
|
||||||
type="password"
|
|
||||||
value={apiKey}
|
|
||||||
onChange={(e) => {
|
|
||||||
setApiKey(e.target.value)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_AI_API_KEY_KEY,
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
placeholder="Your API key"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Overrides{" "}
|
|
||||||
{provider === "openai"
|
|
||||||
? "OPENAI_API_KEY"
|
|
||||||
: provider === "anthropic"
|
|
||||||
? "ANTHROPIC_API_KEY"
|
|
||||||
: provider === "google"
|
|
||||||
? "GOOGLE_GENERATIVE_AI_API_KEY"
|
|
||||||
: provider === "azure"
|
|
||||||
? "AZURE_API_KEY"
|
|
||||||
: provider ===
|
|
||||||
"openrouter"
|
|
||||||
? "OPENROUTER_API_KEY"
|
|
||||||
: provider ===
|
|
||||||
"deepseek"
|
|
||||||
? "DEEPSEEK_API_KEY"
|
|
||||||
: provider ===
|
|
||||||
"siliconflow"
|
|
||||||
? "SILICONFLOW_API_KEY"
|
|
||||||
: "server API key"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="ai-base-url">
|
|
||||||
Base URL (optional)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="ai-base-url"
|
|
||||||
value={baseUrl}
|
|
||||||
onChange={(e) => {
|
|
||||||
setBaseUrl(e.target.value)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_AI_BASE_URL_KEY,
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
placeholder={
|
|
||||||
provider === "anthropic"
|
|
||||||
? "https://api.anthropic.com/v1"
|
|
||||||
: provider === "siliconflow"
|
|
||||||
? "https://api.siliconflow.com/v1"
|
|
||||||
: "Custom endpoint URL"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
localStorage.removeItem(
|
|
||||||
STORAGE_AI_PROVIDER_KEY,
|
|
||||||
)
|
|
||||||
localStorage.removeItem(
|
|
||||||
STORAGE_AI_BASE_URL_KEY,
|
|
||||||
)
|
|
||||||
localStorage.removeItem(
|
|
||||||
STORAGE_AI_API_KEY_KEY,
|
|
||||||
)
|
|
||||||
localStorage.removeItem(
|
|
||||||
STORAGE_AI_MODEL_KEY,
|
|
||||||
)
|
|
||||||
setProvider("")
|
|
||||||
setBaseUrl("")
|
|
||||||
setApiKey("")
|
|
||||||
setModelId("")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Settings
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="theme-toggle">Theme</Label>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Dark/Light mode for interface and DrawIO canvas.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
id="theme-toggle"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={onToggleDarkMode}
|
|
||||||
>
|
|
||||||
{darkMode ? (
|
|
||||||
<Sun className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="drawio-ui">DrawIO Style</Label>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Canvas style:{" "}
|
|
||||||
{drawioUi === "min" ? "Minimal" : "Sketch"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
id="drawio-ui"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onToggleDrawioUi}
|
|
||||||
>
|
|
||||||
Switch to{" "}
|
|
||||||
{drawioUi === "min" ? "Sketch" : "Minimal"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="close-protection">
|
|
||||||
Close Protection
|
|
||||||
</Label>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Show confirmation when leaving the page.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="close-protection"
|
|
||||||
checked={closeProtection}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setCloseProtection(checked)
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_CLOSE_PROTECTION_KEY,
|
|
||||||
checked.toString(),
|
|
||||||
)
|
|
||||||
onCloseProtectionChange?.(checked)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:brightness-75",
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
|
||||||
|
|
||||||
function Collapsible({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
|
||||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollapsibleTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
|
||||||
return (
|
|
||||||
<CollapsiblePrimitive.CollapsibleTrigger
|
|
||||||
data-slot="collapsible-trigger"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollapsibleContent({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
|
||||||
return (
|
|
||||||
<CollapsiblePrimitive.CollapsibleContent
|
|
||||||
data-slot="collapsible-content"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { GripVerticalIcon } from "lucide-react"
|
|
||||||
import * as ResizablePrimitive from "react-resizable-panels"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function ResizablePanelGroup({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
|
||||||
return (
|
|
||||||
<ResizablePrimitive.PanelGroup
|
|
||||||
data-slot="resizable-panel-group"
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResizablePanel({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
|
||||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResizableHandle({
|
|
||||||
withHandle,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
|
||||||
withHandle?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
|
||||||
data-slot="resizable-handle"
|
|
||||||
className={cn(
|
|
||||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{withHandle && (
|
|
||||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
|
||||||
<GripVerticalIcon className="size-2.5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
|
||||||
@@ -18,7 +18,7 @@ function ScrollArea({
|
|||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 !overflow-x-hidden"
|
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Switch({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SwitchPrimitive.Root
|
|
||||||
data-slot="switch"
|
|
||||||
className={cn(
|
|
||||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SwitchPrimitive.Thumb
|
|
||||||
data-slot="switch-thumb"
|
|
||||||
className={cn(
|
|
||||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SwitchPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Switch }
|
|
||||||
@@ -1,128 +1,85 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import type React from "react"
|
import React, { createContext, useContext, useRef, useState } from "react";
|
||||||
import { createContext, useContext, useRef, useState } from "react"
|
import type { DrawIoEmbedRef } from "react-drawio";
|
||||||
import type { DrawIoEmbedRef } from "react-drawio"
|
import { extractDiagramXML } from "../lib/utils";
|
||||||
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
import type { ExportFormat } from "@/components/save-dialog";
|
||||||
import type { ExportFormat } from "@/components/save-dialog"
|
|
||||||
import { extractDiagramXML, validateMxCellStructure } from "../lib/utils"
|
|
||||||
|
|
||||||
interface DiagramContextType {
|
interface DiagramContextType {
|
||||||
chartXML: string
|
chartXML: string;
|
||||||
latestSvg: string
|
latestSvg: string;
|
||||||
diagramHistory: { svg: string; xml: string }[]
|
diagramHistory: { svg: string; xml: string }[];
|
||||||
loadDiagram: (chart: string, skipValidation?: boolean) => string | null
|
loadDiagram: (chart: string) => void;
|
||||||
handleExport: () => void
|
handleExport: () => void;
|
||||||
handleExportWithoutHistory: () => void
|
handleExportWithoutHistory: () => void;
|
||||||
resolverRef: React.Ref<((value: string) => void) | null>
|
resolverRef: React.Ref<((value: string) => void) | null>;
|
||||||
drawioRef: React.Ref<DrawIoEmbedRef | null>
|
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
||||||
handleDiagramExport: (data: any) => void
|
handleDiagramExport: (data: any) => void;
|
||||||
clearDiagram: () => void
|
clearDiagram: () => void;
|
||||||
saveDiagramToFile: (
|
saveDiagramToFile: (filename: string, format: ExportFormat) => void;
|
||||||
filename: string,
|
|
||||||
format: ExportFormat,
|
|
||||||
sessionId?: string,
|
|
||||||
) => void
|
|
||||||
isDrawioReady: boolean
|
|
||||||
onDrawioLoad: () => void
|
|
||||||
resetDrawioReady: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [chartXML, setChartXML] = useState<string>("")
|
const [chartXML, setChartXML] = useState<string>("");
|
||||||
const [latestSvg, setLatestSvg] = useState<string>("")
|
const [latestSvg, setLatestSvg] = useState<string>("");
|
||||||
const [diagramHistory, setDiagramHistory] = useState<
|
const [diagramHistory, setDiagramHistory] = useState<
|
||||||
{ svg: string; xml: string }[]
|
{ svg: string; xml: string }[]
|
||||||
>([])
|
>([]);
|
||||||
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
const drawioRef = useRef<DrawIoEmbedRef | null>(null);
|
||||||
const hasCalledOnLoadRef = useRef(false)
|
const resolverRef = useRef<((value: string) => void) | null>(null);
|
||||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
|
||||||
const resolverRef = useRef<((value: string) => void) | null>(null)
|
|
||||||
// Track if we're expecting an export for history (user-initiated)
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
const expectHistoryExportRef = useRef<boolean>(false)
|
const expectHistoryExportRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const onDrawioLoad = () => {
|
|
||||||
// Only set ready state once to prevent infinite loops
|
|
||||||
if (hasCalledOnLoadRef.current) return
|
|
||||||
hasCalledOnLoadRef.current = true
|
|
||||||
// console.log("[DiagramContext] DrawIO loaded, setting ready state")
|
|
||||||
setIsDrawioReady(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetDrawioReady = () => {
|
|
||||||
// console.log("[DiagramContext] Resetting DrawIO ready state")
|
|
||||||
hasCalledOnLoadRef.current = false
|
|
||||||
setIsDrawioReady(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track if we're expecting an export for file save (stores raw export data)
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
const saveResolverRef = useRef<{
|
const saveResolverRef = useRef<{
|
||||||
resolver: ((data: string) => void) | null
|
resolver: ((data: string) => void) | null;
|
||||||
format: ExportFormat | null
|
format: ExportFormat | null;
|
||||||
}>({ resolver: null, format: null })
|
}>({ resolver: null, format: null });
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
// Mark that this export should be saved to history
|
// Mark that this export should be saved to history
|
||||||
expectHistoryExportRef.current = true
|
expectHistoryExportRef.current = true;
|
||||||
drawioRef.current.exportDiagram({
|
drawioRef.current.exportDiagram({
|
||||||
format: "xmlsvg",
|
format: "xmlsvg",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleExportWithoutHistory = () => {
|
const handleExportWithoutHistory = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
// Export without saving to history (for edit_diagram fetching current state)
|
// Export without saving to history (for edit_diagram fetching current state)
|
||||||
drawioRef.current.exportDiagram({
|
drawioRef.current.exportDiagram({
|
||||||
format: "xmlsvg",
|
format: "xmlsvg",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const loadDiagram = (
|
|
||||||
chart: string,
|
|
||||||
skipValidation?: boolean,
|
|
||||||
): string | null => {
|
|
||||||
// Validate XML structure before loading (unless skipped for internal use)
|
|
||||||
if (!skipValidation) {
|
|
||||||
const validationError = validateMxCellStructure(chart)
|
|
||||||
if (validationError) {
|
|
||||||
console.warn("[loadDiagram] Validation error:", validationError)
|
|
||||||
return validationError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
|
|
||||||
setChartXML(chart)
|
|
||||||
|
|
||||||
|
const loadDiagram = (chart: string) => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: chart,
|
xml: chart,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiagramExport = (data: any) => {
|
const handleDiagramExport = (data: any) => {
|
||||||
// Handle save to file if requested (process raw data before extraction)
|
// Handle save to file if requested (process raw data before extraction)
|
||||||
if (saveResolverRef.current.resolver) {
|
if (saveResolverRef.current.resolver) {
|
||||||
const format = saveResolverRef.current.format
|
const format = saveResolverRef.current.format;
|
||||||
saveResolverRef.current.resolver(data.data)
|
saveResolverRef.current.resolver(data.data);
|
||||||
saveResolverRef.current = { resolver: null, format: null }
|
saveResolverRef.current = { resolver: null, format: null };
|
||||||
// For non-xmlsvg formats, skip XML extraction as it will fail
|
// For non-xmlsvg formats, skip XML extraction as it will fail
|
||||||
// Only drawio (which uses xmlsvg internally) has the content attribute
|
// Only drawio (which uses xmlsvg internally) has the content attribute
|
||||||
if (format === "png" || format === "svg") {
|
if (format === "png" || format === "svg") {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractedXML = extractDiagramXML(data.data)
|
const extractedXML = extractDiagramXML(data.data);
|
||||||
setChartXML(extractedXML)
|
setChartXML(extractedXML);
|
||||||
setLatestSvg(data.data)
|
setLatestSvg(data.data);
|
||||||
|
|
||||||
// Only add to history if this was a user-initiated export
|
// Only add to history if this was a user-initiated export
|
||||||
if (expectHistoryExportRef.current) {
|
if (expectHistoryExportRef.current) {
|
||||||
@@ -132,120 +89,90 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
svg: data.data,
|
svg: data.data,
|
||||||
xml: extractedXML,
|
xml: extractedXML,
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
expectHistoryExportRef.current = false
|
expectHistoryExportRef.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolverRef.current) {
|
if (resolverRef.current) {
|
||||||
resolverRef.current(extractedXML)
|
resolverRef.current(extractedXML);
|
||||||
resolverRef.current = null
|
resolverRef.current = null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearDiagram = () => {
|
const clearDiagram = () => {
|
||||||
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`;
|
||||||
// Skip validation for trusted internal template (loadDiagram also sets chartXML)
|
loadDiagram(emptyDiagram);
|
||||||
loadDiagram(emptyDiagram, true)
|
setChartXML(emptyDiagram);
|
||||||
setLatestSvg("")
|
setLatestSvg("");
|
||||||
setDiagramHistory([])
|
setDiagramHistory([]);
|
||||||
}
|
};
|
||||||
|
|
||||||
const saveDiagramToFile = (
|
const saveDiagramToFile = (filename: string, format: ExportFormat) => {
|
||||||
filename: string,
|
|
||||||
format: ExportFormat,
|
|
||||||
sessionId?: string,
|
|
||||||
) => {
|
|
||||||
if (!drawioRef.current) {
|
if (!drawioRef.current) {
|
||||||
console.warn("Draw.io editor not ready")
|
console.warn("Draw.io editor not ready");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map format to draw.io export format
|
// Map format to draw.io export format
|
||||||
const drawioFormat = format === "drawio" ? "xmlsvg" : format
|
const drawioFormat = format === "drawio" ? "xmlsvg" : format;
|
||||||
|
|
||||||
// Set up the resolver before triggering export
|
// Set up the resolver before triggering export
|
||||||
saveResolverRef.current = {
|
saveResolverRef.current = {
|
||||||
resolver: (exportData: string) => {
|
resolver: (exportData: string) => {
|
||||||
let fileContent: string | Blob
|
let fileContent: string | Blob;
|
||||||
let mimeType: string
|
let mimeType: string;
|
||||||
let extension: string
|
let extension: string;
|
||||||
|
|
||||||
if (format === "drawio") {
|
if (format === "drawio") {
|
||||||
// Extract XML from SVG for .drawio format
|
// Extract XML from SVG for .drawio format
|
||||||
const xml = extractDiagramXML(exportData)
|
const xml = extractDiagramXML(exportData);
|
||||||
let xmlContent = xml
|
let xmlContent = xml;
|
||||||
if (!xml.includes("<mxfile")) {
|
if (!xml.includes("<mxfile")) {
|
||||||
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`
|
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
||||||
}
|
}
|
||||||
fileContent = xmlContent
|
fileContent = xmlContent;
|
||||||
mimeType = "application/xml"
|
mimeType = "application/xml";
|
||||||
extension = ".drawio"
|
extension = ".drawio";
|
||||||
|
|
||||||
// Save to localStorage when user manually saves
|
|
||||||
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xmlContent)
|
|
||||||
} else if (format === "png") {
|
} else if (format === "png") {
|
||||||
// PNG data comes as base64 data URL
|
// PNG data comes as base64 data URL
|
||||||
fileContent = exportData
|
fileContent = exportData;
|
||||||
mimeType = "image/png"
|
mimeType = "image/png";
|
||||||
extension = ".png"
|
extension = ".png";
|
||||||
} else {
|
} else {
|
||||||
// SVG format
|
// SVG format
|
||||||
fileContent = exportData
|
fileContent = exportData;
|
||||||
mimeType = "image/svg+xml"
|
mimeType = "image/svg+xml";
|
||||||
extension = ".svg"
|
extension = ".svg";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log save event to Langfuse (flags the trace)
|
|
||||||
logSaveToLangfuse(filename, format, sessionId)
|
|
||||||
|
|
||||||
// Handle download
|
// Handle download
|
||||||
let url: string
|
let url: string;
|
||||||
if (
|
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
|
||||||
typeof fileContent === "string" &&
|
|
||||||
fileContent.startsWith("data:")
|
|
||||||
) {
|
|
||||||
// Already a data URL (PNG)
|
// Already a data URL (PNG)
|
||||||
url = fileContent
|
url = fileContent;
|
||||||
} else {
|
} else {
|
||||||
const blob = new Blob([fileContent], { type: mimeType })
|
const blob = new Blob([fileContent], { type: mimeType });
|
||||||
url = URL.createObjectURL(blob)
|
url = URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = document.createElement("a")
|
const a = document.createElement("a");
|
||||||
a.href = url
|
a.href = url;
|
||||||
a.download = `${filename}${extension}`
|
a.download = `${filename}${extension}`;
|
||||||
document.body.appendChild(a)
|
document.body.appendChild(a);
|
||||||
a.click()
|
a.click();
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a);
|
||||||
|
|
||||||
// Delay URL revocation to ensure download completes
|
// Delay URL revocation to ensure download completes
|
||||||
if (!url.startsWith("data:")) {
|
if (!url.startsWith("data:")) {
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 100)
|
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
format,
|
format,
|
||||||
}
|
};
|
||||||
|
|
||||||
// Export diagram - callback will be handled in handleDiagramExport
|
// Export diagram - callback will be handled in handleDiagramExport
|
||||||
drawioRef.current.exportDiagram({ format: drawioFormat })
|
drawioRef.current.exportDiagram({ format: drawioFormat });
|
||||||
}
|
};
|
||||||
|
|
||||||
// Log save event to Langfuse (just flags the trace, doesn't send content)
|
|
||||||
const logSaveToLangfuse = async (
|
|
||||||
filename: string,
|
|
||||||
format: string,
|
|
||||||
sessionId?: string,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await fetch("/api/log-save", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ filename, format, sessionId }),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to log save to Langfuse:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiagramContext.Provider
|
<DiagramContext.Provider
|
||||||
@@ -261,20 +188,17 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
handleDiagramExport,
|
handleDiagramExport,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
saveDiagramToFile,
|
saveDiagramToFile,
|
||||||
isDrawioReady,
|
|
||||||
onDrawioLoad,
|
|
||||||
resetDrawioReady,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DiagramContext.Provider>
|
</DiagramContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDiagram() {
|
export function useDiagram() {
|
||||||
const context = useContext(DiagramContext)
|
const context = useContext(DiagramContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useDiagram must be used within a DiagramProvider")
|
throw new Error("useDiagram must be used within a DiagramProvider");
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
services:
|
|
||||||
drawio:
|
|
||||||
image: jgraph/drawio:latest
|
|
||||||
ports: ["8080:8080"]
|
|
||||||
next-ai-draw-io:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
|
||||||
ports: ["3000:3000"]
|
|
||||||
env_file: .env
|
|
||||||
depends_on: [drawio]
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
# AI Provider Configuration
|
|
||||||
|
|
||||||
This guide explains how to configure different AI model providers for next-ai-draw-io.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
1. Copy `.env.example` to `.env.local`
|
|
||||||
2. Set your API key for your chosen provider
|
|
||||||
3. Set `AI_MODEL` to your desired model
|
|
||||||
4. Run `npm run dev`
|
|
||||||
|
|
||||||
## Supported Providers
|
|
||||||
|
|
||||||
### Google Gemini
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
|
|
||||||
AI_MODEL=gemini-2.0-flash
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOOGLE_BASE_URL=https://your-custom-endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenAI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OPENAI_API_KEY=your_api_key
|
|
||||||
AI_MODEL=gpt-4o
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint (for OpenAI-compatible services):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Anthropic
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ANTHROPIC_API_KEY=your_api_key
|
|
||||||
AI_MODEL=claude-sonnet-4-5-20250514
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ANTHROPIC_BASE_URL=https://your-custom-endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
### DeepSeek
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DEEPSEEK_API_KEY=your_api_key
|
|
||||||
AI_MODEL=deepseek-chat
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DEEPSEEK_BASE_URL=https://your-custom-endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
### SiliconFlow (OpenAI-compatible)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SILICONFLOW_API_KEY=your_api_key
|
|
||||||
AI_MODEL=deepseek-ai/DeepSeek-V3 # example; use any SiliconFlow model id
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint (defaults to the recommended domain):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflow.cn/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Azure OpenAI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AZURE_API_KEY=your_api_key
|
|
||||||
AI_MODEL=your-deployment-name
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AZURE_BASE_URL=https://your-resource.openai.azure.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### AWS Bedrock
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AWS_REGION=us-west-2
|
|
||||||
AWS_ACCESS_KEY_ID=your_access_key_id
|
|
||||||
AWS_SECRET_ACCESS_KEY=your_secret_access_key
|
|
||||||
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: On AWS (Lambda, EC2 with IAM role), credentials are automatically obtained from the IAM role.
|
|
||||||
|
|
||||||
### OpenRouter
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OPENROUTER_API_KEY=your_api_key
|
|
||||||
AI_MODEL=anthropic/claude-sonnet-4
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OPENROUTER_BASE_URL=https://your-custom-endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ollama (Local)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AI_PROVIDER=ollama
|
|
||||||
AI_MODEL=llama3.2
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom URL:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OLLAMA_BASE_URL=http://localhost:11434
|
|
||||||
```
|
|
||||||
|
|
||||||
## Auto-Detection
|
|
||||||
|
|
||||||
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
|
|
||||||
|
|
||||||
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, azure, bedrock, openrouter, ollama
|
|
||||||
```
|
|
||||||
|
|
||||||
## Model Capability Requirements
|
|
||||||
|
|
||||||
This task requires exceptionally strong model capabilities, as it involves generating long-form text with strict formatting constraints (draw.io XML).
|
|
||||||
|
|
||||||
**Recommended models**:
|
|
||||||
|
|
||||||
- Claude Sonnet 4.5 / Opus 4.5
|
|
||||||
|
|
||||||
**Note on Ollama**: While Ollama is supported as a provider, it's generally not practical for this use case unless you're running high-capability models like DeepSeek R1 or Qwen3-235B locally.
|
|
||||||
|
|
||||||
## Temperature Setting
|
|
||||||
|
|
||||||
You can optionally configure the temperature via environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
TEMPERATURE=0 # More deterministic output (recommended for diagrams)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Leave `TEMPERATURE` unset for models that don't support temperature settings, such as:
|
|
||||||
- GPT-5.1 and other reasoning models
|
|
||||||
- Some specialized models
|
|
||||||
|
|
||||||
When unset, the model uses its default behavior.
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
- **Best experience**: Use models with vision support (GPT-4o, Claude, Gemini) for image-to-diagram features
|
|
||||||
- **Budget-friendly**: DeepSeek offers competitive pricing
|
|
||||||
- **Privacy**: Use Ollama for fully local, offline operation (requires powerful hardware)
|
|
||||||
- **Flexibility**: OpenRouter provides access to many models through a single API
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Offline Deployment
|
|
||||||
|
|
||||||
Deploy Next AI Draw.io offline by self-hosting draw.io to replace `embed.diagrams.net`.
|
|
||||||
|
|
||||||
**Note:** `NEXT_PUBLIC_DRAWIO_BASE_URL` is a **build-time** variable. Changing it requires rebuilding the Docker image.
|
|
||||||
|
|
||||||
## Docker Compose Setup
|
|
||||||
|
|
||||||
1. Clone the repository and define API keys in `.env`.
|
|
||||||
2. Create `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
drawio:
|
|
||||||
image: jgraph/drawio:latest
|
|
||||||
ports: ["8080:8080"]
|
|
||||||
next-ai-draw-io:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
|
||||||
ports: ["3000:3000"]
|
|
||||||
env_file: .env
|
|
||||||
depends_on: [drawio]
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run `docker compose up -d` and open `http://localhost:3000`.
|
|
||||||
|
|
||||||
## Configuration & Critical Warning
|
|
||||||
|
|
||||||
**The `NEXT_PUBLIC_DRAWIO_BASE_URL` must be accessible from the user's browser.**
|
|
||||||
|
|
||||||
| Scenario | URL Value |
|
|
||||||
|----------|-----------|
|
|
||||||
| Localhost | `http://localhost:8080` |
|
|
||||||
| Remote/Server | `http://YOUR_SERVER_IP:8080` or `https://drawio.your-domain.com` |
|
|
||||||
|
|
||||||
**Do NOT use** internal Docker aliases like `http://drawio:8080`; the browser cannot resolve them.
|
|
||||||
|
|
||||||
54
env.example
54
env.example
@@ -1,6 +1,6 @@
|
|||||||
# AI Provider Configuration
|
# AI Provider Configuration
|
||||||
# AI_PROVIDER: Which provider to use
|
# AI_PROVIDER: Which provider to use
|
||||||
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow
|
# Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek
|
||||||
# Default: bedrock
|
# Default: bedrock
|
||||||
AI_PROVIDER=bedrock
|
AI_PROVIDER=bedrock
|
||||||
|
|
||||||
@@ -11,49 +11,28 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# AWS_REGION=us-east-1
|
# AWS_REGION=us-east-1
|
||||||
# AWS_ACCESS_KEY_ID=your-access-key-id
|
# AWS_ACCESS_KEY_ID=your-access-key-id
|
||||||
# AWS_SECRET_ACCESS_KEY=your-secret-access-key
|
# AWS_SECRET_ACCESS_KEY=your-secret-access-key
|
||||||
# Note: Claude and Nova models support reasoning/extended thinking
|
|
||||||
# BEDROCK_REASONING_BUDGET_TOKENS=12000 # Optional: Claude reasoning budget in tokens (1024-64000)
|
|
||||||
# BEDROCK_REASONING_EFFORT=medium # Optional: Nova reasoning effort (low/medium/high)
|
|
||||||
|
|
||||||
# OpenAI Configuration
|
# OpenAI Configuration
|
||||||
# OPENAI_API_KEY=sk-...
|
# OPENAI_API_KEY=sk-...
|
||||||
# OPENAI_BASE_URL=https://api.openai.com/v1 # Optional: Custom OpenAI-compatible endpoint
|
# OPENAI_BASE_URL=https://api.openai.com/v1 # Optional: Custom OpenAI-compatible endpoint
|
||||||
# OPENAI_ORGANIZATION=org-... # Optional
|
# OPENAI_ORGANIZATION=org-... # Optional
|
||||||
# OPENAI_PROJECT=proj_... # Optional
|
# OPENAI_PROJECT=proj_... # Optional
|
||||||
# Note: o1/o3/gpt-5 models automatically enable reasoning summary (default: detailed)
|
|
||||||
# OPENAI_REASONING_EFFORT=low # Optional: Reasoning effort (minimal/low/medium/high) - for o1/o3/gpt-5
|
|
||||||
# OPENAI_REASONING_SUMMARY=detailed # Optional: Override reasoning summary (none/brief/detailed)
|
|
||||||
|
|
||||||
# Anthropic (Direct) Configuration
|
# Anthropic (Direct) Configuration
|
||||||
# ANTHROPIC_API_KEY=sk-ant-...
|
# ANTHROPIC_API_KEY=sk-ant-...
|
||||||
# ANTHROPIC_BASE_URL=https://your-custom-anthropic/v1
|
# ANTHROPIC_BASE_URL=https://your-custom-anthropic/v1
|
||||||
# ANTHROPIC_THINKING_TYPE=enabled # Optional: Anthropic extended thinking (enabled)
|
|
||||||
# ANTHROPIC_THINKING_BUDGET_TOKENS=12000 # Optional: Budget for extended thinking in tokens
|
|
||||||
|
|
||||||
# Google Generative AI Configuration
|
# Google Generative AI Configuration
|
||||||
# GOOGLE_GENERATIVE_AI_API_KEY=...
|
# GOOGLE_GENERATIVE_AI_API_KEY=...
|
||||||
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta # Optional: Custom endpoint
|
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta # Optional: Custom endpoint
|
||||||
# GOOGLE_CANDIDATE_COUNT=1 # Optional: Number of candidates to generate
|
|
||||||
# GOOGLE_TOP_K=40 # Optional: Top K sampling parameter
|
|
||||||
# GOOGLE_TOP_P=0.95 # Optional: Nucleus sampling parameter
|
|
||||||
# Note: Gemini 2.5/3 models automatically enable reasoning display (includeThoughts: true)
|
|
||||||
# GOOGLE_THINKING_BUDGET=8192 # Optional: Gemini 2.5 thinking budget in tokens (for more/less thinking)
|
|
||||||
# GOOGLE_THINKING_LEVEL=high # Optional: Gemini 3 thinking level (low/high)
|
|
||||||
|
|
||||||
# Azure OpenAI Configuration
|
# Azure OpenAI Configuration
|
||||||
# Configure endpoint using ONE of these methods:
|
|
||||||
# 1. AZURE_RESOURCE_NAME - SDK constructs: https://{name}.openai.azure.com/openai/v1{path}
|
|
||||||
# 2. AZURE_BASE_URL - SDK appends /v1{path} to your URL
|
|
||||||
# If both are set, AZURE_BASE_URL takes precedence.
|
|
||||||
# AZURE_RESOURCE_NAME=your-resource-name
|
# AZURE_RESOURCE_NAME=your-resource-name
|
||||||
# AZURE_API_KEY=...
|
# AZURE_API_KEY=...
|
||||||
# AZURE_BASE_URL=https://your-resource.openai.azure.com/openai # Alternative: Custom endpoint
|
# AZURE_BASE_URL=https://your-resource.openai.azure.com # Optional: Custom endpoint (overrides resourceName)
|
||||||
# AZURE_REASONING_EFFORT=low # Optional: Azure reasoning effort (low, medium, high)
|
|
||||||
# AZURE_REASONING_SUMMARY=detailed
|
|
||||||
|
|
||||||
# Ollama (Local) Configuration
|
# Ollama (Local) Configuration
|
||||||
# OLLAMA_BASE_URL=http://localhost:11434/api # Optional, defaults to localhost
|
# OLLAMA_BASE_URL=http://localhost:11434/api # Optional, defaults to localhost
|
||||||
# OLLAMA_ENABLE_THINKING=true # Optional: Enable thinking for models that support it (e.g., qwen3)
|
|
||||||
|
|
||||||
# OpenRouter Configuration
|
# OpenRouter Configuration
|
||||||
# OPENROUTER_API_KEY=sk-or-v1-...
|
# OPENROUTER_API_KEY=sk-or-v1-...
|
||||||
@@ -62,32 +41,3 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# DeepSeek Configuration
|
# DeepSeek Configuration
|
||||||
# DEEPSEEK_API_KEY=sk-...
|
# DEEPSEEK_API_KEY=sk-...
|
||||||
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
|
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
|
||||||
|
|
||||||
# SiliconFlow Configuration (OpenAI-compatible)
|
|
||||||
# Base domain can be .com or .cn, defaults to https://api.siliconflow.com/v1
|
|
||||||
# SILICONFLOW_API_KEY=sk-...
|
|
||||||
# SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # Optional: switch to https://api.siliconflow.cn/v1 if needed
|
|
||||||
|
|
||||||
# Langfuse Observability (Optional)
|
|
||||||
# Enable LLM tracing and analytics - https://langfuse.com
|
|
||||||
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
|
||||||
# LANGFUSE_SECRET_KEY=sk-lf-...
|
|
||||||
# LANGFUSE_BASEURL=https://cloud.langfuse.com # EU region, use https://us.cloud.langfuse.com for US
|
|
||||||
|
|
||||||
# Temperature (Optional)
|
|
||||||
# Controls randomness in AI responses. Lower = more deterministic.
|
|
||||||
# Leave unset for models that don't support temperature (e.g., GPT-5.1 reasoning models)
|
|
||||||
# TEMPERATURE=0
|
|
||||||
|
|
||||||
# Access Control (Optional)
|
|
||||||
# ACCESS_CODE_LIST=your-secret-code,another-code
|
|
||||||
|
|
||||||
# Draw.io Configuration (Optional)
|
|
||||||
# NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net # Default: https://embed.diagrams.net
|
|
||||||
# Use this to point to a self-hosted draw.io instance
|
|
||||||
|
|
||||||
# PDF Input Feature (Optional)
|
|
||||||
# Enable PDF file upload to extract text and generate diagrams
|
|
||||||
# Enabled by default. Set to "false" to disable.
|
|
||||||
# ENABLE_PDF_INPUT=true
|
|
||||||
# NEXT_PUBLIC_MAX_EXTRACTED_CHARS=150000 # Max characters for PDF/text extraction (default: 150000)
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { LangfuseSpanProcessor } from "@langfuse/otel"
|
|
||||||
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
|
|
||||||
|
|
||||||
export function register() {
|
|
||||||
// Skip telemetry if Langfuse env vars are not configured
|
|
||||||
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
|
||||||
console.warn(
|
|
||||||
"[Langfuse] Environment variables not configured - telemetry disabled",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const langfuseSpanProcessor = new LangfuseSpanProcessor({
|
|
||||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
||||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
||||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
|
||||||
// Filter out Next.js HTTP request spans so AI SDK spans become root traces
|
|
||||||
shouldExportSpan: ({ otelSpan }) => {
|
|
||||||
const spanName = otelSpan.name
|
|
||||||
// Skip Next.js HTTP infrastructure spans
|
|
||||||
if (
|
|
||||||
spanName.startsWith("POST /") ||
|
|
||||||
spanName.startsWith("GET /") ||
|
|
||||||
spanName.includes("BaseServer") ||
|
|
||||||
spanName.includes("handleRequest")
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const tracerProvider = new NodeTracerProvider({
|
|
||||||
spanProcessors: [langfuseSpanProcessor],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register globally so AI SDK's telemetry also uses this processor
|
|
||||||
tracerProvider.register()
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { STORAGE_KEYS } from "./storage"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get AI configuration from localStorage.
|
|
||||||
* Returns API keys and settings for custom AI providers.
|
|
||||||
* Used to override server defaults when user provides their own API key.
|
|
||||||
*/
|
|
||||||
export function getAIConfig() {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return {
|
|
||||||
accessCode: "",
|
|
||||||
aiProvider: "",
|
|
||||||
aiBaseUrl: "",
|
|
||||||
aiApiKey: "",
|
|
||||||
aiModel: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessCode: localStorage.getItem(STORAGE_KEYS.accessCode) || "",
|
|
||||||
aiProvider: localStorage.getItem(STORAGE_KEYS.aiProvider) || "",
|
|
||||||
aiBaseUrl: localStorage.getItem(STORAGE_KEYS.aiBaseUrl) || "",
|
|
||||||
aiApiKey: localStorage.getItem(STORAGE_KEYS.aiApiKey) || "",
|
|
||||||
aiModel: localStorage.getItem(STORAGE_KEYS.aiModel) || "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,405 +1,96 @@
|
|||||||
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
||||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
||||||
import { azure, createAzure } from "@ai-sdk/azure"
|
import { openai, createOpenAI } from '@ai-sdk/openai';
|
||||||
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||||
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||||
import { createOpenAI, openai } from "@ai-sdk/openai"
|
import { azure, createAzure } from '@ai-sdk/azure';
|
||||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
import { ollama, createOllama } from 'ollama-ai-provider-v2';
|
||||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
||||||
import { createOllama, ollama } from "ollama-ai-provider-v2"
|
import { deepseek, createDeepSeek } from '@ai-sdk/deepseek';
|
||||||
|
|
||||||
export type ProviderName =
|
export type ProviderName =
|
||||||
| "bedrock"
|
| 'bedrock'
|
||||||
| "openai"
|
| 'openai'
|
||||||
| "anthropic"
|
| 'anthropic'
|
||||||
| "google"
|
| 'google'
|
||||||
| "azure"
|
| 'azure'
|
||||||
| "ollama"
|
| 'ollama'
|
||||||
| "openrouter"
|
| 'openrouter'
|
||||||
| "deepseek"
|
| 'deepseek';
|
||||||
| "siliconflow"
|
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
model: any
|
model: any;
|
||||||
providerOptions?: any
|
providerOptions?: any;
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>;
|
||||||
modelId: string
|
modelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientOverrides {
|
|
||||||
provider?: string | null
|
|
||||||
baseUrl?: string | null
|
|
||||||
apiKey?: string | null
|
|
||||||
modelId?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Providers that can be used with client-provided API keys
|
|
||||||
const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|
||||||
"openai",
|
|
||||||
"anthropic",
|
|
||||||
"google",
|
|
||||||
"azure",
|
|
||||||
"openrouter",
|
|
||||||
"deepseek",
|
|
||||||
"siliconflow",
|
|
||||||
]
|
|
||||||
|
|
||||||
// Bedrock provider options for Anthropic beta features
|
// Bedrock provider options for Anthropic beta features
|
||||||
const BEDROCK_ANTHROPIC_BETA = {
|
const BEDROCK_ANTHROPIC_BETA = {
|
||||||
bedrock: {
|
bedrock: {
|
||||||
anthropicBeta: ["fine-grained-tool-streaming-2025-05-14"],
|
anthropicBeta: ['fine-grained-tool-streaming-2025-05-14'],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// Direct Anthropic API headers for beta features
|
// Direct Anthropic API headers for beta features
|
||||||
const ANTHROPIC_BETA_HEADERS = {
|
const ANTHROPIC_BETA_HEADERS = {
|
||||||
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
|
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely parse integer from environment variable with validation
|
|
||||||
*/
|
|
||||||
function parseIntSafe(
|
|
||||||
value: string | undefined,
|
|
||||||
varName: string,
|
|
||||||
min?: number,
|
|
||||||
max?: number,
|
|
||||||
): number | undefined {
|
|
||||||
if (!value) return undefined
|
|
||||||
const parsed = Number.parseInt(value, 10)
|
|
||||||
if (Number.isNaN(parsed)) {
|
|
||||||
throw new Error(`${varName} must be a valid integer, got: ${value}`)
|
|
||||||
}
|
|
||||||
if (min !== undefined && parsed < min) {
|
|
||||||
throw new Error(`${varName} must be >= ${min}, got: ${parsed}`)
|
|
||||||
}
|
|
||||||
if (max !== undefined && parsed > max) {
|
|
||||||
throw new Error(`${varName} must be <= ${max}, got: ${parsed}`)
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build provider-specific options from environment variables
|
|
||||||
* Supports various AI SDK providers with their unique configuration options
|
|
||||||
*
|
|
||||||
* Environment variables:
|
|
||||||
* - OPENAI_REASONING_EFFORT: OpenAI reasoning effort level (minimal/low/medium/high) - for o1/o3/gpt-5
|
|
||||||
* - OPENAI_REASONING_SUMMARY: OpenAI reasoning summary (none/brief/detailed) - auto-enabled for o1/o3/gpt-5
|
|
||||||
* - ANTHROPIC_THINKING_BUDGET_TOKENS: Anthropic thinking budget in tokens (1024-64000)
|
|
||||||
* - ANTHROPIC_THINKING_TYPE: Anthropic thinking type (enabled)
|
|
||||||
* - GOOGLE_THINKING_BUDGET: Google Gemini 2.5 thinking budget in tokens (1024-100000)
|
|
||||||
* - GOOGLE_THINKING_LEVEL: Google Gemini 3 thinking level (low/high)
|
|
||||||
* - AZURE_REASONING_EFFORT: Azure/OpenAI reasoning effort (low/medium/high)
|
|
||||||
* - AZURE_REASONING_SUMMARY: Azure reasoning summary (none/brief/detailed)
|
|
||||||
* - BEDROCK_REASONING_BUDGET_TOKENS: Bedrock Claude reasoning budget in tokens (1024-64000)
|
|
||||||
* - BEDROCK_REASONING_EFFORT: Bedrock Nova reasoning effort (low/medium/high)
|
|
||||||
* - OLLAMA_ENABLE_THINKING: Enable Ollama thinking mode (set to "true")
|
|
||||||
*/
|
|
||||||
function buildProviderOptions(
|
|
||||||
provider: ProviderName,
|
|
||||||
modelId?: string,
|
|
||||||
): Record<string, any> | undefined {
|
|
||||||
const options: Record<string, any> = {}
|
|
||||||
|
|
||||||
switch (provider) {
|
|
||||||
case "openai": {
|
|
||||||
const reasoningEffort = process.env.OPENAI_REASONING_EFFORT
|
|
||||||
const reasoningSummary = process.env.OPENAI_REASONING_SUMMARY
|
|
||||||
|
|
||||||
// OpenAI reasoning models (o1, o3, gpt-5) need reasoningSummary to return thoughts
|
|
||||||
if (
|
|
||||||
modelId &&
|
|
||||||
(modelId.includes("o1") ||
|
|
||||||
modelId.includes("o3") ||
|
|
||||||
modelId.includes("gpt-5"))
|
|
||||||
) {
|
|
||||||
options.openai = {
|
|
||||||
// Auto-enable reasoning summary for reasoning models (default: detailed)
|
|
||||||
reasoningSummary:
|
|
||||||
(reasoningSummary as "none" | "brief" | "detailed") ||
|
|
||||||
"detailed",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally configure reasoning effort
|
|
||||||
if (reasoningEffort) {
|
|
||||||
options.openai.reasoningEffort = reasoningEffort as
|
|
||||||
| "minimal"
|
|
||||||
| "low"
|
|
||||||
| "medium"
|
|
||||||
| "high"
|
|
||||||
}
|
|
||||||
} else if (reasoningEffort || reasoningSummary) {
|
|
||||||
// Non-reasoning models: only apply if explicitly configured
|
|
||||||
options.openai = {}
|
|
||||||
if (reasoningEffort) {
|
|
||||||
options.openai.reasoningEffort = reasoningEffort as
|
|
||||||
| "minimal"
|
|
||||||
| "low"
|
|
||||||
| "medium"
|
|
||||||
| "high"
|
|
||||||
}
|
|
||||||
if (reasoningSummary) {
|
|
||||||
options.openai.reasoningSummary = reasoningSummary as
|
|
||||||
| "none"
|
|
||||||
| "brief"
|
|
||||||
| "detailed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "anthropic": {
|
|
||||||
const thinkingBudget = parseIntSafe(
|
|
||||||
process.env.ANTHROPIC_THINKING_BUDGET_TOKENS,
|
|
||||||
"ANTHROPIC_THINKING_BUDGET_TOKENS",
|
|
||||||
1024,
|
|
||||||
64000,
|
|
||||||
)
|
|
||||||
const thinkingType =
|
|
||||||
process.env.ANTHROPIC_THINKING_TYPE || "enabled"
|
|
||||||
|
|
||||||
if (thinkingBudget) {
|
|
||||||
options.anthropic = {
|
|
||||||
thinking: {
|
|
||||||
type: thinkingType,
|
|
||||||
budgetTokens: thinkingBudget,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "google": {
|
|
||||||
const reasoningEffort = process.env.GOOGLE_REASONING_EFFORT
|
|
||||||
const thinkingBudgetVal = parseIntSafe(
|
|
||||||
process.env.GOOGLE_THINKING_BUDGET,
|
|
||||||
"GOOGLE_THINKING_BUDGET",
|
|
||||||
1024,
|
|
||||||
100000,
|
|
||||||
)
|
|
||||||
const thinkingLevel = process.env.GOOGLE_THINKING_LEVEL
|
|
||||||
|
|
||||||
// Google Gemini 2.5/3 models think by default, but need includeThoughts: true
|
|
||||||
// to return the reasoning in the response
|
|
||||||
if (
|
|
||||||
modelId &&
|
|
||||||
(modelId.includes("gemini-2") ||
|
|
||||||
modelId.includes("gemini-3") ||
|
|
||||||
modelId.includes("gemini2") ||
|
|
||||||
modelId.includes("gemini3"))
|
|
||||||
) {
|
|
||||||
const thinkingConfig: Record<string, any> = {
|
|
||||||
includeThoughts: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally configure thinking budget or level
|
|
||||||
if (
|
|
||||||
thinkingBudgetVal &&
|
|
||||||
(modelId.includes("2.5") || modelId.includes("2-5"))
|
|
||||||
) {
|
|
||||||
thinkingConfig.thinkingBudget = thinkingBudgetVal
|
|
||||||
} else if (
|
|
||||||
thinkingLevel &&
|
|
||||||
(modelId.includes("gemini-3") ||
|
|
||||||
modelId.includes("gemini3"))
|
|
||||||
) {
|
|
||||||
thinkingConfig.thinkingLevel = thinkingLevel as
|
|
||||||
| "low"
|
|
||||||
| "high"
|
|
||||||
}
|
|
||||||
|
|
||||||
options.google = { thinkingConfig }
|
|
||||||
} else if (reasoningEffort) {
|
|
||||||
options.google = {
|
|
||||||
reasoningEffort: reasoningEffort as
|
|
||||||
| "low"
|
|
||||||
| "medium"
|
|
||||||
| "high",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep existing Google options
|
|
||||||
const options_obj: Record<string, any> = {}
|
|
||||||
const candidateCount = parseIntSafe(
|
|
||||||
process.env.GOOGLE_CANDIDATE_COUNT,
|
|
||||||
"GOOGLE_CANDIDATE_COUNT",
|
|
||||||
1,
|
|
||||||
8,
|
|
||||||
)
|
|
||||||
if (candidateCount) {
|
|
||||||
options_obj.candidateCount = candidateCount
|
|
||||||
}
|
|
||||||
const topK = parseIntSafe(
|
|
||||||
process.env.GOOGLE_TOP_K,
|
|
||||||
"GOOGLE_TOP_K",
|
|
||||||
1,
|
|
||||||
100,
|
|
||||||
)
|
|
||||||
if (topK) {
|
|
||||||
options_obj.topK = topK
|
|
||||||
}
|
|
||||||
if (process.env.GOOGLE_TOP_P) {
|
|
||||||
const topP = Number.parseFloat(process.env.GOOGLE_TOP_P)
|
|
||||||
if (Number.isNaN(topP) || topP < 0 || topP > 1) {
|
|
||||||
throw new Error(
|
|
||||||
`GOOGLE_TOP_P must be a number between 0 and 1, got: ${process.env.GOOGLE_TOP_P}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options_obj.topP = topP
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(options_obj).length > 0) {
|
|
||||||
options.google = { ...options.google, ...options_obj }
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "azure": {
|
|
||||||
const reasoningEffort = process.env.AZURE_REASONING_EFFORT
|
|
||||||
const reasoningSummary = process.env.AZURE_REASONING_SUMMARY
|
|
||||||
|
|
||||||
if (reasoningEffort || reasoningSummary) {
|
|
||||||
options.azure = {}
|
|
||||||
if (reasoningEffort) {
|
|
||||||
options.azure.reasoningEffort = reasoningEffort as
|
|
||||||
| "low"
|
|
||||||
| "medium"
|
|
||||||
| "high"
|
|
||||||
}
|
|
||||||
if (reasoningSummary) {
|
|
||||||
options.azure.reasoningSummary = reasoningSummary as
|
|
||||||
| "none"
|
|
||||||
| "brief"
|
|
||||||
| "detailed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "bedrock": {
|
|
||||||
const budgetTokens = parseIntSafe(
|
|
||||||
process.env.BEDROCK_REASONING_BUDGET_TOKENS,
|
|
||||||
"BEDROCK_REASONING_BUDGET_TOKENS",
|
|
||||||
1024,
|
|
||||||
64000,
|
|
||||||
)
|
|
||||||
const reasoningEffort = process.env.BEDROCK_REASONING_EFFORT
|
|
||||||
|
|
||||||
// Bedrock reasoning ONLY for Claude and Nova models
|
|
||||||
// Other models (MiniMax, etc.) don't support reasoningConfig
|
|
||||||
if (
|
|
||||||
modelId &&
|
|
||||||
(budgetTokens || reasoningEffort) &&
|
|
||||||
(modelId.includes("claude") ||
|
|
||||||
modelId.includes("anthropic") ||
|
|
||||||
modelId.includes("nova") ||
|
|
||||||
modelId.includes("amazon"))
|
|
||||||
) {
|
|
||||||
const reasoningConfig: Record<string, any> = { type: "enabled" }
|
|
||||||
|
|
||||||
// Claude models: use budgetTokens (1024-64000)
|
|
||||||
if (
|
|
||||||
budgetTokens &&
|
|
||||||
(modelId.includes("claude") ||
|
|
||||||
modelId.includes("anthropic"))
|
|
||||||
) {
|
|
||||||
reasoningConfig.budgetTokens = budgetTokens
|
|
||||||
}
|
|
||||||
// Nova models: use maxReasoningEffort (low/medium/high)
|
|
||||||
else if (
|
|
||||||
reasoningEffort &&
|
|
||||||
(modelId.includes("nova") || modelId.includes("amazon"))
|
|
||||||
) {
|
|
||||||
reasoningConfig.maxReasoningEffort = reasoningEffort as
|
|
||||||
| "low"
|
|
||||||
| "medium"
|
|
||||||
| "high"
|
|
||||||
}
|
|
||||||
|
|
||||||
options.bedrock = { reasoningConfig }
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ollama": {
|
|
||||||
const enableThinking = process.env.OLLAMA_ENABLE_THINKING
|
|
||||||
// Ollama supports reasoning with think: true for models like qwen3
|
|
||||||
if (enableThinking === "true") {
|
|
||||||
options.ollama = { think: true }
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "deepseek":
|
|
||||||
case "openrouter":
|
|
||||||
case "siliconflow": {
|
|
||||||
// These providers don't have reasoning configs in AI SDK yet
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(options).length > 0 ? options : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of provider to required environment variable
|
// Map of provider to required environment variable
|
||||||
const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
||||||
bedrock: null, // AWS SDK auto-uses IAM role on AWS, or env vars locally
|
bedrock: null, // AWS SDK auto-uses IAM role on AWS, or env vars locally
|
||||||
openai: "OPENAI_API_KEY",
|
openai: 'OPENAI_API_KEY',
|
||||||
anthropic: "ANTHROPIC_API_KEY",
|
anthropic: 'ANTHROPIC_API_KEY',
|
||||||
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
google: 'GOOGLE_GENERATIVE_AI_API_KEY',
|
||||||
azure: "AZURE_API_KEY",
|
azure: 'AZURE_API_KEY',
|
||||||
ollama: null, // No credentials needed for local Ollama
|
ollama: null, // No credentials needed for local Ollama
|
||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: 'OPENROUTER_API_KEY',
|
||||||
deepseek: "DEEPSEEK_API_KEY",
|
deepseek: 'DEEPSEEK_API_KEY',
|
||||||
siliconflow: "SILICONFLOW_API_KEY",
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-detect provider based on available API keys
|
* Auto-detect provider based on available API keys
|
||||||
* Returns the provider if exactly one is configured, otherwise null
|
* Returns the provider if exactly one is configured, otherwise null
|
||||||
*/
|
*/
|
||||||
function detectProvider(): ProviderName | null {
|
function detectProvider(): ProviderName | null {
|
||||||
const configuredProviders: ProviderName[] = []
|
const configuredProviders: ProviderName[] = [];
|
||||||
|
|
||||||
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
|
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
|
||||||
if (envVar === null) {
|
if (envVar === null) {
|
||||||
// Skip ollama - it doesn't require credentials
|
// Skip ollama - it doesn't require credentials
|
||||||
continue
|
continue;
|
||||||
}
|
|
||||||
if (process.env[envVar]) {
|
|
||||||
configuredProviders.push(provider as ProviderName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (process.env[envVar]) {
|
||||||
if (configuredProviders.length === 1) {
|
configuredProviders.push(provider as ProviderName);
|
||||||
return configuredProviders[0]
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
if (configuredProviders.length === 1) {
|
||||||
|
return configuredProviders[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that required API keys are present for the selected provider
|
* Validate that required API keys are present for the selected provider
|
||||||
*/
|
*/
|
||||||
function validateProviderCredentials(provider: ProviderName): void {
|
function validateProviderCredentials(provider: ProviderName): void {
|
||||||
const requiredVar = PROVIDER_ENV_VARS[provider]
|
const requiredVar = PROVIDER_ENV_VARS[provider];
|
||||||
if (requiredVar && !process.env[requiredVar]) {
|
if (requiredVar && !process.env[requiredVar]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${requiredVar} environment variable is required for ${provider} provider. ` +
|
`${requiredVar} environment variable is required for ${provider} provider. ` +
|
||||||
`Please set it in your .env.local file.`,
|
`Please set it in your .env.local file.`
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the AI model based on environment variables
|
* Get the AI model based on environment variables
|
||||||
*
|
*
|
||||||
* Environment variables:
|
* Environment variables:
|
||||||
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
|
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||||
* - AI_MODEL: The model ID/name for the selected provider
|
* - AI_MODEL: The model ID/name for the selected provider
|
||||||
*
|
*
|
||||||
* Provider-specific env vars:
|
* Provider-specific env vars:
|
||||||
@@ -413,258 +104,160 @@ function validateProviderCredentials(provider: ProviderName): void {
|
|||||||
* - OPENROUTER_API_KEY: OpenRouter API key
|
* - OPENROUTER_API_KEY: OpenRouter API key
|
||||||
* - DEEPSEEK_API_KEY: DeepSeek API key
|
* - DEEPSEEK_API_KEY: DeepSeek API key
|
||||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||||
* - SILICONFLOW_API_KEY: SiliconFlow API key
|
|
||||||
* - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
|
|
||||||
*/
|
*/
|
||||||
export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
export function getAIModel(): ModelConfig {
|
||||||
// Check if client is providing their own provider override
|
const modelId = process.env.AI_MODEL;
|
||||||
const isClientOverride = !!(overrides?.provider && overrides?.apiKey)
|
|
||||||
|
|
||||||
// Use client override if provided, otherwise fall back to env vars
|
if (!modelId) {
|
||||||
const modelId = overrides?.modelId || process.env.AI_MODEL
|
throw new Error(
|
||||||
|
`AI_MODEL environment variable is required. Example: AI_MODEL=claude-sonnet-4-5`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!modelId) {
|
// Determine provider: explicit config > auto-detect > error
|
||||||
if (isClientOverride) {
|
let provider: ProviderName;
|
||||||
throw new Error(
|
if (process.env.AI_PROVIDER) {
|
||||||
`Model ID is required when using custom AI provider. Please specify a model in Settings.`,
|
provider = process.env.AI_PROVIDER as ProviderName;
|
||||||
)
|
} else {
|
||||||
}
|
const detected = detectProvider();
|
||||||
throw new Error(
|
if (detected) {
|
||||||
`AI_MODEL environment variable is required. Example: AI_MODEL=claude-sonnet-4-5`,
|
provider = detected;
|
||||||
)
|
console.log(`[AI Provider] Auto-detected provider: ${provider}`);
|
||||||
}
|
|
||||||
|
|
||||||
// Determine provider: client override > explicit config > auto-detect > error
|
|
||||||
let provider: ProviderName
|
|
||||||
if (overrides?.provider) {
|
|
||||||
// Validate client-provided provider
|
|
||||||
if (
|
|
||||||
!ALLOWED_CLIENT_PROVIDERS.includes(
|
|
||||||
overrides.provider as ProviderName,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid provider: ${overrides.provider}. Allowed providers: ${ALLOWED_CLIENT_PROVIDERS.join(", ")}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
provider = overrides.provider as ProviderName
|
|
||||||
} else if (process.env.AI_PROVIDER) {
|
|
||||||
provider = process.env.AI_PROVIDER as ProviderName
|
|
||||||
} else {
|
} else {
|
||||||
const detected = detectProvider()
|
// List configured providers for better error message
|
||||||
if (detected) {
|
const configured = Object.entries(PROVIDER_ENV_VARS)
|
||||||
provider = detected
|
.filter(([, envVar]) => envVar && process.env[envVar as string])
|
||||||
console.log(`[AI Provider] Auto-detected provider: ${provider}`)
|
.map(([p]) => p);
|
||||||
} else {
|
|
||||||
// List configured providers for better error message
|
|
||||||
const configured = Object.entries(PROVIDER_ENV_VARS)
|
|
||||||
.filter(([, envVar]) => envVar && process.env[envVar as string])
|
|
||||||
.map(([p]) => p)
|
|
||||||
|
|
||||||
if (configured.length === 0) {
|
if (configured.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
||||||
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
||||||
`- OPENAI_API_KEY for OpenAI\n` +
|
`- OPENAI_API_KEY for OpenAI\n` +
|
||||||
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
||||||
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
|
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
|
||||||
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
||||||
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
||||||
`- AZURE_API_KEY for Azure\n` +
|
`- AZURE_API_KEY for Azure\n` +
|
||||||
`- SILICONFLOW_API_KEY for SiliconFlow\n` +
|
`Or set AI_PROVIDER=ollama for local Ollama.`
|
||||||
`Or set AI_PROVIDER=ollama for local Ollama.`,
|
);
|
||||||
)
|
} else {
|
||||||
} else {
|
throw new Error(
|
||||||
throw new Error(
|
`Multiple AI providers configured (${configured.join(', ')}). ` +
|
||||||
`Multiple AI providers configured (${configured.join(", ")}). ` +
|
`Please set AI_PROVIDER to specify which one to use.`
|
||||||
`Please set AI_PROVIDER to specify which one to use.`,
|
);
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate provider credentials
|
||||||
|
validateProviderCredentials(provider);
|
||||||
|
|
||||||
|
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`);
|
||||||
|
|
||||||
|
let model: any;
|
||||||
|
let providerOptions: any = undefined;
|
||||||
|
let headers: Record<string, string> | undefined = undefined;
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case 'bedrock': {
|
||||||
|
// Use credential provider chain for IAM role support (Amplify, Lambda, etc.)
|
||||||
|
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
|
||||||
|
const bedrockProvider = createAmazonBedrock({
|
||||||
|
region: process.env.AWS_REGION || 'us-west-2',
|
||||||
|
credentialProvider: fromNodeProviderChain(),
|
||||||
|
});
|
||||||
|
model = bedrockProvider(modelId);
|
||||||
|
// Add Anthropic beta options if using Claude models via Bedrock
|
||||||
|
if (modelId.includes('anthropic.claude')) {
|
||||||
|
providerOptions = BEDROCK_ANTHROPIC_BETA;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only validate server credentials if client isn't providing their own API key
|
case 'openai':
|
||||||
if (!isClientOverride) {
|
if (process.env.OPENAI_BASE_URL) {
|
||||||
validateProviderCredentials(provider)
|
const customOpenAI = createOpenAI({
|
||||||
}
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
baseURL: process.env.OPENAI_BASE_URL,
|
||||||
|
});
|
||||||
|
model = customOpenAI.chat(modelId);
|
||||||
|
} else {
|
||||||
|
model = openai(modelId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`)
|
case 'anthropic':
|
||||||
|
const customProvider = createAnthropic({
|
||||||
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||||
|
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
|
||||||
|
headers: ANTHROPIC_BETA_HEADERS,
|
||||||
|
});
|
||||||
|
model = customProvider(modelId);
|
||||||
|
// Add beta headers for fine-grained tool streaming
|
||||||
|
headers = ANTHROPIC_BETA_HEADERS;
|
||||||
|
break;
|
||||||
|
|
||||||
let model: any
|
case 'google':
|
||||||
let providerOptions: any
|
if (process.env.GOOGLE_BASE_URL) {
|
||||||
let headers: Record<string, string> | undefined
|
const customGoogle = createGoogleGenerativeAI({
|
||||||
|
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
|
||||||
|
baseURL: process.env.GOOGLE_BASE_URL,
|
||||||
|
});
|
||||||
|
model = customGoogle(modelId);
|
||||||
|
} else {
|
||||||
|
model = google(modelId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Build provider-specific options from environment variables
|
case 'azure':
|
||||||
const customProviderOptions = buildProviderOptions(provider, modelId)
|
if (process.env.AZURE_BASE_URL) {
|
||||||
|
const customAzure = createAzure({
|
||||||
|
apiKey: process.env.AZURE_API_KEY,
|
||||||
|
baseURL: process.env.AZURE_BASE_URL,
|
||||||
|
});
|
||||||
|
model = customAzure(modelId);
|
||||||
|
} else {
|
||||||
|
model = azure(modelId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
switch (provider) {
|
case 'ollama':
|
||||||
case "bedrock": {
|
if (process.env.OLLAMA_BASE_URL) {
|
||||||
// Use credential provider chain for IAM role support (Lambda, EC2, etc.)
|
const customOllama = createOllama({
|
||||||
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
|
baseURL: process.env.OLLAMA_BASE_URL,
|
||||||
const bedrockProvider = createAmazonBedrock({
|
});
|
||||||
region: process.env.AWS_REGION || "us-west-2",
|
model = customOllama(modelId);
|
||||||
credentialProvider: fromNodeProviderChain(),
|
} else {
|
||||||
})
|
model = ollama(modelId);
|
||||||
model = bedrockProvider(modelId)
|
}
|
||||||
// Add Anthropic beta options if using Claude models via Bedrock
|
break;
|
||||||
if (modelId.includes("anthropic.claude")) {
|
|
||||||
// Deep merge to preserve both anthropicBeta and reasoningConfig
|
|
||||||
providerOptions = {
|
|
||||||
bedrock: {
|
|
||||||
...BEDROCK_ANTHROPIC_BETA.bedrock,
|
|
||||||
...(customProviderOptions?.bedrock || {}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else if (customProviderOptions) {
|
|
||||||
providerOptions = customProviderOptions
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "openai": {
|
case 'openrouter':
|
||||||
const apiKey = overrides?.apiKey || process.env.OPENAI_API_KEY
|
const openrouter = createOpenRouter({
|
||||||
const baseURL = overrides?.baseUrl || process.env.OPENAI_BASE_URL
|
apiKey: process.env.OPENROUTER_API_KEY,
|
||||||
if (baseURL || overrides?.apiKey) {
|
...(process.env.OPENROUTER_BASE_URL && { baseURL: process.env.OPENROUTER_BASE_URL }),
|
||||||
const customOpenAI = createOpenAI({
|
});
|
||||||
apiKey,
|
model = openrouter(modelId);
|
||||||
...(baseURL && { baseURL }),
|
break;
|
||||||
})
|
|
||||||
model = customOpenAI.chat(modelId)
|
|
||||||
} else {
|
|
||||||
model = openai(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "anthropic": {
|
case 'deepseek':
|
||||||
const apiKey = overrides?.apiKey || process.env.ANTHROPIC_API_KEY
|
if (process.env.DEEPSEEK_BASE_URL) {
|
||||||
const baseURL =
|
const customDeepSeek = createDeepSeek({
|
||||||
overrides?.baseUrl ||
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
process.env.ANTHROPIC_BASE_URL ||
|
baseURL: process.env.DEEPSEEK_BASE_URL,
|
||||||
"https://api.anthropic.com/v1"
|
});
|
||||||
const customProvider = createAnthropic({
|
model = customDeepSeek(modelId);
|
||||||
apiKey,
|
} else {
|
||||||
baseURL,
|
model = deepseek(modelId);
|
||||||
headers: ANTHROPIC_BETA_HEADERS,
|
}
|
||||||
})
|
break;
|
||||||
model = customProvider(modelId)
|
|
||||||
// Add beta headers for fine-grained tool streaming
|
|
||||||
headers = ANTHROPIC_BETA_HEADERS
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "google": {
|
default:
|
||||||
const apiKey =
|
throw new Error(
|
||||||
overrides?.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek`
|
||||||
const baseURL = overrides?.baseUrl || process.env.GOOGLE_BASE_URL
|
);
|
||||||
if (baseURL || overrides?.apiKey) {
|
}
|
||||||
const customGoogle = createGoogleGenerativeAI({
|
|
||||||
apiKey,
|
|
||||||
...(baseURL && { baseURL }),
|
|
||||||
})
|
|
||||||
model = customGoogle(modelId)
|
|
||||||
} else {
|
|
||||||
model = google(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "azure": {
|
return { model, providerOptions, headers, modelId };
|
||||||
const apiKey = overrides?.apiKey || process.env.AZURE_API_KEY
|
|
||||||
const baseURL = overrides?.baseUrl || process.env.AZURE_BASE_URL
|
|
||||||
const resourceName = process.env.AZURE_RESOURCE_NAME
|
|
||||||
// Azure requires either baseURL or resourceName to construct the endpoint
|
|
||||||
// resourceName constructs: https://{resourceName}.openai.azure.com/openai/v1{path}
|
|
||||||
if (baseURL || resourceName || overrides?.apiKey) {
|
|
||||||
const customAzure = createAzure({
|
|
||||||
apiKey,
|
|
||||||
// baseURL takes precedence over resourceName per SDK behavior
|
|
||||||
...(baseURL && { baseURL }),
|
|
||||||
...(!baseURL && resourceName && { resourceName }),
|
|
||||||
})
|
|
||||||
model = customAzure(modelId)
|
|
||||||
} else {
|
|
||||||
model = azure(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ollama":
|
|
||||||
if (process.env.OLLAMA_BASE_URL) {
|
|
||||||
const customOllama = createOllama({
|
|
||||||
baseURL: process.env.OLLAMA_BASE_URL,
|
|
||||||
})
|
|
||||||
model = customOllama(modelId)
|
|
||||||
} else {
|
|
||||||
model = ollama(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case "openrouter": {
|
|
||||||
const apiKey = overrides?.apiKey || process.env.OPENROUTER_API_KEY
|
|
||||||
const baseURL =
|
|
||||||
overrides?.baseUrl || process.env.OPENROUTER_BASE_URL
|
|
||||||
const openrouter = createOpenRouter({
|
|
||||||
apiKey,
|
|
||||||
...(baseURL && { baseURL }),
|
|
||||||
})
|
|
||||||
model = openrouter(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "deepseek": {
|
|
||||||
const apiKey = overrides?.apiKey || process.env.DEEPSEEK_API_KEY
|
|
||||||
const baseURL = overrides?.baseUrl || process.env.DEEPSEEK_BASE_URL
|
|
||||||
if (baseURL || overrides?.apiKey) {
|
|
||||||
const customDeepSeek = createDeepSeek({
|
|
||||||
apiKey,
|
|
||||||
...(baseURL && { baseURL }),
|
|
||||||
})
|
|
||||||
model = customDeepSeek(modelId)
|
|
||||||
} else {
|
|
||||||
model = deepseek(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "siliconflow": {
|
|
||||||
const apiKey = overrides?.apiKey || process.env.SILICONFLOW_API_KEY
|
|
||||||
const baseURL =
|
|
||||||
overrides?.baseUrl ||
|
|
||||||
process.env.SILICONFLOW_BASE_URL ||
|
|
||||||
"https://api.siliconflow.com/v1"
|
|
||||||
const siliconflowProvider = createOpenAI({
|
|
||||||
apiKey,
|
|
||||||
baseURL,
|
|
||||||
})
|
|
||||||
model = siliconflowProvider.chat(modelId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply provider-specific options for all providers except bedrock (which has special handling)
|
|
||||||
if (customProviderOptions && provider !== "bedrock" && !providerOptions) {
|
|
||||||
providerOptions = customProviderOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
return { model, providerOptions, headers, modelId }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a model supports prompt caching.
|
|
||||||
* Currently only Claude models on Bedrock support prompt caching.
|
|
||||||
*/
|
|
||||||
export function supportsPromptCaching(modelId: string): boolean {
|
|
||||||
// Bedrock prompt caching is supported for Claude models
|
|
||||||
return (
|
|
||||||
modelId.includes("claude") ||
|
|
||||||
modelId.includes("anthropic") ||
|
|
||||||
modelId.startsWith("us.anthropic") ||
|
|
||||||
modelId.startsWith("eu.anthropic")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
export interface CachedResponse {
|
export interface CachedResponse {
|
||||||
promptText: string
|
promptText: string;
|
||||||
hasImage: boolean
|
hasImage: boolean;
|
||||||
xml: string
|
xml: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
||||||
{
|
{
|
||||||
promptText:
|
promptText: "Give me a **animated connector** diagram of transformer's architecture",
|
||||||
"Give me a **animated connector** diagram of transformer's architecture",
|
hasImage: false,
|
||||||
hasImage: false,
|
xml: `<root>
|
||||||
xml: `<root>
|
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -256,11 +255,11 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
<mxGeometry x="660" y="530" width="100" height="30" as="geometry"/>
|
<mxGeometry x="660" y="530" width="100" height="30" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
promptText: "Replicate this in aws style",
|
promptText: "Replicate this in aws style",
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
xml: `<root>
|
xml: `<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -326,11 +325,11 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
promptText: "Replicate this flowchart.",
|
promptText: "Replicate this flowchart.",
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
xml: `<root>
|
xml: `<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -393,371 +392,11 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
<mxGeometry x="130" y="650" width="200" height="60" as="geometry"/>
|
<mxGeometry x="130" y="650" width="200" height="60" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
promptText: "Summarize this paper as a diagram",
|
promptText: "Draw a cat for me",
|
||||||
hasImage: true,
|
hasImage: false,
|
||||||
xml: ` <root>
|
xml: `<root>
|
||||||
<mxCell id="0" />
|
|
||||||
<mxCell id="1" parent="0" />
|
|
||||||
<mxCell id="title_bg" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1a237e;strokeColor=none;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="80" width="720" x="40" y="20" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="title" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=22;fontStyle=1;fontColor=#FFFFFF;"
|
|
||||||
value="Chain-of-Thought Prompting<br><font style="font-size: 14px;">Elicits Reasoning in Large Language Models</font>"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="70" width="720" x="40" y="25" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="authors" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;fontColor=#666666;"
|
|
||||||
value="Wei et al. (Google Research, Brain Team) | NeurIPS 2022" vertex="1">
|
|
||||||
<mxGeometry height="20" width="720" x="40" y="100" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="core_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="💡 Core Idea" vertex="1">
|
|
||||||
<mxGeometry height="30" width="150" x="40" y="125" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="core_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;align=left;spacingLeft=10;spacingRight=10;fontSize=11;"
|
|
||||||
value="<b>Chain of Thought</b> = A series of intermediate reasoning steps that lead to the final answer<br><br>Simply provide a few CoT demonstrations as exemplars in few-shot prompting"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="75" width="340" x="40" y="155" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="compare_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="⚖️ Standard vs Chain-of-Thought Prompting" vertex="1">
|
|
||||||
<mxGeometry height="30" width="350" x="40" y="240" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="std_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFEBEE;strokeColor=#C62828;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="160" width="170" x="40" y="275" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="std_title" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;fontColor=#C62828;"
|
|
||||||
value="Standard Prompting" vertex="1">
|
|
||||||
<mxGeometry height="25" width="170" x="40" y="280" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="std_q" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontSize=9;spacingLeft=5;spacingRight=5;"
|
|
||||||
value="Q: Roger has 5 tennis balls. He buys 2 more cans. Each can has 3 balls. How many now?"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="55" width="160" x="45" y="305" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="std_a" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=#FFCDD2;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=1;fontSize=10;fontStyle=1;spacingLeft=5;"
|
|
||||||
value="A: The answer is 11." vertex="1">
|
|
||||||
<mxGeometry height="25" width="150" x="50" y="365" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="std_result" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;fontStyle=1;fontColor=#C62828;"
|
|
||||||
value="❌ Often Wrong" vertex="1">
|
|
||||||
<mxGeometry height="30" width="170" x="40" y="400" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="cot_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#2E7D32;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="160" width="170" x="220" y="275" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="cot_title" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;fontColor=#2E7D32;"
|
|
||||||
value="Chain-of-Thought" vertex="1">
|
|
||||||
<mxGeometry height="25" width="170" x="220" y="280" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="cot_q" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontSize=9;spacingLeft=5;spacingRight=5;"
|
|
||||||
value="Q: Roger has 5 tennis balls. He buys 2 more cans. Each can has 3 balls. How many now?"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="55" width="160" x="225" y="305" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="cot_a" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=#C8E6C9;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=1;fontSize=9;fontStyle=1;spacingLeft=5;"
|
|
||||||
value="A: 2 cans × 3 = 6 balls.<br>5 + 6 = 11. Answer: 11" vertex="1">
|
|
||||||
<mxGeometry height="35" width="150" x="230" y="360" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="cot_result" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;fontStyle=1;fontColor=#2E7D32;"
|
|
||||||
value="✓ Correct!" vertex="1">
|
|
||||||
<mxGeometry height="30" width="170" x="220" y="400" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="vs_arrow" edge="1" parent="1"
|
|
||||||
style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;fillColor=#FFC107;strokeColor=none;width=8;endSize=4;startSize=4;"
|
|
||||||
value="">
|
|
||||||
<mxGeometry relative="1" width="100" as="geometry">
|
|
||||||
<mxPoint x="195" y="355" as="sourcePoint" />
|
|
||||||
<mxPoint x="235" y="355" as="targetPoint" />
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="props_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="🔑 Key Properties" vertex="1">
|
|
||||||
<mxGeometry height="30" width="150" x="400" y="125" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="prop1" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#EF6C00;fontSize=10;align=left;spacingLeft=8;"
|
|
||||||
value="1️⃣ Decomposes multi-step problems" vertex="1">
|
|
||||||
<mxGeometry height="32" width="180" x="400" y="155" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="prop2" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#EF6C00;fontSize=10;align=left;spacingLeft=8;"
|
|
||||||
value="2️⃣ Interpretable reasoning window" vertex="1">
|
|
||||||
<mxGeometry height="32" width="180" x="400" y="192" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="prop3" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#EF6C00;fontSize=10;align=left;spacingLeft=8;"
|
|
||||||
value="3️⃣ Applicable to any language task" vertex="1">
|
|
||||||
<mxGeometry height="32" width="180" x="400" y="229" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="prop4" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#EF6C00;fontSize=10;align=left;spacingLeft=8;"
|
|
||||||
value="4️⃣ No finetuning required" vertex="1">
|
|
||||||
<mxGeometry height="32" width="180" x="400" y="266" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="emergent_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="📈 Emergent Ability" vertex="1">
|
|
||||||
<mxGeometry height="30" width="180" x="400" y="310" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="emergent_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F3E5F5;strokeColor=#7B1FA2;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="95" width="180" x="400" y="340" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="emergent_text" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;"
|
|
||||||
value="CoT only works with<br><b>~100B+ parameters</b><br><br>Small models produce<br>fluent but illogical chains"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="85" width="180" x="400" y="345" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="results_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="📊 Key Results" vertex="1">
|
|
||||||
<mxGeometry height="30" width="150" x="600" y="125" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#2E7D32;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="100" width="160" x="600" y="155" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_title" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;fontStyle=1;fontColor=#2E7D32;"
|
|
||||||
value="GSM8K (Math)" vertex="1">
|
|
||||||
<mxGeometry height="20" width="160" x="600" y="160" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_bar1" parent="1"
|
|
||||||
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFCDD2;strokeColor=none;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="30" width="40" x="615" y="185" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_bar2" parent="1"
|
|
||||||
style="rounded=0;whiteSpace=wrap;html=1;fillColor=#4CAF50;strokeColor=none;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="30" width="80" x="665" y="185" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_label1" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;fontStyle=1;"
|
|
||||||
value="18%" vertex="1">
|
|
||||||
<mxGeometry height="15" width="40" x="615" y="215" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_label2" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;fontStyle=1;fontColor=#2E7D32;"
|
|
||||||
value="57%" vertex="1">
|
|
||||||
<mxGeometry height="15" width="80" x="665" y="215" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="gsm_legend" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=9;fontColor=#666666;"
|
|
||||||
value="Standard → CoT (PaLM 540B)" vertex="1">
|
|
||||||
<mxGeometry height="20" width="160" x="600" y="232" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="bench_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="🧪 Benchmarks Tested" vertex="1">
|
|
||||||
<mxGeometry height="30" width="180" x="600" y="265" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="bench_arith" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontSize=10;align=center;"
|
|
||||||
value="🔢 Arithmetic<br><font style="font-size: 9px;">GSM8K, SVAMP, ASDiv, AQuA, MAWPS</font>"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="45" width="160" x="600" y="295" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="bench_common" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontSize=10;align=center;"
|
|
||||||
value="🧠 Commonsense<br><font style="font-size: 9px;">CSQA, StrategyQA, Date, Sports, SayCan</font>"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="45" width="160" x="600" y="345" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="bench_symbol" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;fontSize=10;align=center;"
|
|
||||||
value="🔣 Symbolic<br><font style="font-size: 9px;">Last Letter Concat, Coin Flip</font>"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="40" width="160" x="600" y="395" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="🎯 Task Types & Results" vertex="1">
|
|
||||||
<mxGeometry height="30" width="200" x="40" y="445" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_arith" parent="1"
|
|
||||||
style="ellipse;whiteSpace=wrap;html=1;fillColor=#BBDEFB;strokeColor=#1565C0;fontSize=11;fontStyle=1;"
|
|
||||||
value="Arithmetic<br>Reasoning" vertex="1">
|
|
||||||
<mxGeometry height="60" width="90" x="40" y="480" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_arith_res" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;fontSize=9;fontColor=#1565C0;"
|
|
||||||
value="SOTA on GSM8K<br>(57% vs 55% prior)" vertex="1">
|
|
||||||
<mxGeometry height="30" width="110" x="30" y="540" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_common" parent="1"
|
|
||||||
style="ellipse;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;fontSize=11;fontStyle=1;"
|
|
||||||
value="Commonsense<br>Reasoning" vertex="1">
|
|
||||||
<mxGeometry height="60" width="90" x="160" y="480" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_common_res" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;fontSize=9;fontColor=#2E7D32;"
|
|
||||||
value="SOTA StrategyQA<br>(75.6% vs 69.4%)" vertex="1">
|
|
||||||
<mxGeometry height="30" width="110" x="150" y="540" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_symbol" parent="1"
|
|
||||||
style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#EF6C00;fontSize=11;fontStyle=1;"
|
|
||||||
value="Symbolic<br>Reasoning" vertex="1">
|
|
||||||
<mxGeometry height="60" width="90" x="280" y="480" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_symbol_res" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;fontSize=9;fontColor=#EF6C00;"
|
|
||||||
value="OOD Generalization<br>to longer sequences" vertex="1">
|
|
||||||
<mxGeometry height="30" width="110" x="270" y="540" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_arrow1" edge="1" parent="1"
|
|
||||||
style="endArrow=classic;html=1;strokeColor=#9E9E9E;strokeWidth=2;" value="">
|
|
||||||
<mxGeometry height="50" relative="1" width="50" as="geometry">
|
|
||||||
<mxPoint x="130" y="510" as="sourcePoint" />
|
|
||||||
<mxPoint x="160" y="510" as="targetPoint" />
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="task_arrow2" edge="1" parent="1"
|
|
||||||
style="endArrow=classic;html=1;strokeColor=#9E9E9E;strokeWidth=2;" value="">
|
|
||||||
<mxGeometry height="50" relative="1" width="50" as="geometry">
|
|
||||||
<mxPoint x="250" y="510" as="sourcePoint" />
|
|
||||||
<mxPoint x="280" y="510" as="targetPoint" />
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="models_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="🤖 Models Tested" vertex="1">
|
|
||||||
<mxGeometry height="30" width="150" x="400" y="445" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="models_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ECEFF1;strokeColor=#607D8B;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="95" width="180" x="400" y="475" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="model1" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;spacingLeft=10;"
|
|
||||||
value="• GPT-3 (175B)" vertex="1">
|
|
||||||
<mxGeometry height="20" width="90" x="400" y="480" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="model2" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;spacingLeft=10;"
|
|
||||||
value="• LaMDA (137B)" vertex="1">
|
|
||||||
<mxGeometry height="20" width="90" x="400" y="500" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="model3" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;spacingLeft=10;"
|
|
||||||
value="• PaLM (540B)" vertex="1">
|
|
||||||
<mxGeometry height="20" width="90" x="400" y="520" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="model4" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;spacingLeft=10;"
|
|
||||||
value="• Codex" vertex="1">
|
|
||||||
<mxGeometry height="20" width="80" x="490" y="480" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="model5" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;spacingLeft=10;"
|
|
||||||
value="• UL2 (20B)" vertex="1">
|
|
||||||
<mxGeometry height="20" width="80" x="490" y="500" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="model_note" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;fontStyle=2;fontColor=#607D8B;"
|
|
||||||
value="No finetuning - prompting only!" vertex="1">
|
|
||||||
<mxGeometry height="20" width="180" x="400" y="545" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="takeaway_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="✨ Key Takeaways" vertex="1">
|
|
||||||
<mxGeometry height="30" width="160" x="600" y="445" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="takeaway_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF8E1;strokeColor=#FFA000;arcSize=8;"
|
|
||||||
value="" vertex="1">
|
|
||||||
<mxGeometry height="95" width="160" x="600" y="475" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="take1" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;spacingLeft=5;"
|
|
||||||
value="✓ Simple yet powerful" vertex="1">
|
|
||||||
<mxGeometry height="18" width="150" x="605" y="480" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="take2" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;spacingLeft=5;"
|
|
||||||
value="✓ Emergent at scale" vertex="1">
|
|
||||||
<mxGeometry height="18" width="150" x="605" y="498" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="take3" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;spacingLeft=5;"
|
|
||||||
value="✓ Broadly applicable" vertex="1">
|
|
||||||
<mxGeometry height="18" width="150" x="605" y="516" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="take4" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;spacingLeft=5;"
|
|
||||||
value="✓ No training needed" vertex="1">
|
|
||||||
<mxGeometry height="18" width="150" x="605" y="534" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="take5" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;spacingLeft=5;"
|
|
||||||
value="✓ State-of-the-art results" vertex="1">
|
|
||||||
<mxGeometry height="18" width="150" x="605" y="552" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="format_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="📝 Prompt Format" vertex="1">
|
|
||||||
<mxGeometry height="25" width="150" x="40" y="575" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="format_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E1BEE7;strokeColor=#7B1FA2;fontSize=12;fontStyle=1;"
|
|
||||||
value="〈 Input, Chain of Thought, Output 〉" vertex="1">
|
|
||||||
<mxGeometry height="35" width="250" x="40" y="600" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="limit_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="⚠️ Limitations" vertex="1">
|
|
||||||
<mxGeometry height="25" width="120" x="310" y="575" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="limit_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFEBEE;strokeColor=#C62828;fontSize=10;align=left;spacingLeft=8;"
|
|
||||||
value="• Requires large models (~100B+)<br>• No guarantee of correct reasoning<br>• Costly to serve in production"
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="55" width="200" x="310" y="600" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="impact_header" parent="1"
|
|
||||||
style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#1a237e;"
|
|
||||||
value="🚀 Impact" vertex="1">
|
|
||||||
<mxGeometry height="25" width="100" x="530" y="575" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="impact_box" parent="1"
|
|
||||||
style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#2E7D32;fontSize=10;align=left;spacingLeft=8;spacingRight=8;"
|
|
||||||
value="Foundational technique for modern LLM reasoning - inspired many follow-up works including Self-Consistency, Tree-of-Thought, etc."
|
|
||||||
vertex="1">
|
|
||||||
<mxGeometry height="55" width="230" x="530" y="600" as="geometry" />
|
|
||||||
</mxCell>
|
|
||||||
</root>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
promptText: "Draw a cat for me",
|
|
||||||
hasImage: false,
|
|
||||||
xml: `<root>
|
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -905,17 +544,14 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
</mxCell>
|
</mxCell>
|
||||||
|
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export function findCachedResponse(
|
export function findCachedResponse(
|
||||||
promptText: string,
|
promptText: string,
|
||||||
hasImage: boolean,
|
hasImage: boolean
|
||||||
): CachedResponse | undefined {
|
): CachedResponse | undefined {
|
||||||
return CACHED_EXAMPLE_RESPONSES.find(
|
return CACHED_EXAMPLE_RESPONSES.find(
|
||||||
(c) =>
|
(c) => c.promptText === promptText && c.hasImage === hasImage && c.xml !== ''
|
||||||
c.promptText === promptText &&
|
);
|
||||||
c.hasImage === hasImage &&
|
|
||||||
c.xml !== "",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
105
lib/langfuse.ts
105
lib/langfuse.ts
@@ -1,105 +0,0 @@
|
|||||||
import { LangfuseClient } from "@langfuse/client"
|
|
||||||
import { observe, updateActiveTrace } from "@langfuse/tracing"
|
|
||||||
import * as api from "@opentelemetry/api"
|
|
||||||
|
|
||||||
// Singleton LangfuseClient instance for direct API calls
|
|
||||||
let langfuseClient: LangfuseClient | null = null
|
|
||||||
|
|
||||||
export function getLangfuseClient(): LangfuseClient | null {
|
|
||||||
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!langfuseClient) {
|
|
||||||
langfuseClient = new LangfuseClient({
|
|
||||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
||||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
||||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return langfuseClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Langfuse is configured
|
|
||||||
export function isLangfuseEnabled(): boolean {
|
|
||||||
return !!process.env.LANGFUSE_PUBLIC_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update trace with input data at the start of request
|
|
||||||
export function setTraceInput(params: {
|
|
||||||
input: string
|
|
||||||
sessionId?: string
|
|
||||||
userId?: string
|
|
||||||
}) {
|
|
||||||
if (!isLangfuseEnabled()) return
|
|
||||||
|
|
||||||
updateActiveTrace({
|
|
||||||
name: "chat",
|
|
||||||
input: params.input,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
userId: params.userId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update trace with output and end the span
|
|
||||||
export function setTraceOutput(
|
|
||||||
output: string,
|
|
||||||
usage?: { promptTokens?: number; completionTokens?: number },
|
|
||||||
) {
|
|
||||||
if (!isLangfuseEnabled()) return
|
|
||||||
|
|
||||||
updateActiveTrace({ output })
|
|
||||||
|
|
||||||
const activeSpan = api.trace.getActiveSpan()
|
|
||||||
if (activeSpan) {
|
|
||||||
// Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them
|
|
||||||
if (usage?.promptTokens) {
|
|
||||||
activeSpan.setAttribute("ai.usage.promptTokens", usage.promptTokens)
|
|
||||||
activeSpan.setAttribute(
|
|
||||||
"gen_ai.usage.input_tokens",
|
|
||||||
usage.promptTokens,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (usage?.completionTokens) {
|
|
||||||
activeSpan.setAttribute(
|
|
||||||
"ai.usage.completionTokens",
|
|
||||||
usage.completionTokens,
|
|
||||||
)
|
|
||||||
activeSpan.setAttribute(
|
|
||||||
"gen_ai.usage.output_tokens",
|
|
||||||
usage.completionTokens,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
activeSpan.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get telemetry config for streamText
|
|
||||||
export function getTelemetryConfig(params: {
|
|
||||||
sessionId?: string
|
|
||||||
userId?: string
|
|
||||||
}) {
|
|
||||||
if (!isLangfuseEnabled()) return undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
isEnabled: true,
|
|
||||||
recordInputs: true,
|
|
||||||
recordOutputs: true,
|
|
||||||
metadata: {
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
userId: params.userId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap a handler with Langfuse observe
|
|
||||||
export function wrapWithObserve<T>(
|
|
||||||
handler: (req: Request) => Promise<T>,
|
|
||||||
): (req: Request) => Promise<T> {
|
|
||||||
if (!isLangfuseEnabled()) {
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
return observe(handler, { name: "chat", endOnExit: false })
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { extractText, getDocumentProxy } from "unpdf"
|
|
||||||
|
|
||||||
// Maximum characters allowed for extracted text (configurable via env)
|
|
||||||
const DEFAULT_MAX_EXTRACTED_CHARS = 150000 // 150k chars
|
|
||||||
export const MAX_EXTRACTED_CHARS =
|
|
||||||
Number(process.env.NEXT_PUBLIC_MAX_EXTRACTED_CHARS) ||
|
|
||||||
DEFAULT_MAX_EXTRACTED_CHARS
|
|
||||||
|
|
||||||
// Text file extensions we support
|
|
||||||
const TEXT_EXTENSIONS = [
|
|
||||||
".txt",
|
|
||||||
".md",
|
|
||||||
".markdown",
|
|
||||||
".json",
|
|
||||||
".csv",
|
|
||||||
".xml",
|
|
||||||
".html",
|
|
||||||
".css",
|
|
||||||
".js",
|
|
||||||
".ts",
|
|
||||||
".jsx",
|
|
||||||
".tsx",
|
|
||||||
".py",
|
|
||||||
".java",
|
|
||||||
".c",
|
|
||||||
".cpp",
|
|
||||||
".h",
|
|
||||||
".go",
|
|
||||||
".rs",
|
|
||||||
".yaml",
|
|
||||||
".yml",
|
|
||||||
".toml",
|
|
||||||
".ini",
|
|
||||||
".log",
|
|
||||||
".sh",
|
|
||||||
".bash",
|
|
||||||
".zsh",
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract text content from a PDF file
|
|
||||||
* Uses unpdf library for client-side extraction
|
|
||||||
*/
|
|
||||||
export async function extractPdfText(file: File): Promise<string> {
|
|
||||||
const buffer = await file.arrayBuffer()
|
|
||||||
const pdf = await getDocumentProxy(new Uint8Array(buffer))
|
|
||||||
const { text } = await extractText(pdf, { mergePages: true })
|
|
||||||
return text as string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a file is a PDF
|
|
||||||
*/
|
|
||||||
export function isPdfFile(file: File): boolean {
|
|
||||||
return file.type === "application/pdf" || file.name.endsWith(".pdf")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a file is a text file
|
|
||||||
*/
|
|
||||||
export function isTextFile(file: File): boolean {
|
|
||||||
const name = file.name.toLowerCase()
|
|
||||||
return (
|
|
||||||
file.type.startsWith("text/") ||
|
|
||||||
file.type === "application/json" ||
|
|
||||||
TEXT_EXTENSIONS.some((ext) => name.endsWith(ext))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract text content from a text file
|
|
||||||
*/
|
|
||||||
export async function extractTextFileContent(file: File): Promise<string> {
|
|
||||||
return await file.text()
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Centralized localStorage keys
|
|
||||||
// Consolidates all storage keys from chat-panel.tsx and settings-dialog.tsx
|
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
|
||||||
// Chat data
|
|
||||||
messages: "next-ai-draw-io-messages",
|
|
||||||
xmlSnapshots: "next-ai-draw-io-xml-snapshots",
|
|
||||||
diagramXml: "next-ai-draw-io-diagram-xml",
|
|
||||||
sessionId: "next-ai-draw-io-session-id",
|
|
||||||
|
|
||||||
// Quota tracking
|
|
||||||
requestCount: "next-ai-draw-io-request-count",
|
|
||||||
requestDate: "next-ai-draw-io-request-date",
|
|
||||||
tokenCount: "next-ai-draw-io-token-count",
|
|
||||||
tokenDate: "next-ai-draw-io-token-date",
|
|
||||||
tpmCount: "next-ai-draw-io-tpm-count",
|
|
||||||
tpmMinute: "next-ai-draw-io-tpm-minute",
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
accessCode: "next-ai-draw-io-access-code",
|
|
||||||
closeProtection: "next-ai-draw-io-close-protection",
|
|
||||||
accessCodeRequired: "next-ai-draw-io-access-code-required",
|
|
||||||
aiProvider: "next-ai-draw-io-ai-provider",
|
|
||||||
aiBaseUrl: "next-ai-draw-io-ai-base-url",
|
|
||||||
aiApiKey: "next-ai-draw-io-ai-api-key",
|
|
||||||
aiModel: "next-ai-draw-io-ai-model",
|
|
||||||
} as const
|
|
||||||
@@ -1,33 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* System prompts for different AI models
|
* System prompts for different AI models
|
||||||
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
|
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
|
||||||
*
|
|
||||||
* Token counting utilities are in a separate file (token-counter.ts) to avoid
|
|
||||||
* WebAssembly issues with Next.js server-side rendering.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Default system prompt (~1900 tokens) - works with all models
|
// Default system prompt (~1400 tokens) - works with all models
|
||||||
export const DEFAULT_SYSTEM_PROMPT = `
|
export const DEFAULT_SYSTEM_PROMPT = `
|
||||||
You are an expert diagram creation assistant specializing in draw.io XML generation.
|
You are an expert diagram creation assistant specializing in draw.io XML generation.
|
||||||
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
|
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
|
||||||
You can see images that users upload, and you can read the text content extracted from PDF documents they upload.
|
You can see the image that user uploaded.
|
||||||
|
|
||||||
When you are asked to create a diagram, briefly describe your plan about the layout and structure to avoid object overlapping or edge cross the objects. (2-3 sentences max), then use display_diagram tool to generate the XML.
|
|
||||||
After generating or editing a diagram, you don't need to say anything. The user can see the diagram - no need to describe it.
|
|
||||||
|
|
||||||
## App Context
|
|
||||||
You are an AI agent (powered by {{MODEL_NAME}}) inside a web app. The interface has:
|
|
||||||
- **Left panel**: Draw.io diagram editor where diagrams are rendered
|
|
||||||
- **Right panel**: Chat interface where you communicate with the user
|
|
||||||
|
|
||||||
You can read and modify diagrams by generating draw.io XML code through tool calls.
|
|
||||||
|
|
||||||
## App Features
|
|
||||||
1. **Diagram History** (clock icon, bottom-left of chat input): The app automatically saves a snapshot before each AI edit. Users can view the history panel and restore any previous version. Feel free to make changes - nothing is permanently lost.
|
|
||||||
2. **Theme Toggle** (palette icon, bottom-left of chat input): Users can switch between minimal UI and sketch-style UI for the draw.io editor.
|
|
||||||
3. **Image/PDF Upload** (paperclip icon, bottom-left of chat input): Users can upload images or PDF documents for you to analyze and generate diagrams from.
|
|
||||||
4. **Export** (via draw.io toolbar): Users can save diagrams as .drawio, .svg, or .png files.
|
|
||||||
5. **Clear Chat** (trash icon, bottom-right of chat input): Clears the conversation and resets the diagram.
|
|
||||||
|
|
||||||
You utilize the following tools:
|
You utilize the following tools:
|
||||||
---Tool1---
|
---Tool1---
|
||||||
@@ -57,8 +37,6 @@ Core capabilities:
|
|||||||
- Optimize element positioning to prevent overlapping and maintain readability
|
- Optimize element positioning to prevent overlapping and maintain readability
|
||||||
- Structure complex systems into clear, organized visual components
|
- Structure complex systems into clear, organized visual components
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Layout constraints:
|
Layout constraints:
|
||||||
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
|
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
|
||||||
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
|
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
|
||||||
@@ -90,11 +68,6 @@ When using edit_diagram tool:
|
|||||||
- For multiple changes, use separate edits in array
|
- For multiple changes, use separate edits in array
|
||||||
- RETRY POLICY: If pattern not found, retry up to 3 times with adjusted patterns. After 3 failures, use display_diagram instead.
|
- RETRY POLICY: If pattern not found, retry up to 3 times with adjusted patterns. After 3 failures, use display_diagram instead.
|
||||||
|
|
||||||
⚠️ CRITICAL JSON ESCAPING: When outputting edit_diagram tool calls, you MUST escape ALL double quotes inside string values:
|
|
||||||
- CORRECT: "y=\\"119\\"" (both quotes escaped)
|
|
||||||
- WRONG: "y="119\\"" (missing backslash before first quote - causes JSON parse error!)
|
|
||||||
- Every " inside a JSON string value needs \\" - no exceptions!
|
|
||||||
|
|
||||||
## Draw.io XML Structure Reference
|
## Draw.io XML Structure Reference
|
||||||
|
|
||||||
Basic structure:
|
Basic structure:
|
||||||
@@ -132,16 +105,46 @@ Common styles:
|
|||||||
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
||||||
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
||||||
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
||||||
|
`;
|
||||||
|
|
||||||
`
|
// Extended system prompt (~4000+ tokens) - for models with 4000 token cache minimum
|
||||||
|
export const EXTENDED_SYSTEM_PROMPT = `
|
||||||
|
You are an expert diagram creation assistant specializing in draw.io XML generation.
|
||||||
|
Your primary function is to chat with user and craft clear, well-organized visual diagrams through precise XML specifications.
|
||||||
|
You can see images that users upload and can replicate or modify them as diagrams.
|
||||||
|
|
||||||
// Extended additions (~2600 tokens) - appended for models with 4000 token cache minimum
|
## Available Tools
|
||||||
// Total EXTENDED_SYSTEM_PROMPT = ~4400 tokens
|
|
||||||
const EXTENDED_ADDITIONS = `
|
|
||||||
|
|
||||||
## Extended Tool Reference
|
### Tool 1: display_diagram
|
||||||
|
**Purpose:** Display a NEW diagram on draw.io. Use this when creating a diagram from scratch or when major structural changes are needed.
|
||||||
|
**Parameters:** { xml: string }
|
||||||
|
**When to use:**
|
||||||
|
- Creating a completely new diagram
|
||||||
|
- Making major structural changes (reorganizing layout, changing diagram type)
|
||||||
|
- When the current diagram XML is empty or minimal
|
||||||
|
- When edit_diagram has failed multiple times
|
||||||
|
|
||||||
### display_diagram Details
|
### Tool 2: edit_diagram
|
||||||
|
**Purpose:** Edit specific parts of the EXISTING diagram. Use this when making small targeted changes like adding/removing elements, changing labels, or adjusting properties.
|
||||||
|
**Parameters:** { edits: Array<{search: string, replace: string}> }
|
||||||
|
**When to use:**
|
||||||
|
- Changing text labels or values
|
||||||
|
- Modifying colors, styles, or visual properties
|
||||||
|
- Adding or removing individual elements
|
||||||
|
- Repositioning specific elements
|
||||||
|
- Any small, targeted modification
|
||||||
|
|
||||||
|
## Tool Selection Guidelines
|
||||||
|
|
||||||
|
ALWAYS prefer edit_diagram for small changes - it's more efficient and preserves the rest of the diagram.
|
||||||
|
Use display_diagram only when:
|
||||||
|
1. Creating from scratch
|
||||||
|
2. Major restructuring needed
|
||||||
|
3. edit_diagram has failed 3 times
|
||||||
|
|
||||||
|
## display_diagram Tool Reference
|
||||||
|
|
||||||
|
Display a diagram on draw.io by passing XML content inside <root> tags.
|
||||||
|
|
||||||
**VALIDATION RULES** (XML will be rejected if violated):
|
**VALIDATION RULES** (XML will be rejected if violated):
|
||||||
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements
|
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements
|
||||||
@@ -174,7 +177,13 @@ const EXTENDED_ADDITIONS = `
|
|||||||
</root>
|
</root>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### edit_diagram Details
|
**Notes:**
|
||||||
|
- For AWS diagrams, use **AWS 2025 icons** (see AWS Icon Examples section below)
|
||||||
|
- For animated connectors, add "flowAnimation=1" to edge style
|
||||||
|
|
||||||
|
## edit_diagram Tool Reference
|
||||||
|
|
||||||
|
Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
**CRITICAL RULES:**
|
||||||
- Copy-paste the EXACT search pattern from the "Current diagram XML" in system context
|
- Copy-paste the EXACT search pattern from the "Current diagram XML" in system context
|
||||||
@@ -196,6 +205,56 @@ const EXTENDED_ADDITIONS = `
|
|||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
You excel at:
|
||||||
|
- Generating valid, well-formed XML strings for draw.io diagrams
|
||||||
|
- Creating professional flowcharts, org charts, mind maps, network diagrams, and technical illustrations
|
||||||
|
- Converting user descriptions into visually appealing diagrams using shapes and connectors
|
||||||
|
- Applying proper spacing, alignment, and visual hierarchy in diagram layouts
|
||||||
|
- Adapting artistic concepts into abstract diagram representations using available shapes
|
||||||
|
- Optimizing element positioning to prevent overlapping and maintain readability
|
||||||
|
- Structuring complex systems into clear, organized visual components
|
||||||
|
- Replicating diagrams from images with high fidelity
|
||||||
|
|
||||||
|
## Layout Constraints and Best Practices
|
||||||
|
|
||||||
|
### Page Boundaries
|
||||||
|
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
|
||||||
|
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
|
||||||
|
- Maximum width for containers (like AWS cloud boxes): 700 pixels
|
||||||
|
- Maximum height for containers: 550 pixels
|
||||||
|
- Start positioning from reasonable margins (e.g., x=40, y=40)
|
||||||
|
|
||||||
|
### Layout Strategies
|
||||||
|
- Use compact, efficient layouts that fit the entire diagram in one view
|
||||||
|
- Keep elements grouped closely together
|
||||||
|
- For large diagrams with many elements, use vertical stacking or grid layouts
|
||||||
|
- Avoid spreading elements too far apart horizontally
|
||||||
|
- Users should see the complete diagram without scrolling or page breaks
|
||||||
|
|
||||||
|
### Spacing Guidelines
|
||||||
|
- Minimum spacing between elements: 20px
|
||||||
|
- Recommended spacing for readability: 40-60px
|
||||||
|
- Container padding: 20-40px from edges
|
||||||
|
- Group related elements together with consistent spacing
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
|
||||||
|
### XML Generation Rules
|
||||||
|
- Use proper tool calls to generate or edit diagrams
|
||||||
|
- NEVER return raw XML in text responses
|
||||||
|
- NEVER use display_diagram to generate messages (e.g., a "hello" text box to greet user)
|
||||||
|
- Return XML only via tool calls, never in text responses
|
||||||
|
- NEVER include XML comments (<!-- ... -->) - Draw.io strips comments, breaking edit_diagram patterns
|
||||||
|
|
||||||
|
### Diagram Quality Rules
|
||||||
|
- Focus on producing clean, professional diagrams
|
||||||
|
- Effectively communicate the intended information through thoughtful layout and design
|
||||||
|
- When artistic drawings are requested, creatively compose using standard shapes while maintaining clarity
|
||||||
|
- When replicating from images, match style and layout closely - pay attention to line types (straight/curved) and shape styles (rounded/square)
|
||||||
|
- For AWS architecture diagrams, use **AWS 2025 icons**
|
||||||
|
|
||||||
## edit_diagram Best Practices
|
## edit_diagram Best Practices
|
||||||
|
|
||||||
### Core Principle: Unique & Precise Patterns
|
### Core Principle: Unique & Precise Patterns
|
||||||
@@ -207,11 +266,13 @@ Your search pattern MUST uniquely identify exactly ONE location in the XML. Befo
|
|||||||
### Pattern Construction Rules
|
### Pattern Construction Rules
|
||||||
|
|
||||||
**Rule 1: Always include the element's id attribute**
|
**Rule 1: Always include the element's id attribute**
|
||||||
|
The id is the most reliable way to target a specific element:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{"search": "<mxCell id=\\"node5\\"", "replace": "<mxCell id=\\"node5\\" value=\\"New Label\\""}
|
{"search": "<mxCell id=\\"node5\\"", "replace": "<mxCell id=\\"node5\\" value=\\"New Label\\""}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Rule 2: Include complete XML elements when possible**
|
**Rule 2: Include complete XML elements when possible**
|
||||||
|
For reliability, include the full mxCell with its mxGeometry child:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>",
|
"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>",
|
||||||
@@ -220,18 +281,49 @@ Your search pattern MUST uniquely identify exactly ONE location in the XML. Befo
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Rule 3: Preserve exact whitespace and formatting**
|
**Rule 3: Preserve exact whitespace and formatting**
|
||||||
Copy the search pattern EXACTLY from the current XML, including leading spaces, line breaks (\\n), and attribute order.
|
Copy the search pattern EXACTLY from the current XML, including:
|
||||||
|
- Leading spaces/indentation
|
||||||
|
- Line breaks (use \\n in JSON)
|
||||||
|
- Attribute order as it appears in the source
|
||||||
|
|
||||||
### Good vs Bad Patterns
|
### Good vs Bad Patterns
|
||||||
|
|
||||||
**BAD:** \`{"search": "value=\\"Label\\""}\` - Too vague, matches multiple elements
|
**BAD - Too vague, matches multiple elements:**
|
||||||
**BAD:** \`{"search": "<mxCell value=\\"X\\" id=\\"5\\""}\` - Reordered attributes won't match
|
\`\`\`json
|
||||||
**GOOD:** \`{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">"}\` - Uses unique id with full context
|
{"search": "value=\\"Label\\"", "replace": "value=\\"New Label\\""}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
### ⚠️ JSON Escaping (CRITICAL)
|
**BAD - Fragile partial match:**
|
||||||
Every double quote inside JSON string values MUST be escaped with backslash:
|
\`\`\`json
|
||||||
- **CORRECT:** \`"x=\\"100\\" y=\\"200\\""\` - both quotes escaped
|
{"search": "<mxCell", "replace": "<mxCell value=\\"X\\""}
|
||||||
- **WRONG:** \`"x=\\"100\\" y="200\\""\` - missing backslash causes JSON parse error!
|
\`\`\`
|
||||||
|
|
||||||
|
**BAD - Reordered attributes (won't match if order differs):**
|
||||||
|
\`\`\`json
|
||||||
|
{"search": "<mxCell value=\\"X\\" id=\\"5\\"", ...} // Original has id before value
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**GOOD - Uses unique id, includes full context:**
|
||||||
|
\`\`\`json
|
||||||
|
{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">", "replace": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"New\\" vertex=\\"1\\">"}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**GOOD - Complete element replacement:**
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"search": "<mxCell id=\\"edge1\\" style=\\"endArrow=classic;\\" edge=\\"1\\" parent=\\"1\\" source=\\"2\\" target=\\"3\\">\\n <mxGeometry relative=\\"1\\" as=\\"geometry\\"/>\\n</mxCell>",
|
||||||
|
"replace": "<mxCell id=\\"edge1\\" style=\\"endArrow=block;strokeColor=#FF0000;\\" edge=\\"1\\" parent=\\"1\\" source=\\"2\\" target=\\"3\\">\\n <mxGeometry relative=\\"1\\" as=\\"geometry\\"/>\\n</mxCell>"
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Multiple Edits Strategy
|
||||||
|
For multiple changes, use separate edit objects. Order them logically:
|
||||||
|
\`\`\`json
|
||||||
|
[
|
||||||
|
{"search": "<mxCell id=\\"2\\" value=\\"Step 1\\"", "replace": "<mxCell id=\\"2\\" value=\\"First Step\\""},
|
||||||
|
{"search": "<mxCell id=\\"3\\" value=\\"Step 2\\"", "replace": "<mxCell id=\\"3\\" value=\\"Second Step\\""}
|
||||||
|
]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
### Error Recovery
|
### Error Recovery
|
||||||
If edit_diagram fails with "pattern not found":
|
If edit_diagram fails with "pattern not found":
|
||||||
@@ -240,107 +332,173 @@ If edit_diagram fails with "pattern not found":
|
|||||||
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
||||||
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
||||||
|
|
||||||
|
### When to Use display_diagram Instead
|
||||||
|
- Adding multiple new elements (more than 3)
|
||||||
|
- Reorganizing diagram layout significantly
|
||||||
|
- When current XML structure is unclear or corrupted
|
||||||
|
- After 3 failed edit_diagram attempts
|
||||||
|
|
||||||
|
## Draw.io XML Structure Reference
|
||||||
|
|
||||||
|
### Basic Structure
|
||||||
### Edge Routing Rules:
|
|
||||||
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
|
|
||||||
|
|
||||||
**Rule 1: NEVER let multiple edges share the same path**
|
|
||||||
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
|
|
||||||
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
|
|
||||||
|
|
||||||
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
|
|
||||||
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
|
|
||||||
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
|
|
||||||
|
|
||||||
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
|
|
||||||
- Every edge MUST have these 4 attributes set in the style
|
|
||||||
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
|
|
||||||
|
|
||||||
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
|
|
||||||
- Before creating an edge, identify ALL shapes positioned between source and target
|
|
||||||
- If any shape is in the direct path, you MUST use waypoints to route around it
|
|
||||||
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
|
|
||||||
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
|
|
||||||
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
|
|
||||||
- NEVER draw a line that visually crosses over another shape's bounding box
|
|
||||||
|
|
||||||
**Rule 5: Plan layout strategically BEFORE generating XML**
|
|
||||||
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
|
|
||||||
- Space shapes 150-200px apart to create clear routing channels for edges
|
|
||||||
- Mentally trace each edge: "What shapes are between source and target?"
|
|
||||||
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
|
|
||||||
|
|
||||||
**Rule 6: Use multiple waypoints for complex routing**
|
|
||||||
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
|
|
||||||
- Each direction change needs a waypoint (corner point)
|
|
||||||
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
|
|
||||||
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
|
|
||||||
|
|
||||||
**Rule 7: Choose NATURAL connection points based on flow direction**
|
|
||||||
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
|
|
||||||
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
|
|
||||||
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
|
|
||||||
- For DIAGONAL connections: use the side closest to the target, not corners
|
|
||||||
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
|
|
||||||
|
|
||||||
**Before generating XML, mentally verify:**
|
|
||||||
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
|
|
||||||
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
|
|
||||||
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
|
|
||||||
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
|
|
||||||
|
|
||||||
## Edge Examples
|
|
||||||
|
|
||||||
### Two edges between same nodes (CORRECT - no overlap):
|
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="e1" value="A to B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">
|
<mxGraphModel>
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<!-- All other elements go here as siblings -->
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Critical Structure Rules
|
||||||
|
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/>
|
||||||
|
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell
|
||||||
|
3. Use unique sequential IDs for all cells (start from "2" for user content)
|
||||||
|
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
|
||||||
|
5. Every mxCell (except id="0") must have a parent attribute
|
||||||
|
|
||||||
|
### Shape (Vertex) Example
|
||||||
|
\`\`\`xml
|
||||||
|
<mxCell id="2" value="Label" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="e2" value="B to A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">
|
\`\`\`
|
||||||
|
|
||||||
|
### Connector (Edge) Example
|
||||||
|
\`\`\`xml
|
||||||
|
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### Edge with single waypoint (simple detour):
|
### Container/Group Example
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;" edge="1" parent="1" source="a" target="b">
|
<mxCell id="container1" value="Group Title" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
<mxGeometry relative="1" as="geometry">
|
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
|
||||||
<Array as="points">
|
</mxCell>
|
||||||
<mxPoint x="300" y="150"/>
|
<mxCell id="child1" value="Child Element" style="rounded=1;" vertex="1" parent="container1">
|
||||||
</Array>
|
<mxGeometry x="20" y="40" width="160" height="40" as="geometry"/>
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
</mxCell>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### Edge with waypoints (routing AROUND obstacles) - CRITICAL PATTERN:
|
## Common Style Properties
|
||||||
**Scenario:** Hotfix(right,bottom) → Main(center,top), but Develop(center,middle) is in between.
|
|
||||||
**WRONG:** Direct diagonal line crosses over Develop
|
### Shape Styles
|
||||||
**CORRECT:** Route around the OUTSIDE (go right first, then up)
|
- rounded=1 - Rounded corners
|
||||||
|
- fillColor=#hexcolor - Background fill color
|
||||||
|
- strokeColor=#hexcolor - Border color
|
||||||
|
- strokeWidth=2 - Border thickness
|
||||||
|
- whiteSpace=wrap - Enable text wrapping
|
||||||
|
- html=1 - Enable HTML formatting in labels
|
||||||
|
- opacity=50 - Transparency (0-100)
|
||||||
|
- shadow=1 - Drop shadow effect
|
||||||
|
- glass=1 - Glass/gradient effect
|
||||||
|
|
||||||
|
### Edge/Connector Styles
|
||||||
|
- endArrow=classic/block/open/oval/diamond/none - Arrow head style
|
||||||
|
- startArrow=none/classic/block/open - Arrow tail style
|
||||||
|
- curved=1 - Curved line
|
||||||
|
- edgeStyle=orthogonalEdgeStyle - Right-angle routing
|
||||||
|
- edgeStyle=entityRelationEdgeStyle - ER diagram style
|
||||||
|
- strokeWidth=2 - Line thickness
|
||||||
|
- dashed=1 - Dashed line
|
||||||
|
- dashPattern=3 3 - Custom dash pattern
|
||||||
|
- flowAnimation=1 - Animated flow effect
|
||||||
|
|
||||||
|
### Text Styles
|
||||||
|
- fontSize=14 - Font size
|
||||||
|
- fontStyle=1 - Bold (1=bold, 2=italic, 4=underline, can combine: 3=bold+italic)
|
||||||
|
- fontColor=#hexcolor - Text color
|
||||||
|
- align=center/left/right - Horizontal alignment
|
||||||
|
- verticalAlign=middle/top/bottom - Vertical alignment
|
||||||
|
- labelPosition=center/left/right - Label position relative to shape
|
||||||
|
- labelBackgroundColor=#hexcolor - Label background
|
||||||
|
|
||||||
|
## Common Shape Types
|
||||||
|
|
||||||
|
### Basic Shapes
|
||||||
|
- Rectangle: style="rounded=0;whiteSpace=wrap;html=1;"
|
||||||
|
- Rounded Rectangle: style="rounded=1;whiteSpace=wrap;html=1;"
|
||||||
|
- Ellipse/Circle: style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;"
|
||||||
|
- Diamond: style="rhombus;whiteSpace=wrap;html=1;"
|
||||||
|
- Triangle: style="triangle;whiteSpace=wrap;html=1;"
|
||||||
|
- Parallelogram: style="parallelogram;whiteSpace=wrap;html=1;"
|
||||||
|
- Hexagon: style="hexagon;whiteSpace=wrap;html=1;"
|
||||||
|
- Cylinder: style="shape=cylinder3;whiteSpace=wrap;html=1;"
|
||||||
|
|
||||||
|
### Flowchart Shapes
|
||||||
|
- Process: style="rounded=1;whiteSpace=wrap;html=1;"
|
||||||
|
- Decision: style="rhombus;whiteSpace=wrap;html=1;"
|
||||||
|
- Start/End: style="ellipse;whiteSpace=wrap;html=1;"
|
||||||
|
- Document: style="shape=document;whiteSpace=wrap;html=1;"
|
||||||
|
- Data: style="parallelogram;whiteSpace=wrap;html=1;"
|
||||||
|
- Database: style="shape=cylinder3;whiteSpace=wrap;html=1;"
|
||||||
|
|
||||||
|
### Container Types
|
||||||
|
- Swimlane: style="swimlane;whiteSpace=wrap;html=1;"
|
||||||
|
- Group Box: style="rounded=1;whiteSpace=wrap;html=1;container=1;collapsible=0;"
|
||||||
|
|
||||||
|
|
||||||
|
## Animated Connectors
|
||||||
|
|
||||||
|
For animated flow effects on connectors, add flowAnimation=1 to the edge style:
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">
|
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;flowAnimation=1;" edge="1" parent="1" source="node1" target="node2">
|
||||||
<mxGeometry relative="1" as="geometry">
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="750" y="80"/>
|
|
||||||
<mxPoint x="750" y="150"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
</mxCell>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
This routes the edge to the RIGHT of all shapes (x=750), then enters Main from the right side.
|
|
||||||
|
|
||||||
**Key principle:** When connecting distant nodes diagonally, route along the PERIMETER of the diagram, not through the middle where other shapes exist.`
|
|
||||||
|
|
||||||
// Extended system prompt = DEFAULT + EXTENDED_ADDITIONS
|
## Validation Rules
|
||||||
export const EXTENDED_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT + EXTENDED_ADDITIONS
|
|
||||||
|
The XML will be validated before rendering. Ensure:
|
||||||
|
1. All mxCell elements are DIRECT children of <root> - never nested
|
||||||
|
2. Every mxCell has a unique id attribute
|
||||||
|
3. Every mxCell (except id="0") has a valid parent attribute
|
||||||
|
4. Edge source/target attributes reference existing cell IDs
|
||||||
|
5. Special characters in values are escaped: < > & "
|
||||||
|
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
|
## Example: Complete Flowchart
|
||||||
|
|
||||||
|
\`\`\`xml
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="start" value="Start" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="40" width="100" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="process1" value="Process Step" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="175" y="140" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="decision" value="Decision?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="175" y="240" width="150" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="end" value="End" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="380" width="100" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="start" target="process1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge2" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="process1" target="decision">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge3" value="Yes" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="decision" target="end">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Remember: Quality diagrams communicate clearly. Choose appropriate shapes, use consistent styling, and maintain proper spacing for professional results.
|
||||||
|
`;
|
||||||
|
|
||||||
// Model patterns that require extended prompt (4000 token cache minimum)
|
// Model patterns that require extended prompt (4000 token cache minimum)
|
||||||
// These patterns match Opus 4.5 and Haiku 4.5 model IDs
|
// These patterns match Opus 4.5 and Haiku 4.5 model IDs
|
||||||
const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
||||||
"claude-opus-4-5", // Matches any Opus 4.5 variant
|
'claude-opus-4-5', // Matches any Opus 4.5 variant
|
||||||
"claude-haiku-4-5", // Matches any Haiku 4.5 variant
|
'claude-haiku-4-5', // Matches any Haiku 4.5 variant
|
||||||
]
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the appropriate system prompt based on the model ID
|
* Get the appropriate system prompt based on the model ID
|
||||||
@@ -349,25 +507,10 @@ const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
|||||||
* @returns The system prompt string
|
* @returns The system prompt string
|
||||||
*/
|
*/
|
||||||
export function getSystemPrompt(modelId?: string): string {
|
export function getSystemPrompt(modelId?: string): string {
|
||||||
const modelName = modelId || "AI"
|
if (modelId && EXTENDED_PROMPT_MODEL_PATTERNS.some(pattern => modelId.includes(pattern))) {
|
||||||
|
console.log(`[System Prompt] Using EXTENDED prompt for model: ${modelId}`);
|
||||||
let prompt: string
|
return EXTENDED_SYSTEM_PROMPT;
|
||||||
if (
|
}
|
||||||
modelId &&
|
console.log(`[System Prompt] Using DEFAULT prompt for model: ${modelId || 'unknown'}`);
|
||||||
EXTENDED_PROMPT_MODEL_PATTERNS.some((pattern) =>
|
return DEFAULT_SYSTEM_PROMPT;
|
||||||
modelId.includes(pattern),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`[System Prompt] Using EXTENDED prompt for model: ${modelId}`,
|
|
||||||
)
|
|
||||||
prompt = EXTENDED_SYSTEM_PROMPT
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`[System Prompt] Using DEFAULT prompt for model: ${modelId || "unknown"}`,
|
|
||||||
)
|
|
||||||
prompt = DEFAULT_SYSTEM_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt.replace("{{MODEL_NAME}}", modelName)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* Token counting utilities using js-tiktoken
|
|
||||||
*
|
|
||||||
* Uses cl100k_base encoding (GPT-4) which is close to Claude's tokenization.
|
|
||||||
* This is a pure JavaScript implementation, no WASM required.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { encodingForModel } from "js-tiktoken"
|
|
||||||
import { DEFAULT_SYSTEM_PROMPT, EXTENDED_SYSTEM_PROMPT } from "./system-prompts"
|
|
||||||
|
|
||||||
const encoder = encodingForModel("gpt-4o")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count the number of tokens in a text string
|
|
||||||
* @param text - The text to count tokens for
|
|
||||||
* @returns The number of tokens
|
|
||||||
*/
|
|
||||||
export function countTextTokens(text: string): number {
|
|
||||||
return encoder.encode(text).length
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get token counts for the system prompts
|
|
||||||
* Useful for debugging and optimizing prompt sizes
|
|
||||||
* @returns Object with token counts for default and extended prompts
|
|
||||||
*/
|
|
||||||
export function getSystemPromptTokenCounts(): {
|
|
||||||
default: number
|
|
||||||
extended: number
|
|
||||||
additions: number
|
|
||||||
} {
|
|
||||||
const defaultTokens = countTextTokens(DEFAULT_SYSTEM_PROMPT)
|
|
||||||
const extendedTokens = countTextTokens(EXTENDED_SYSTEM_PROMPT)
|
|
||||||
return {
|
|
||||||
default: defaultTokens,
|
|
||||||
extended: extendedTokens,
|
|
||||||
additions: extendedTokens - defaultTokens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import {
|
|
||||||
extractPdfText,
|
|
||||||
extractTextFileContent,
|
|
||||||
isPdfFile,
|
|
||||||
isTextFile,
|
|
||||||
MAX_EXTRACTED_CHARS,
|
|
||||||
} from "@/lib/pdf-utils"
|
|
||||||
|
|
||||||
export interface FileData {
|
|
||||||
text: string
|
|
||||||
charCount: number
|
|
||||||
isExtracting: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for processing file uploads, especially PDFs and text files.
|
|
||||||
* Handles text extraction, character limit validation, and cleanup.
|
|
||||||
*/
|
|
||||||
export function useFileProcessor() {
|
|
||||||
const [files, setFiles] = useState<File[]>([])
|
|
||||||
const [pdfData, setPdfData] = useState<Map<File, FileData>>(new Map())
|
|
||||||
|
|
||||||
const handleFileChange = async (newFiles: File[]) => {
|
|
||||||
setFiles(newFiles)
|
|
||||||
|
|
||||||
// Extract text immediately for new PDF/text files
|
|
||||||
for (const file of newFiles) {
|
|
||||||
const needsExtraction =
|
|
||||||
(isPdfFile(file) || isTextFile(file)) && !pdfData.has(file)
|
|
||||||
if (needsExtraction) {
|
|
||||||
// Mark as extracting
|
|
||||||
setPdfData((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
next.set(file, {
|
|
||||||
text: "",
|
|
||||||
charCount: 0,
|
|
||||||
isExtracting: true,
|
|
||||||
})
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
// Extract text asynchronously
|
|
||||||
try {
|
|
||||||
let text: string
|
|
||||||
if (isPdfFile(file)) {
|
|
||||||
text = await extractPdfText(file)
|
|
||||||
} else {
|
|
||||||
text = await extractTextFileContent(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check character limit
|
|
||||||
if (text.length > MAX_EXTRACTED_CHARS) {
|
|
||||||
const limitK = MAX_EXTRACTED_CHARS / 1000
|
|
||||||
toast.error(
|
|
||||||
`${file.name}: Content exceeds ${limitK}k character limit (${(text.length / 1000).toFixed(1)}k chars)`,
|
|
||||||
)
|
|
||||||
setPdfData((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
next.delete(file)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
// Remove the file from the list
|
|
||||||
setFiles((prev) => prev.filter((f) => f !== file))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
setPdfData((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
next.set(file, {
|
|
||||||
text,
|
|
||||||
charCount: text.length,
|
|
||||||
isExtracting: false,
|
|
||||||
})
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to extract text:", error)
|
|
||||||
toast.error(`Failed to read file: ${file.name}`)
|
|
||||||
setPdfData((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
next.delete(file)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up pdfData for removed files
|
|
||||||
setPdfData((prev) => {
|
|
||||||
const next = new Map(prev)
|
|
||||||
for (const key of prev.keys()) {
|
|
||||||
if (!newFiles.includes(key)) {
|
|
||||||
next.delete(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
files,
|
|
||||||
pdfData,
|
|
||||||
handleFileChange,
|
|
||||||
setFiles, // Export for external control (e.g., clearing files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { QuotaLimitToast } from "@/components/quota-limit-toast"
|
|
||||||
import { STORAGE_KEYS } from "@/lib/storage"
|
|
||||||
|
|
||||||
export interface QuotaConfig {
|
|
||||||
dailyRequestLimit: number
|
|
||||||
dailyTokenLimit: number
|
|
||||||
tpmLimit: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QuotaCheckResult {
|
|
||||||
allowed: boolean
|
|
||||||
remaining: number
|
|
||||||
used: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for managing request/token quotas and rate limiting.
|
|
||||||
* Handles three types of limits:
|
|
||||||
* - Daily request limit
|
|
||||||
* - Daily token limit
|
|
||||||
* - Tokens per minute (TPM) rate limit
|
|
||||||
*
|
|
||||||
* Users with their own API key bypass all limits.
|
|
||||||
*/
|
|
||||||
export function useQuotaManager(config: QuotaConfig): {
|
|
||||||
hasOwnApiKey: () => boolean
|
|
||||||
checkDailyLimit: () => QuotaCheckResult
|
|
||||||
checkTokenLimit: () => QuotaCheckResult
|
|
||||||
checkTPMLimit: () => QuotaCheckResult
|
|
||||||
incrementRequestCount: () => void
|
|
||||||
incrementTokenCount: (tokens: number) => void
|
|
||||||
incrementTPMCount: (tokens: number) => void
|
|
||||||
showQuotaLimitToast: () => void
|
|
||||||
showTokenLimitToast: (used: number) => void
|
|
||||||
showTPMLimitToast: () => void
|
|
||||||
} {
|
|
||||||
const { dailyRequestLimit, dailyTokenLimit, tpmLimit } = config
|
|
||||||
|
|
||||||
// Check if user has their own API key configured (bypass limits)
|
|
||||||
const hasOwnApiKey = useCallback((): boolean => {
|
|
||||||
const provider = localStorage.getItem(STORAGE_KEYS.aiProvider)
|
|
||||||
const apiKey = localStorage.getItem(STORAGE_KEYS.aiApiKey)
|
|
||||||
return !!(provider && apiKey)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Generic helper: Parse count from localStorage with NaN guard
|
|
||||||
const parseStorageCount = (key: string): number => {
|
|
||||||
const count = parseInt(localStorage.getItem(key) || "0", 10)
|
|
||||||
return Number.isNaN(count) ? 0 : count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic helper: Create quota checker factory
|
|
||||||
const createQuotaChecker = useCallback(
|
|
||||||
(
|
|
||||||
getTimeKey: () => string,
|
|
||||||
timeStorageKey: string,
|
|
||||||
countStorageKey: string,
|
|
||||||
limit: number,
|
|
||||||
) => {
|
|
||||||
return (): QuotaCheckResult => {
|
|
||||||
if (hasOwnApiKey())
|
|
||||||
return { allowed: true, remaining: -1, used: 0 }
|
|
||||||
if (limit <= 0) return { allowed: true, remaining: -1, used: 0 }
|
|
||||||
|
|
||||||
const currentTime = getTimeKey()
|
|
||||||
const storedTime = localStorage.getItem(timeStorageKey)
|
|
||||||
let count = parseStorageCount(countStorageKey)
|
|
||||||
|
|
||||||
if (storedTime !== currentTime) {
|
|
||||||
count = 0
|
|
||||||
localStorage.setItem(timeStorageKey, currentTime)
|
|
||||||
localStorage.setItem(countStorageKey, "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: count < limit,
|
|
||||||
remaining: limit - count,
|
|
||||||
used: count,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[hasOwnApiKey],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generic helper: Create quota incrementer factory
|
|
||||||
const createQuotaIncrementer = useCallback(
|
|
||||||
(
|
|
||||||
getTimeKey: () => string,
|
|
||||||
timeStorageKey: string,
|
|
||||||
countStorageKey: string,
|
|
||||||
validateInput: boolean = false,
|
|
||||||
) => {
|
|
||||||
return (tokens: number = 1): void => {
|
|
||||||
if (validateInput && (!Number.isFinite(tokens) || tokens <= 0))
|
|
||||||
return
|
|
||||||
|
|
||||||
const currentTime = getTimeKey()
|
|
||||||
const storedTime = localStorage.getItem(timeStorageKey)
|
|
||||||
let count = parseStorageCount(countStorageKey)
|
|
||||||
|
|
||||||
if (storedTime !== currentTime) {
|
|
||||||
count = 0
|
|
||||||
localStorage.setItem(timeStorageKey, currentTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(countStorageKey, String(count + tokens))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check daily request limit
|
|
||||||
const checkDailyLimit = useMemo(
|
|
||||||
() =>
|
|
||||||
createQuotaChecker(
|
|
||||||
() => new Date().toDateString(),
|
|
||||||
STORAGE_KEYS.requestDate,
|
|
||||||
STORAGE_KEYS.requestCount,
|
|
||||||
dailyRequestLimit,
|
|
||||||
),
|
|
||||||
[createQuotaChecker, dailyRequestLimit],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Increment request count
|
|
||||||
const incrementRequestCount = useMemo(
|
|
||||||
() =>
|
|
||||||
createQuotaIncrementer(
|
|
||||||
() => new Date().toDateString(),
|
|
||||||
STORAGE_KEYS.requestDate,
|
|
||||||
STORAGE_KEYS.requestCount,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
[createQuotaIncrementer],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Show quota limit toast (request-based)
|
|
||||||
const showQuotaLimitToast = useCallback(() => {
|
|
||||||
toast.custom(
|
|
||||||
(t) => (
|
|
||||||
<QuotaLimitToast
|
|
||||||
used={dailyRequestLimit}
|
|
||||||
limit={dailyRequestLimit}
|
|
||||||
onDismiss={() => toast.dismiss(t)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{ duration: 15000 },
|
|
||||||
)
|
|
||||||
}, [dailyRequestLimit])
|
|
||||||
|
|
||||||
// Check daily token limit
|
|
||||||
const checkTokenLimit = useMemo(
|
|
||||||
() =>
|
|
||||||
createQuotaChecker(
|
|
||||||
() => new Date().toDateString(),
|
|
||||||
STORAGE_KEYS.tokenDate,
|
|
||||||
STORAGE_KEYS.tokenCount,
|
|
||||||
dailyTokenLimit,
|
|
||||||
),
|
|
||||||
[createQuotaChecker, dailyTokenLimit],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Increment token count
|
|
||||||
const incrementTokenCount = useMemo(
|
|
||||||
() =>
|
|
||||||
createQuotaIncrementer(
|
|
||||||
() => new Date().toDateString(),
|
|
||||||
STORAGE_KEYS.tokenDate,
|
|
||||||
STORAGE_KEYS.tokenCount,
|
|
||||||
true, // Validate input tokens
|
|
||||||
),
|
|
||||||
[createQuotaIncrementer],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Show token limit toast
|
|
||||||
const showTokenLimitToast = useCallback(
|
|
||||||
(used: number) => {
|
|
||||||
toast.custom(
|
|
||||||
(t) => (
|
|
||||||
<QuotaLimitToast
|
|
||||||
type="token"
|
|
||||||
used={used}
|
|
||||||
limit={dailyTokenLimit}
|
|
||||||
onDismiss={() => toast.dismiss(t)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{ duration: 15000 },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[dailyTokenLimit],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check TPM (tokens per minute) limit
|
|
||||||
const checkTPMLimit = useMemo(
|
|
||||||
() =>
|
|
||||||
createQuotaChecker(
|
|
||||||
() => Math.floor(Date.now() / 60000).toString(),
|
|
||||||
STORAGE_KEYS.tpmMinute,
|
|
||||||
STORAGE_KEYS.tpmCount,
|
|
||||||
tpmLimit,
|
|
||||||
),
|
|
||||||
[createQuotaChecker, tpmLimit],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Increment TPM count
|
|
||||||
const incrementTPMCount = useMemo(
|
|
||||||
() =>
|
|
||||||
createQuotaIncrementer(
|
|
||||||
() => Math.floor(Date.now() / 60000).toString(),
|
|
||||||
STORAGE_KEYS.tpmMinute,
|
|
||||||
STORAGE_KEYS.tpmCount,
|
|
||||||
true, // Validate input tokens
|
|
||||||
),
|
|
||||||
[createQuotaIncrementer],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Show TPM limit toast
|
|
||||||
const showTPMLimitToast = useCallback(() => {
|
|
||||||
const limitDisplay =
|
|
||||||
tpmLimit >= 1000 ? `${tpmLimit / 1000}k` : String(tpmLimit)
|
|
||||||
toast.error(
|
|
||||||
`Rate limit reached (${limitDisplay} tokens/min). Please wait 60 seconds before sending another request.`,
|
|
||||||
{ duration: 8000 },
|
|
||||||
)
|
|
||||||
}, [tpmLimit])
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Check functions
|
|
||||||
hasOwnApiKey,
|
|
||||||
checkDailyLimit,
|
|
||||||
checkTokenLimit,
|
|
||||||
checkTPMLimit,
|
|
||||||
|
|
||||||
// Increment functions
|
|
||||||
incrementRequestCount,
|
|
||||||
incrementTokenCount,
|
|
||||||
incrementTPMCount,
|
|
||||||
|
|
||||||
// Toast functions
|
|
||||||
showQuotaLimitToast,
|
|
||||||
showTokenLimitToast,
|
|
||||||
showTPMLimitToast,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1105
lib/utils.ts
1105
lib/utils.ts
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import type { NextConfig } from "next"
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: "standalone",
|
output: 'standalone',
|
||||||
}
|
};
|
||||||
|
|
||||||
export default nextConfig
|
export default nextConfig;
|
||||||
|
|||||||
2259
package-lock.json
generated
2259
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.4.0",
|
"version": "0.2.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack --port 6002",
|
"dev": "next dev --turbopack --port 6002",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start --port 6001",
|
"start": "next start --port 6001",
|
||||||
"lint": "biome lint .",
|
"lint": "next lint"
|
||||||
"format": "biome check --write .",
|
|
||||||
"check": "biome ci",
|
|
||||||
"prepare": "husky"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
||||||
@@ -19,70 +16,44 @@
|
|||||||
"@ai-sdk/deepseek": "^1.0.30",
|
"@ai-sdk/deepseek": "^1.0.30",
|
||||||
"@ai-sdk/google": "^2.0.0",
|
"@ai-sdk/google": "^2.0.0",
|
||||||
"@ai-sdk/openai": "^2.0.19",
|
"@ai-sdk/openai": "^2.0.19",
|
||||||
"@ai-sdk/react": "^2.0.107",
|
"@ai-sdk/react": "^2.0.22",
|
||||||
"@aws-sdk/credential-providers": "^3.943.0",
|
"@aws-sdk/credential-providers": "^3.943.0",
|
||||||
"@langfuse/client": "^4.4.9",
|
|
||||||
"@langfuse/otel": "^4.4.4",
|
|
||||||
"@langfuse/tracing": "^4.4.9",
|
|
||||||
"@next/third-parties": "^16.0.6",
|
"@next/third-parties": "^16.0.6",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
|
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
"ai": "^5.0.89",
|
"ai": "^5.0.89",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-tiktoken": "^1.0.21",
|
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
"motion": "^12.23.25",
|
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"ollama-ai-provider-v2": "^1.5.4",
|
"ollama-ai-provider-v2": "^1.5.4",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"react": "^19.1.2",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.1.2",
|
"react-dom": "^19.0.0",
|
||||||
"react-drawio": "^1.0.3",
|
"react-drawio": "^1.0.3",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-resizable-panels": "^3.0.6",
|
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.7",
|
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unpdf": "^1.4.0",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
|
||||||
"*.{js,ts,jsx,tsx,json,css}": [
|
|
||||||
"biome check --write --no-errors-on-unmatched",
|
|
||||||
"biome check --no-errors-on-unmatched"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
||||||
"@biomejs/biome": "2.3.8",
|
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
|
||||||
"@types/node": "^20",
|
"@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",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-next": "16.0.5",
|
"eslint-config-next": "16.0.5",
|
||||||
"husky": "^9.1.7",
|
|
||||||
"lint-staged": "^16.2.7",
|
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: ["@tailwindcss/postcss"],
|
||||||
}
|
};
|
||||||
|
|
||||||
export default config
|
export default config;
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
Here is an extended summary of the paper **"Chain-of-Thought Prompting Elicits Reasoning in Large Language Models"** by Jason Wei, et al. This detailed overview covers the background, methodology, extensive experimental results, emergent properties, and qualitative analysis found in the study.
|
|
||||||
|
|
||||||
### **1. Introduction and Motivation**
|
|
||||||
The paper addresses a significant limitation in Large Language Models (LLMs): while scaling up model size (increasing parameters) has revolutionized performance on standard NLP tasks, it has not proven sufficient for challenging logical tasks such as arithmetic, commonsense, and symbolic reasoning.
|
|
||||||
|
|
||||||
Traditional techniques to solve these problems fell into two camps:
|
|
||||||
1. **Finetuning:** Training models manually with large datasets of explanations (expensive and task-specific).
|
|
||||||
2. **Standard Few-Shot Prompting:** Providing input-output pairs (e.g., Question $\rightarrow$ Answer) without explaining *how* the answer was derived. This often fails on multi-step problems.
|
|
||||||
|
|
||||||
The authors introduce **Chain-of-Thought (CoT) Prompting**, a simple method that combines the strengths of both approaches. It leverages the model's existing capabilities to generate natural language rationales without requiring any model parameter updates (finetuning).
|
|
||||||
|
|
||||||
### **2. Methodology: What is Chain-of-Thought?**
|
|
||||||
The core innovation is changing the structure of the "exemplars" (the few-shot examples included in the prompt).
|
|
||||||
* **Standard Prompting:** The model is shown a question and an immediate answer.
|
|
||||||
* *Q: Roger has 5 balls. He buys 2 cans of 3 balls. How many now?*
|
|
||||||
* *A: 11.*
|
|
||||||
* **Chain-of-Thought Prompting:** The model is shown a question, followed by a series of intermediate natural language reasoning steps that lead to the answer.
|
|
||||||
* *A: Roger started with 5 balls. 2 cans of 3 tennis balls each is 6 tennis balls. 5 + 6 = 11. The answer is 11.*
|
|
||||||
|
|
||||||
By interacting with the model using this format, the LLM learns to generate its own "thought process" for new, unseen questions. This allows the model to decompose complex problems into manageable intermediate steps.
|
|
||||||
|
|
||||||
### **3. Experimental Setup**
|
|
||||||
The researchers evaluated CoT prompting on several large language models, including **GPT-3 (175B)**, **LaMDA (137B)**, **PaLM (540B)**, **UL2 (20B)**, and **Codex**. They tested across three distinct domains of reasoning:
|
|
||||||
* **Arithmetic Reasoning:** Using benchmarks like **GSM8K** (math word problems), **SVAMP**, **ASDiv**, **AQuA**, and **MAWPS**.
|
|
||||||
* **Commonsense Reasoning:** Using datasets like **CSQA**, **StrategyQA**, **Date Understanding**, and **Sports Understanding**.
|
|
||||||
* **Symbolic Reasoning:** Using tasks like **Last Letter Concatenation** and **Coin Flip** tracking (determining if a coin is heads or tails after a sequence of flips).
|
|
||||||
|
|
||||||
### **4. Key Findings and Results**
|
|
||||||
|
|
||||||
#### **Arithmetic Reasoning**
|
|
||||||
The results on math word problems were striking. Standard prompting struggled significantly, often exhibiting a flat scaling curve (performance didn't improve much even as models got bigger).
|
|
||||||
* **Performance Jump:** On the difficult **GSM8K** benchmark, **PaLM 540B** with CoT prompting achieved **56.9%** accuracy, compared to just 17.9% with standard prompting.
|
|
||||||
* **Surpassing State-of-the-Art:** PaLM 540B with CoT outperformed a previously finetuned GPT-3 model (55%), establishing a new state-of-the-art without needing a training set.
|
|
||||||
* **Calculator Integration:** The authors noted that some errors were simple calculation mistakes in otherwise correct logic. By hooking the CoT output into an external Python calculator, accuracy on GSM8K rose further to **58.6%**.
|
|
||||||
|
|
||||||
#### **Commonsense Reasoning**
|
|
||||||
CoT prompting improved performance on tasks requiring background knowledge and physical intuition.
|
|
||||||
* **StrategyQA:** PaLM 540B achieved **75.6%** accuracy via CoT, beating the prior state-of-the-art (69.4%).
|
|
||||||
* **Sports Understanding:** The model achieved **95.4%** accuracy, surpassing the performance of an unaided sports enthusiast (84%).
|
|
||||||
* The gains were minimal on CSQA, likely because many questions in that dataset did not require multi-step logic.
|
|
||||||
|
|
||||||
#### **Symbolic Reasoning and Generalization**
|
|
||||||
A unique strength of CoT was enabling **Out-of-Domain (OOD) Generalization**.
|
|
||||||
* In the **Coin Flip** task, the models were given examples with only 2 flips. However, using CoT, the models could successfully track coins flipped 3 or 4 times.
|
|
||||||
* Standard prompting failed completely on these longer sequences, while CoT allowed the model to repeat the logical steps as many times as necessary to reach the solution.
|
|
||||||
|
|
||||||
### **5. Emergent Ability of Scale**
|
|
||||||
One of the paper's most critical insights is that CoT reasoning is an **emergent ability** that depends on model size.
|
|
||||||
* **Small Models (<10B parameters):** CoT prompting provided **no benefit** and often hurt performance. Small models produced fluent but illogical chains of thought (hallucinations) or suffered from repetition.
|
|
||||||
* **Large Models (~100B+ parameters):** The ability to reason sequentially emerges at this scale. The performance gains from CoT are negligible for small models but increase dramatically for models like GPT-3 (175B) and PaLM (540B).
|
|
||||||
|
|
||||||
### **6. Why Does It Work? (Ablation Studies)**
|
|
||||||
To ensure the improvement was due to the reasoning steps and not other factors, the authors conducted three specific ablations:
|
|
||||||
1. **Equation Only:** They prompted the model to output just the math equation without words. This performed worse than CoT, suggesting that natural language helps the model "understand" the question semantics.
|
|
||||||
2. **Variable Compute:** They prompted the model to output dots (...) to consume compute time before answering. This yielded no improvement, proving that the *content* of the reasoning steps matters, not just the extra tokens.
|
|
||||||
3. **Reasoning After Answer:** They asked the model to give the answer first, then the explanation. This performed about the same as the baseline, proving that the chain of thought must come *before* the answer to guide the model's inference process.
|
|
||||||
|
|
||||||
### **7. Error Analysis and Robustness**
|
|
||||||
The authors manually analyzed errors made by the models.
|
|
||||||
* **Error Types:** In math problems, errors were categorized as **Semantic Understanding** (misunderstanding the question), **One-Step Missing** (skipping a logical step), or **Calculation Errors**.
|
|
||||||
* **Impact of Scale:** Scaling from PaLM 62B to PaLM 540B significantly reduced semantic and missing-step errors, confirming that larger models are better at logic, not just memorization.
|
|
||||||
* **Robustness:** The method proved robust to different annotators (different people writing the prompts) and different specific examples, though, like all prompting, different prompt styles did result in some variance.
|
|
||||||
|
|
||||||
### **Conclusion**
|
|
||||||
The paper establishes Chain-of-Thought prompting as a powerful paradigm for unlocking the reasoning potential of Large Language Models. By simply asking the model to "show its work," researchers can elicit complex logical behaviors that were previously thought to require specialized architectures or extensive finetuning. The work highlights that reasoning is an emergent capability of sufficiently large language models.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="36" viewBox="0 0 140 36">
|
|
||||||
<rect width="140" height="36" rx="8" fill="#6366f1"/>
|
|
||||||
<text x="70" y="24" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="15" font-weight="600" fill="white" text-anchor="middle">🚀 Live Demo</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 340 B |
@@ -1,33 +1,41 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
"allowJs": true,
|
"dom",
|
||||||
"skipLibCheck": true,
|
"dom.iterable",
|
||||||
"strict": true,
|
"esnext"
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
".next/dev/types/**/*.ts"
|
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user