mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 23:02:31 +08:00
Compare commits
25 Commits
feat/add-d
...
feat/langf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36bf127d9 | ||
|
|
30b598d960 | ||
|
|
d84edb529c | ||
|
|
39322c2793 | ||
|
|
110cccb09c | ||
|
|
5021076864 | ||
|
|
efdf4f2b90 | ||
|
|
45f74df349 | ||
|
|
a61d37c818 | ||
|
|
c0cd393baa | ||
|
|
595f24857a | ||
|
|
33fed6fa9f | ||
|
|
a8e627f1f8 | ||
|
|
c458947553 | ||
|
|
443a937370 | ||
|
|
3f5cdd807d | ||
|
|
894740ba58 | ||
|
|
271f3b0f58 | ||
|
|
bc0f767ad7 | ||
|
|
61ef41addf | ||
|
|
5d38ed59eb | ||
|
|
53754e627a | ||
|
|
bca80c0856 | ||
|
|
e2adfb49aa | ||
|
|
af3173623a |
60
.dockerignore
Normal file
60
.dockerignore
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Operating System
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
!env.example
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.log
|
||||||
|
.cache
|
||||||
|
.turbo
|
||||||
|
|
||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: dayuanjiang
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
66
.github/workflows/docker-build.yml
vendored
Normal file
66
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Docker Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=sha,prefix=sha-
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
55
Dockerfile
Normal file
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Multi-stage Dockerfile for Next.js
|
||||||
|
|
||||||
|
# Stage 1: Install dependencies
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Stage 2: Build application
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy node_modules from deps stage
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Disable Next.js telemetry during build
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Build Next.js application (standalone mode)
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 3: Production runtime
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Copy standalone build output
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|
||||||
78
README.md
78
README.md
@@ -1,11 +1,24 @@
|
|||||||
# Next AI Draw.io
|
# Next AI Draw.io
|
||||||
|
|
||||||
A next.js web application that integrates AI capabilities with draw.io diagrams. This app allows you to create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
<div align="center">
|
||||||
|
|
||||||
|
**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**
|
||||||
|
|
||||||
|
English | [中文](./README_CN.md) | [日本語](./README_JA.md)
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
|
[🚀 Live Demo](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
</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.
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
Demo site: [https://next-ai-draw-io.vercel.app](https://next-ai-draw-io.vercel.app)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands
|
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands
|
||||||
@@ -21,6 +34,13 @@ Here are some example prompts and their generated diagrams:
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top" align="center">
|
||||||
|
<strong>Animated transformer connectors</strong><br />
|
||||||
|
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
||||||
|
<img src="./public/animated_connectors.svg" alt="Transformer Architecture with Animated Connectors" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>GCP architecture diagram</strong><br />
|
<strong>GCP architecture diagram</strong><br />
|
||||||
@@ -40,16 +60,9 @@ Here are some example prompts and their generated diagrams:
|
|||||||
<img src="./public/azure_demo.svg" alt="Azure Architecture Diagram" width="480" />
|
<img src="./public/azure_demo.svg" alt="Azure Architecture Diagram" width="480" />
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
<strong>Animated transformer connectors</strong><br />
|
|
||||||
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
|
||||||
<img src="./public/animated_connectors.svg" alt="Transformer Architecture with Animated Connectors" width="480" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" valign="top" align="center">
|
|
||||||
<strong>Cat sketch prompt</strong><br />
|
<strong>Cat sketch prompt</strong><br />
|
||||||
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||||
<img src="./public/cat_demo.svg" alt="Cat Drawing" width="260" />
|
<img src="./public/cat_demo.svg" alt="Cat Drawing" width="240" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -60,7 +73,7 @@ Here are some example prompts and their generated diagrams:
|
|||||||
The application uses the following technologies:
|
The application uses the following technologies:
|
||||||
|
|
||||||
- **Next.js**: For the frontend framework and routing
|
- **Next.js**: For the frontend framework and routing
|
||||||
- **@ai-sdk/react**: For the chat interface and AI interactions
|
- **Vercel AI SDK** (`ai` + `@ai-sdk/*`): For streaming AI responses and multi-provider support
|
||||||
- **react-drawio**: For diagram representation and manipulation
|
- **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.
|
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
||||||
@@ -80,6 +93,26 @@ Note that `claude-sonnet-4-5` has trained on draw.io diagrams with AWS logos, so
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
### Run with Docker (Recommended)
|
||||||
|
|
||||||
|
If you just want to run it locally, the best way is to use Docker.
|
||||||
|
|
||||||
|
First, install Docker if you haven't already: [Get Docker](https://docs.docker.com/get-docker/)
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e AI_PROVIDER=openai \
|
||||||
|
-e AI_MODEL=gpt-4o \
|
||||||
|
-e OPENAI_API_KEY=your_api_key \
|
||||||
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
@@ -135,14 +168,19 @@ Be sure to **set the environment variables** in the Vercel dashboard as you did
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
app/ # Next.js application routes and pages
|
app/ # Next.js App Router
|
||||||
extract_xml.ts # Utilities for XML processing
|
api/chat/ # Chat API endpoint with AI tools
|
||||||
|
page.tsx # Main page with DrawIO embed
|
||||||
components/ # React components
|
components/ # React components
|
||||||
chat-input.tsx # User input component for AI interaction
|
chat-panel.tsx # Chat interface with diagram control
|
||||||
chatPanel.tsx # Chat interface with diagram control
|
chat-input.tsx # User input component with file upload
|
||||||
|
history-dialog.tsx # Diagram version history viewer
|
||||||
ui/ # UI components (buttons, cards, etc.)
|
ui/ # UI components (buttons, cards, etc.)
|
||||||
|
contexts/ # React context providers
|
||||||
|
diagram-context.tsx # Global diagram state management
|
||||||
lib/ # Utility functions and helpers
|
lib/ # Utility functions and helpers
|
||||||
utils.ts # General utilities including XML conversion
|
ai-providers.ts # Multi-provider AI configuration
|
||||||
|
utils.ts # XML processing and conversion utilities
|
||||||
public/ # Static assets including example images
|
public/ # Static assets including example images
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -154,12 +192,10 @@ public/ # Static assets including example images
|
|||||||
- [x] Solve the bug that generation will fail for session that longer than 60s.
|
- [x] Solve the bug that generation will fail for session that longer than 60s.
|
||||||
- [ ] Add API config on the UI.
|
- [ ] Add API config on the UI.
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
|
||||||
|
|
||||||
## 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!
|
||||||
|
|
||||||
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:
|
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:
|
||||||
|
|
||||||
- Email: me[at]jiang.jp
|
- Email: me[at]jiang.jp
|
||||||
|
|||||||
207
README_CN.md
Normal file
207
README_CN.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Next AI Draw.io
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**AI驱动的图表创建工具 - 对话、绘制、可视化**
|
||||||
|
|
||||||
|
[English](./README.md) | 中文 | [日本語](./README_JA.md)
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
|
[🚀 在线演示](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **LLM驱动的图表创建**:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
||||||
|
- **基于图像的图表复制**:上传现有图表或图像,让AI自动复制和增强
|
||||||
|
- **图表历史记录**:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本
|
||||||
|
- **交互式聊天界面**:与AI实时对话来完善您的图表
|
||||||
|
- **AWS架构图支持**:专门支持生成AWS架构图
|
||||||
|
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
|
||||||
|
## **示例**
|
||||||
|
|
||||||
|
以下是一些示例提示词及其生成的图表:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top" align="center">
|
||||||
|
<strong>动画Transformer连接器</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
|
||||||
|
<img src="./public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>GCP架构图</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
|
<img src="./public/gcp_demo.svg" alt="GCP架构图" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>AWS架构图</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
|
<img src="./public/aws_demo.svg" alt="AWS架构图" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>Azure架构图</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
|
<img src="./public/azure_demo.svg" alt="Azure架构图" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>猫咪素描</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
|
||||||
|
<img src="./public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
本应用使用以下技术:
|
||||||
|
|
||||||
|
- **Next.js**:用于前端框架和路由
|
||||||
|
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):用于流式AI响应和多提供商支持
|
||||||
|
- **react-drawio**:用于图表表示和操作
|
||||||
|
|
||||||
|
图表以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架构图,这是最佳选择。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 使用Docker运行(推荐)
|
||||||
|
|
||||||
|
如果您只想在本地运行,最好的方式是使用Docker。
|
||||||
|
|
||||||
|
首先,如果您还没有安装Docker,请先安装:[获取Docker](https://docs.docker.com/get-docker/)
|
||||||
|
|
||||||
|
然后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e AI_PROVIDER=openai \
|
||||||
|
-e AI_MODEL=gpt-4o \
|
||||||
|
-e OPENAI_API_KEY=your_api_key \
|
||||||
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
|
||||||
|
|
||||||
|
请根据您首选的AI提供商配置替换环境变量。可用选项请参阅[多提供商支持](#多提供商支持)。
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
1. 克隆仓库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DayuanJiang/next-ai-draw-io
|
||||||
|
cd next-ai-draw-io
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# 或
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 配置您的AI提供商:
|
||||||
|
|
||||||
|
在根目录创建 `.env.local` 文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env.local` 并配置您选择的提供商:
|
||||||
|
|
||||||
|
- 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||||
|
- 将 `AI_MODEL` 设置为您要使用的特定模型
|
||||||
|
- 添加您的提供商所需的API密钥
|
||||||
|
|
||||||
|
请参阅上面的[多提供商支持](#多提供商支持)部分了解特定提供商的配置示例。
|
||||||
|
|
||||||
|
4. 运行开发服务器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 在浏览器中打开 [http://localhost:3000](http://localhost:3000) 查看应用。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。
|
||||||
|
|
||||||
|
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
|
||||||
|
|
||||||
|
或者您可以通过此按钮部署:
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
请确保在Vercel控制台中**设置环境变量**,就像您在本地 `.env.local` 文件中所做的那样。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
app/ # Next.js App Router
|
||||||
|
api/chat/ # 带AI工具的聊天API端点
|
||||||
|
page.tsx # 带DrawIO嵌入的主页面
|
||||||
|
components/ # React组件
|
||||||
|
chat-panel.tsx # 带图表控制的聊天界面
|
||||||
|
chat-input.tsx # 带文件上传的用户输入组件
|
||||||
|
history-dialog.tsx # 图表版本历史查看器
|
||||||
|
ui/ # UI组件(按钮、卡片等)
|
||||||
|
contexts/ # React上下文提供者
|
||||||
|
diagram-context.tsx # 全局图表状态管理
|
||||||
|
lib/ # 工具函数和辅助程序
|
||||||
|
ai-providers.ts # 多提供商AI配置
|
||||||
|
utils.ts # XML处理和转换工具
|
||||||
|
public/ # 静态资源包括示例图片
|
||||||
|
```
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
|
||||||
|
- [x] 允许LLM修改XML而不是每次从头生成
|
||||||
|
- [x] 提高形状流式更新的流畅度
|
||||||
|
- [x] 添加多AI提供商支持(OpenAI, Anthropic, Google, Azure, Ollama)
|
||||||
|
- [x] 解决超过60秒的会话生成失败的bug
|
||||||
|
- [ ] 在UI上添加API配置
|
||||||
|
|
||||||
|
## 支持与联系
|
||||||
|
|
||||||
|
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
||||||
|
|
||||||
|
如需支持或咨询,请在GitHub仓库上提交issue或联系维护者:
|
||||||
|
|
||||||
|
- 邮箱:me[at]jiang.jp
|
||||||
|
|
||||||
|
## Star历史
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
|
||||||
|
|
||||||
|
---
|
||||||
207
README_JA.md
Normal file
207
README_JA.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Next AI Draw.io
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**
|
||||||
|
|
||||||
|
[English](./README.md) | [中文](./README_CN.md) | 日本語
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
|
[🚀 ライブデモ](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
|
||||||
|
- **LLM搭載のダイアグラム作成**:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
||||||
|
- **画像ベースのダイアグラム複製**:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
|
||||||
|
- **ダイアグラム履歴**:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
|
||||||
|
- **インタラクティブなチャットインターフェース**:AIとリアルタイムでコミュニケーションしてダイアグラムを改善
|
||||||
|
- **AWSアーキテクチャダイアグラムサポート**:AWSアーキテクチャダイアグラムの生成を専門的にサポート
|
||||||
|
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
|
||||||
|
## **例**
|
||||||
|
|
||||||
|
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top" align="center">
|
||||||
|
<strong>アニメーションTransformerコネクタ</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
|
||||||
|
<img src="./public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>GCPアーキテクチャ図</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
|
<img src="./public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>AWSアーキテクチャ図</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
|
<img src="./public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>Azureアーキテクチャ図</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
|
<img src="./public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>猫のスケッチ</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
|
||||||
|
<img src="./public/cat_demo.svg" alt="猫の絵" width="240" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 仕組み
|
||||||
|
|
||||||
|
本アプリケーションは以下の技術を使用しています:
|
||||||
|
|
||||||
|
- **Next.js**:フロントエンドフレームワークとルーティング
|
||||||
|
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
||||||
|
- **react-drawio**:ダイアグラムの表現と操作
|
||||||
|
|
||||||
|
ダイアグラムは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アーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
|
|
||||||
|
## はじめに
|
||||||
|
|
||||||
|
### Dockerで実行(推奨)
|
||||||
|
|
||||||
|
ローカルで実行したいだけなら、Dockerを使用するのが最も簡単です。
|
||||||
|
|
||||||
|
まず、Dockerをインストールしていない場合はインストールしてください:[Dockerを入手](https://docs.docker.com/get-docker/)
|
||||||
|
|
||||||
|
次に実行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e AI_PROVIDER=openai \
|
||||||
|
-e AI_MODEL=gpt-4o \
|
||||||
|
-e OPENAI_API_KEY=your_api_key \
|
||||||
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
ブラウザで [http://localhost:3000](http://localhost:3000) を開いてください。
|
||||||
|
|
||||||
|
環境変数はお好みのAIプロバイダー設定に置き換えてください。利用可能なオプションについては[マルチプロバイダーサポート](#マルチプロバイダーサポート)を参照してください。
|
||||||
|
|
||||||
|
### インストール
|
||||||
|
|
||||||
|
1. リポジトリをクローン:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DayuanJiang/next-ai-draw-io
|
||||||
|
cd next-ai-draw-io
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 依存関係をインストール:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# または
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. AIプロバイダーを設定:
|
||||||
|
|
||||||
|
ルートディレクトリに`.env.local`ファイルを作成:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env.local`を編集して選択したプロバイダーを設定:
|
||||||
|
|
||||||
|
- `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
|
||||||
|
- `AI_MODEL`を使用する特定のモデルに設定
|
||||||
|
- プロバイダーに必要なAPIキーを追加
|
||||||
|
|
||||||
|
プロバイダー固有の設定例については、上記の[マルチプロバイダーサポート](#マルチプロバイダーサポート)セクションを参照してください。
|
||||||
|
|
||||||
|
4. 開発サーバーを起動:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. ブラウザで[http://localhost:3000](http://localhost:3000)を開いてアプリケーションを確認。
|
||||||
|
|
||||||
|
## デプロイ
|
||||||
|
|
||||||
|
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。
|
||||||
|
|
||||||
|
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
|
||||||
|
|
||||||
|
または、このボタンでデプロイできます:
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
ローカルの`.env.local`ファイルと同様に、Vercelダッシュボードで**環境変数を設定**してください。
|
||||||
|
|
||||||
|
## プロジェクト構造
|
||||||
|
|
||||||
|
```
|
||||||
|
app/ # Next.js App Router
|
||||||
|
api/chat/ # AIツール付きチャットAPIエンドポイント
|
||||||
|
page.tsx # DrawIO埋め込み付きメインページ
|
||||||
|
components/ # Reactコンポーネント
|
||||||
|
chat-panel.tsx # ダイアグラム制御付きチャットインターフェース
|
||||||
|
chat-input.tsx # ファイルアップロード付きユーザー入力コンポーネント
|
||||||
|
history-dialog.tsx # ダイアグラムバージョン履歴ビューア
|
||||||
|
ui/ # UIコンポーネント(ボタン、カードなど)
|
||||||
|
contexts/ # Reactコンテキストプロバイダー
|
||||||
|
diagram-context.tsx # グローバルダイアグラム状態管理
|
||||||
|
lib/ # ユーティリティ関数とヘルパー
|
||||||
|
ai-providers.ts # マルチプロバイダーAI設定
|
||||||
|
utils.ts # XML処理と変換ユーティリティ
|
||||||
|
public/ # サンプル画像を含む静的アセット
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [x] LLMが毎回ゼロから生成する代わりにXMLを修正できるようにする
|
||||||
|
- [x] シェイプストリーミング更新の滑らかさを改善
|
||||||
|
- [x] 複数のAIプロバイダーサポートを追加(OpenAI, Anthropic, Google, Azure, Ollama)
|
||||||
|
- [x] 60秒以上のセッションで生成が失敗するバグを解決
|
||||||
|
- [ ] UIにAPI設定を追加
|
||||||
|
|
||||||
|
## サポート&お問い合わせ
|
||||||
|
|
||||||
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
||||||
|
|
||||||
|
サポートやお問い合わせについては、GitHubリポジトリでissueを開くか、メンテナーにご連絡ください:
|
||||||
|
|
||||||
|
- メール:me[at]jiang.jp
|
||||||
|
|
||||||
|
## スター履歴
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
|
||||||
|
|
||||||
|
---
|
||||||
205
app/about/cn/page.tsx
Normal file
205
app/about/cn/page.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FaGithub } from "react-icons/fa";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "关于 - Next AI Draw.io",
|
||||||
|
description: "AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
|
||||||
|
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutCN() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<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="flex items-center justify-between">
|
||||||
|
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
||||||
|
Next AI Draw.io
|
||||||
|
</Link>
|
||||||
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
|
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
||||||
|
编辑器
|
||||||
|
</Link>
|
||||||
|
<Link href="/about/cn" className="text-blue-600 font-semibold">
|
||||||
|
关于
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
aria-label="在GitHub上查看"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article className="prose prose-lg max-w-none">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
||||||
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
|
AI驱动的图表创建工具 - 对话、绘制、可视化
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
|
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link href="/about/cn" className="text-blue-600 font-semibold">中文</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600">日本語</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-amber-800">
|
||||||
|
本应用设计运行于 Claude Opus 4.5 以获得最佳性能。但由于流量超出预期,运行顶级模型的成本变得难以承受。为避免服务中断并控制成本,我已将后端切换至 Claude Haiku 4.5。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-700">
|
||||||
|
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">功能特性</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li><strong>LLM驱动的图表创建</strong>:利用大语言模型通过自然语言命令直接创建和操作draw.io图表</li>
|
||||||
|
<li><strong>基于图像的图表复制</strong>:上传现有图表或图像,让AI自动复制和增强</li>
|
||||||
|
<li><strong>图表历史记录</strong>:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本</li>
|
||||||
|
<li><strong>交互式聊天界面</strong>:与AI实时对话来完善您的图表</li>
|
||||||
|
<li><strong>AWS架构图支持</strong>:专门支持生成AWS架构图</li>
|
||||||
|
<li><strong>动画连接器</strong>:在图表元素之间创建动态动画连接器,实现更好的可视化效果</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">示例</h2>
|
||||||
|
<p className="text-gray-700 mb-6">以下是一些示例提示词及其生成的图表:</p>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Animated Transformer */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">动画Transformer连接器</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
<strong>提示词:</strong> 给我一个带有<strong>动画连接器</strong>的Transformer架构图。
|
||||||
|
</p>
|
||||||
|
<Image src="/animated_connectors.svg" alt="带动画连接器的Transformer架构" width={480} height={360} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cloud Architecture Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP架构图</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 使用<strong>GCP图标</strong>生成一个GCP架构图。用户连接到托管在实例上的前端。
|
||||||
|
</p>
|
||||||
|
<Image src="/gcp_demo.svg" alt="GCP架构图" width={400} height={300} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS架构图</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 使用<strong>AWS图标</strong>生成一个AWS架构图。用户连接到托管在实例上的前端。
|
||||||
|
</p>
|
||||||
|
<Image src="/aws_demo.svg" alt="AWS架构图" width={400} height={300} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure架构图</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 使用<strong>Azure图标</strong>生成一个Azure架构图。用户连接到托管在实例上的前端。
|
||||||
|
</p>
|
||||||
|
<Image src="/azure_demo.svg" alt="Azure架构图" width={400} height={300} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">猫咪素描</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 给我画一只可爱的猫。
|
||||||
|
</p>
|
||||||
|
<Image src="/cat_demo.svg" alt="猫咪绘图" width={240} height={240} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">工作原理</h2>
|
||||||
|
<p className="text-gray-700 mb-4">本应用使用以下技术:</p>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li><strong>Next.js</strong>:用于前端框架和路由</li>
|
||||||
|
<li><strong>Vercel AI SDK</strong>(<code>ai</code> + <code>@ai-sdk/*</code>):用于流式AI响应和多提供商支持</li>
|
||||||
|
<li><strong>react-drawio</strong>:用于图表表示和操作</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Multi-Provider Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">多提供商支持</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
|
<li>AWS Bedrock(默认)</li>
|
||||||
|
<li>OpenAI / OpenAI兼容API(通过 <code>OPENAI_BASE_URL</code>)</li>
|
||||||
|
<li>Anthropic</li>
|
||||||
|
<li>Google AI</li>
|
||||||
|
<li>Azure OpenAI</li>
|
||||||
|
<li>Ollama</li>
|
||||||
|
<li>OpenRouter</li>
|
||||||
|
<li>DeepSeek</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
注意:<code>claude-sonnet-4-5</code> 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Support */}
|
||||||
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900">支持与联系</h2>
|
||||||
|
<iframe
|
||||||
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
|
title="Sponsor DayuanJiang"
|
||||||
|
height="32"
|
||||||
|
width="114"
|
||||||
|
style={{ border: 0, borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
如果您觉得这个项目有用,请考虑{" "}
|
||||||
|
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
赞助
|
||||||
|
</a>{" "}
|
||||||
|
来帮助托管在线演示站点!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 mt-2">
|
||||||
|
如需支持或咨询,请在{" "}
|
||||||
|
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
GitHub仓库
|
||||||
|
</a>{" "}
|
||||||
|
上提交issue或联系:me[at]jiang.jp
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
打开编辑器
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<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">
|
||||||
|
<p className="text-center text-gray-600 text-sm">
|
||||||
|
Next AI Draw.io - 开源AI驱动的图表生成器
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
205
app/about/ja/page.tsx
Normal file
205
app/about/ja/page.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FaGithub } from "react-icons/fa";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "概要 - Next AI Draw.io",
|
||||||
|
description: "AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
|
||||||
|
keywords: ["AIダイアグラム", "draw.io", "AWSアーキテクチャ", "GCPダイアグラム", "Azureダイアグラム", "LLM"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutJA() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<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="flex items-center justify-between">
|
||||||
|
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
||||||
|
Next AI Draw.io
|
||||||
|
</Link>
|
||||||
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
|
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
||||||
|
エディタ
|
||||||
|
</Link>
|
||||||
|
<Link href="/about/ja" className="text-blue-600 font-semibold">
|
||||||
|
概要
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
aria-label="GitHubで見る"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article className="prose prose-lg max-w-none">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
||||||
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
|
AI搭載のダイアグラム作成ツール - チャット、描画、可視化
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
|
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600">中文</Link>
|
||||||
|
<span className="text-gray-400">|</span>
|
||||||
|
<Link href="/about/ja" className="text-blue-600 font-semibold">日本語</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-amber-800">
|
||||||
|
本アプリは最高のパフォーマンスを発揮するため、Claude Opus 4.5 で動作するよう設計されています。しかし、予想以上のトラフィックにより、最上位モデルの運用コストが負担となっています。サービスの中断を避け、コストを管理するため、バックエンドを Claude Haiku 4.5 に切り替えました。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-700">
|
||||||
|
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">機能</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li><strong>LLM搭載のダイアグラム作成</strong>:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作</li>
|
||||||
|
<li><strong>画像ベースのダイアグラム複製</strong>:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化</li>
|
||||||
|
<li><strong>ダイアグラム履歴</strong>:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能</li>
|
||||||
|
<li><strong>インタラクティブなチャットインターフェース</strong>:AIとリアルタイムでコミュニケーションしてダイアグラムを改善</li>
|
||||||
|
<li><strong>AWSアーキテクチャダイアグラムサポート</strong>:AWSアーキテクチャダイアグラムの生成を専門的にサポート</li>
|
||||||
|
<li><strong>アニメーションコネクタ</strong>:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">例</h2>
|
||||||
|
<p className="text-gray-700 mb-6">以下はいくつかのプロンプト例と生成されたダイアグラムです:</p>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Animated Transformer */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">アニメーションTransformerコネクタ</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
<strong>プロンプト:</strong> <strong>アニメーションコネクタ</strong>付きのTransformerアーキテクチャ図を作成してください。
|
||||||
|
</p>
|
||||||
|
<Image src="/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width={480} height={360} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cloud Architecture Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCPアーキテクチャ図</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong> <strong>GCPアイコン</strong>を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
|
</p>
|
||||||
|
<Image src="/gcp_demo.svg" alt="GCPアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWSアーキテクチャ図</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong> <strong>AWSアイコン</strong>を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
|
</p>
|
||||||
|
<Image src="/aws_demo.svg" alt="AWSアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azureアーキテクチャ図</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong> <strong>Azureアイコン</strong>を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
|
</p>
|
||||||
|
<Image src="/azure_demo.svg" alt="Azureアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">猫のスケッチ</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong> かわいい猫を描いてください。
|
||||||
|
</p>
|
||||||
|
<Image src="/cat_demo.svg" alt="猫の絵" width={240} height={240} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">仕組み</h2>
|
||||||
|
<p className="text-gray-700 mb-4">本アプリケーションは以下の技術を使用しています:</p>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li><strong>Next.js</strong>:フロントエンドフレームワークとルーティング</li>
|
||||||
|
<li><strong>Vercel AI SDK</strong>(<code>ai</code> + <code>@ai-sdk/*</code>):ストリーミングAIレスポンスとマルチプロバイダーサポート</li>
|
||||||
|
<li><strong>react-drawio</strong>:ダイアグラムの表現と操作</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Multi-Provider Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">マルチプロバイダーサポート</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
|
<li>AWS Bedrock(デフォルト)</li>
|
||||||
|
<li>OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>経由)</li>
|
||||||
|
<li>Anthropic</li>
|
||||||
|
<li>Google AI</li>
|
||||||
|
<li>Azure OpenAI</li>
|
||||||
|
<li>Ollama</li>
|
||||||
|
<li>OpenRouter</li>
|
||||||
|
<li>DeepSeek</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
注:<code>claude-sonnet-4-5</code>はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Support */}
|
||||||
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900">サポート&お問い合わせ</h2>
|
||||||
|
<iframe
|
||||||
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
|
title="Sponsor DayuanJiang"
|
||||||
|
height="32"
|
||||||
|
width="114"
|
||||||
|
style={{ border: 0, borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
||||||
|
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
スポンサー
|
||||||
|
</a>{" "}
|
||||||
|
をご検討ください!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 mt-2">
|
||||||
|
サポートやお問い合わせについては、{" "}
|
||||||
|
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
GitHubリポジトリ
|
||||||
|
</a>{" "}
|
||||||
|
でissueを開くか、ご連絡ください:me[at]jiang.jp
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
エディタを開く
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<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">
|
||||||
|
<p className="text-center text-gray-600 text-sm">
|
||||||
|
Next AI Draw.io - オープンソースAI搭載ダイアグラムジェネレーター
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
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 - AI-Powered Diagram Generator | Next AI Draw.io",
|
title: "About - Next AI Draw.io",
|
||||||
description: "Learn about Next AI Draw.io, a free AI-powered diagram creation tool. Create AWS architecture diagrams, flowcharts, and UML diagrams using Claude Sonnet and GPT-4. No login required.",
|
description: "AI-Powered Diagram Creation Tool - Chat, Draw, Visualize. Create AWS, GCP, and Azure architecture diagrams with natural language.",
|
||||||
keywords: ["about AI diagram generator", "diagram tool features", "how to create diagrams", "AI drawing tool capabilities", "draw.io integration"],
|
keywords: ["AI diagram", "draw.io", "AWS architecture", "GCP diagram", "Azure diagram", "LLM"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
@@ -13,7 +14,7 @@ export default function About() {
|
|||||||
<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-7xl 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 href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
||||||
Next AI Draw.io
|
Next AI Draw.io
|
||||||
@@ -40,295 +41,164 @@ export default function About() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
<article>
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Hero Section */}
|
{/* Title */}
|
||||||
<header className="mb-12">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
||||||
AI-Powered Diagram Generator | Create Professional Diagrams Instantly
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
</h1>
|
AI-Powered Diagram Creation Tool - Chat, Draw, Visualize
|
||||||
<p className="text-xl text-gray-600">
|
|
||||||
Free, open-source diagram creation tool powered by AI. No login required, no installation needed.
|
|
||||||
</p>
|
</p>
|
||||||
</header>
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
|
<Link href="/about" className="text-blue-600 font-semibold">English</Link>
|
||||||
{/* Introduction */}
|
<span className="text-gray-400">|</span>
|
||||||
<section className="mb-12">
|
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600">中文</Link>
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-4">What is Next AI Draw.io?</h2>
|
<span className="text-gray-400">|</span>
|
||||||
<div className="prose prose-lg max-w-none text-gray-700">
|
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600">日本語</Link>
|
||||||
<p className="mb-4">
|
|
||||||
<strong>Next AI Draw.io</strong> is a free, AI-powered diagram creation tool that integrates seamlessly with draw.io.
|
|
||||||
Generate AWS architecture diagrams, flowcharts, UML diagrams, and technical documentation diagrams using natural language
|
|
||||||
prompts. No login required, no installation needed—start creating professional diagrams instantly in your browser.
|
|
||||||
</p>
|
|
||||||
<p className="mb-4">
|
|
||||||
Our intelligent diagram generator uses advanced AI models including <strong>Claude Sonnet</strong> and <strong>GPT-4</strong> to
|
|
||||||
understand your requirements and automatically create properly structured diagrams with appropriate symbols, layouts, and connections.
|
|
||||||
Simply describe what you need, upload reference images, or ask the AI to modify existing diagrams with our targeted XML editing feature.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Whether you're a software architect designing system infrastructure, a developer documenting APIs, a business analyst creating
|
|
||||||
process flows, or a student working on technical assignments, Next AI Draw.io makes diagram creation fast, accurate, and effortless.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
{/* Key Features */}
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<section className="mb-12">
|
<p className="text-amber-800">
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Key Features</h2>
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-700">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Features</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li><strong>LLM-Powered Diagram Creation</strong>: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands</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>
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Examples</h2>
|
||||||
|
<p className="text-gray-700 mb-6">Here are some example prompts and their generated diagrams:</p>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Animated Transformer */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Animated Transformer Connectors</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
<strong>Prompt:</strong> Give me an <strong>animated connector</strong> diagram of transformer's architecture.
|
||||||
|
</p>
|
||||||
|
<Image src="/animated_connectors.svg" alt="Transformer Architecture with Animated Connectors" width={480} height={360} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP Architecture Diagram</h3>
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
AI-Powered Diagram Creation
|
<strong>Prompt:</strong> Generate a GCP architecture diagram with <strong>GCP icons</strong>. Users connect to a frontend hosted on an instance.
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Generate diagrams from natural language descriptions using Claude Sonnet or GPT-4.
|
|
||||||
Describe your diagram in plain English, and watch the AI create it with proper symbols,
|
|
||||||
layouts, and connections automatically.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Image src="/gcp_demo.svg" alt="GCP Architecture Diagram" width={400} height={300} className="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS Architecture Diagram</h3>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
<strong>Prompt:</strong> Generate an AWS architecture diagram with <strong>AWS icons</strong>. Users connect to a frontend hosted on an instance.
|
||||||
AWS Architecture Diagrams
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Create professional cloud infrastructure diagrams with AWS-style icons and layouts.
|
|
||||||
Perfect for designing EC2 instances, Lambda functions, S3 buckets, RDS databases, VPCs,
|
|
||||||
and complete AWS solution architectures.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Image src="/aws_demo.svg" alt="AWS Architecture Diagram" width={400} height={300} className="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure Architecture Diagram</h3>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
<strong>Prompt:</strong> Generate an Azure architecture diagram with <strong>Azure icons</strong>. Users connect to a frontend hosted on an instance.
|
||||||
Image-Based Diagram Replication
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Upload existing diagrams or sketches, and the AI will automatically recreate them in draw.io format.
|
|
||||||
Modify uploaded images by describing the changes you want—the AI handles the rest.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Image src="/azure_demo.svg" alt="Azure Architecture Diagram" width={400} height={300} className="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Cat Sketch</h3>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
<strong>Prompt:</strong> Draw a cute cat for me.
|
||||||
Diagram History & Version Control
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Access previous versions of your diagrams and restore any version from your session history.
|
|
||||||
Never lose work—every AI modification is saved and can be undone with a single click.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
Targeted XML Editing
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Precise diagram modifications using intelligent XML manipulation. Unlike full diagram regeneration,
|
|
||||||
targeted edits preserve your existing layout while making specific changes, ensuring consistent
|
|
||||||
and predictable results.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
Multi-Provider AI Support
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Choose between Claude Sonnet, GPT-4, and other leading AI models for optimal results.
|
|
||||||
Each model has unique strengths—select the one that best fits your diagram complexity and style.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Image src="/cat_demo.svg" alt="Cat Drawing" width={240} height={240} className="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
{/* Use Cases */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Popular Use Cases</h2>
|
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
|
||||||
<div className="bg-blue-50 p-6 rounded-lg border border-blue-200">
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">AWS Cloud Architecture</h3>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
Design scalable cloud infrastructure with EC2 instances, Lambda functions, S3 storage,
|
|
||||||
RDS databases, and VPC networking. Perfect for solution architects, cloud engineers,
|
|
||||||
and DevOps teams planning AWS deployments.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 italic">
|
|
||||||
Example: "Create an AWS diagram with an Application Load Balancer, two EC2 instances
|
|
||||||
in different availability zones, an RDS database, and an S3 bucket for static assets."
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-green-50 p-6 rounded-lg border border-green-200">
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Flowcharts & Process Diagrams</h3>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
Create business process flows, decision trees, workflow diagrams, and algorithm flowcharts
|
|
||||||
for documentation, presentations, and process optimization. Ideal for business analysts,
|
|
||||||
project managers, and operations teams.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 italic">
|
|
||||||
Example: "Draw a flowchart for user authentication: check if user exists, verify password,
|
|
||||||
generate JWT token on success, show error message on failure."
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-purple-50 p-6 rounded-lg border border-purple-200">
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">System Design & UML Diagrams</h3>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
Generate system architecture diagrams, class diagrams, sequence diagrams, and
|
|
||||||
entity-relationship diagrams for software projects. Essential for software engineers,
|
|
||||||
system designers, and technical documentation.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 italic">
|
|
||||||
Example: "Create a class diagram for an e-commerce system with User, Product, Order,
|
|
||||||
and Payment classes showing their relationships and key methods."
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<section className="mb-12 bg-white p-8 rounded-lg border border-gray-200">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">How It Works</h2>
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">How to Use Next AI Draw.io</h2>
|
<p className="text-gray-700 mb-4">The application uses the following technologies:</p>
|
||||||
<div className="space-y-6">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<div className="flex items-start">
|
<li><strong>Next.js</strong>: For the frontend framework and routing</li>
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
<li><strong>Vercel AI SDK</strong> (<code>ai</code> + <code>@ai-sdk/*</code>): For streaming AI responses and multi-provider support</li>
|
||||||
1
|
<li><strong>react-drawio</strong>: For diagram representation and manipulation</li>
|
||||||
</div>
|
</ul>
|
||||||
<div>
|
<p className="text-gray-700 mt-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Open the Editor</h3>
|
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
||||||
<p className="text-gray-700">
|
</p>
|
||||||
Navigate to the main page and you'll see the draw.io editor with an AI chat panel on the right.
|
|
||||||
No account creation or login required—start immediately.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
{/* Multi-Provider Support */}
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Multi-Provider Support</h2>
|
||||||
2
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
</div>
|
<li>AWS Bedrock (default)</li>
|
||||||
<div>
|
<li>OpenAI / OpenAI-compatible APIs (via <code>OPENAI_BASE_URL</code>)</li>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Describe Your Diagram</h3>
|
<li>Anthropic</li>
|
||||||
<p className="text-gray-700">
|
<li>Google AI</li>
|
||||||
Type your diagram request in natural language. Be as detailed or as general as you like.
|
<li>Azure OpenAI</li>
|
||||||
You can also upload reference images for the AI to analyze and replicate.
|
<li>Ollama</li>
|
||||||
</p>
|
<li>OpenRouter</li>
|
||||||
</div>
|
<li>DeepSeek</li>
|
||||||
</div>
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="flex items-start">
|
{/* Support */}
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
3
|
<h2 className="text-2xl font-semibold text-gray-900">Support & Contact</h2>
|
||||||
</div>
|
<iframe
|
||||||
<div>
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">AI Generates Your Diagram</h3>
|
title="Sponsor DayuanJiang"
|
||||||
<p className="text-gray-700">
|
height="32"
|
||||||
The AI processes your request and automatically creates your diagram in seconds.
|
width="114"
|
||||||
Watch as it appears in the editor with proper symbols, layouts, and connections.
|
style={{ border: 0, borderRadius: 6 }}
|
||||||
</p>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p className="text-gray-700">
|
||||||
|
If you find this project useful, please consider{" "}
|
||||||
|
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
sponsoring
|
||||||
|
</a>{" "}
|
||||||
|
to help host the live demo site!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 mt-2">
|
||||||
|
For support or inquiries, please open an issue on the{" "}
|
||||||
|
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||||
|
GitHub repository
|
||||||
|
</a>{" "}
|
||||||
|
or contact: me[at]jiang.jp
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="flex items-start">
|
{/* CTA */}
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
<div className="mt-12 text-center">
|
||||||
4
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Refine and Export</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Request modifications using the chat, manually edit in draw.io, or export to PNG, SVG,
|
|
||||||
or XML format. Access diagram history to restore previous versions anytime.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Benefits */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Why Choose Next AI Draw.io?</h2>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">⚡</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Save Time</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Create complex diagrams in seconds instead of hours. No more dragging, aligning,
|
|
||||||
or searching for the right symbols—AI handles it all.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">🎯</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Precision Editing</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Targeted XML editing ensures changes are precise and predictable, unlike tools
|
|
||||||
that regenerate entire diagrams and lose your layout.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">🆓</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Completely Free</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
No subscriptions, no usage limits, no hidden costs. Open-source and free forever.
|
|
||||||
Use it for personal projects, work, or education.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">🔒</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Privacy First</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
No account required means your diagrams stay private. Work on sensitive
|
|
||||||
architecture designs without worrying about data storage or privacy policies.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<section className="bg-blue-600 text-white p-8 rounded-lg text-center">
|
|
||||||
<h2 className="text-3xl font-bold mb-4">Ready to Create Your First AI Diagram?</h2>
|
|
||||||
<p className="text-xl mb-6">
|
|
||||||
Start generating professional diagrams in seconds. No signup required.
|
|
||||||
</p>
|
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="inline-block bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors"
|
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Open Editor
|
Open Editor
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<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-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<div className="text-center text-gray-600 text-sm">
|
<p className="text-center text-gray-600 text-sm">
|
||||||
<p className="mb-2">
|
Next AI Draw.io - Open Source AI-Powered Diagram Generator
|
||||||
Next AI Draw.io - Free AI-Powered Diagram Generator
|
</p>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Perfect for developers, architects, students, and business analysts.
|
|
||||||
Open source. No login required.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,16 @@ function createCachedStreamResponse(xml: string): Response {
|
|||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { messages, xml } = await req.json();
|
const { messages, xml, sessionId } = await req.json();
|
||||||
|
|
||||||
|
// Get user IP for Langfuse tracking
|
||||||
|
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||||
|
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
||||||
|
|
||||||
|
// Validate sessionId for Langfuse (must be string, max 200 chars)
|
||||||
|
const validSessionId = sessionId && typeof sessionId === 'string' && sessionId.length <= 200
|
||||||
|
? sessionId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// === CACHE CHECK START ===
|
// === CACHE CHECK START ===
|
||||||
const isFirstMessage = messages.length === 1;
|
const isFirstMessage = messages.length === 1;
|
||||||
@@ -52,9 +61,8 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const systemMessage = `
|
const systemMessage = `
|
||||||
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 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 the image that user uploaded.
|
You can see the image that user uploaded.
|
||||||
Note that when you need to generate diagram about aws architecture, use **AWS 2025 icons**.
|
|
||||||
|
|
||||||
You utilize the following tools:
|
You utilize the following tools:
|
||||||
---Tool1---
|
---Tool1---
|
||||||
@@ -95,6 +103,9 @@ Layout constraints:
|
|||||||
- Avoid spreading elements too far apart horizontally - users should see the complete diagram without a page break line
|
- Avoid spreading elements too far apart horizontally - users should see the complete diagram without a page break line
|
||||||
|
|
||||||
Note that:
|
Note that:
|
||||||
|
- Use proper tool calls to generate or edit diagrams;
|
||||||
|
- never return raw XML in text responses,
|
||||||
|
- never use display_diagram to generate messages that you want to send user directly. e.g. to generate a "hello" text box when you want to greet user.
|
||||||
- Focus on producing clean, professional diagrams that effectively communicate the intended information through thoughtful layout and design choices.
|
- Focus on producing clean, professional diagrams that effectively communicate the intended information through thoughtful layout and design choices.
|
||||||
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
|
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
|
||||||
- Return XML only via tool calls, never in text responses.
|
- Return XML only via tool calls, never in text responses.
|
||||||
@@ -110,6 +121,44 @@ When using edit_diagram tool:
|
|||||||
* You may retry edit_diagram up to 3 times with adjusted search patterns
|
* You may retry edit_diagram up to 3 times with adjusted search patterns
|
||||||
* After 3 failed attempts, you MUST fall back to using display_diagram to regenerate the entire diagram
|
* After 3 failed attempts, you MUST fall back to using display_diagram to regenerate the entire diagram
|
||||||
* The error message will indicate how many retries remain
|
* The error message will indicate how many retries remain
|
||||||
|
|
||||||
|
## Draw.io XML Structure Reference
|
||||||
|
|
||||||
|
Basic structure:
|
||||||
|
\`\`\`xml
|
||||||
|
<mxGraphModel>
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<!-- All other cells go here as siblings -->
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
CRITICAL 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
|
||||||
|
|
||||||
|
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>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
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"/>
|
||||||
|
</mxCell>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Common styles:
|
||||||
|
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
||||||
|
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
||||||
|
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
@@ -217,6 +266,16 @@ ${lastMessageText}
|
|||||||
messages: [systemMessageWithCache, ...enhancedMessages],
|
messages: [systemMessageWithCache, ...enhancedMessages],
|
||||||
...(providerOptions && { providerOptions }),
|
...(providerOptions && { providerOptions }),
|
||||||
...(headers && { headers }),
|
...(headers && { headers }),
|
||||||
|
// Only enable telemetry if Langfuse is configured
|
||||||
|
...(process.env.LANGFUSE_PUBLIC_KEY && {
|
||||||
|
experimental_telemetry: {
|
||||||
|
isEnabled: true,
|
||||||
|
metadata: {
|
||||||
|
sessionId: validSessionId,
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
onFinish: ({ usage, providerMetadata }) => {
|
onFinish: ({ usage, providerMetadata }) => {
|
||||||
console.log('[Cache] Usage:', JSON.stringify({
|
console.log('[Cache] Usage:', JSON.stringify({
|
||||||
inputTokens: usage?.inputTokens,
|
inputTokens: usage?.inputTokens,
|
||||||
@@ -228,19 +287,41 @@ ${lastMessageText}
|
|||||||
tools: {
|
tools: {
|
||||||
// Client-side tool that will be executed on the client
|
// Client-side tool that will be executed on the client
|
||||||
display_diagram: {
|
display_diagram: {
|
||||||
description: `Display a diagram on draw.io. You only need to pass the nodes inside the <root> tag (including the <root> tag itself) in the XML string.
|
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
|
||||||
For example:
|
|
||||||
<root>
|
VALIDATION RULES (XML will be rejected if violated):
|
||||||
<mxCell id="0"/>
|
1. All mxCell elements must be DIRECT children of <root> - never nested
|
||||||
<mxCell id="1" parent="0"/>
|
2. Every mxCell needs a unique id
|
||||||
<mxGeometry x="20" y="20" width="100" height="100" as="geometry"/>
|
3. Every mxCell (except id="0") needs a valid parent attribute
|
||||||
<mxCell id="2" value="Hello, World!" style="shape=rectangle" parent="1">
|
4. Edge source/target must reference existing cell IDs
|
||||||
<mxGeometry x="20" y="20" width="100" height="100" as="geometry"/>
|
5. Escape special chars in values: < > & "
|
||||||
</mxCell>
|
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/>
|
||||||
</root>
|
|
||||||
- Note that when you need to generate diagram about aws architecture, use **AWS 2025 icons**.
|
Example with swimlanes and edges (note: all mxCells are siblings):
|
||||||
- If you are asked to generate animated connectors, make sure to include "flowAnimation=1" in the style of the connector elements.
|
<root>
|
||||||
`,
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
|
||||||
|
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
|
||||||
|
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- For AWS diagrams, use **AWS 2025 icons**.
|
||||||
|
- For animated connectors, add "flowAnimation=1" to edge style.
|
||||||
|
`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
xml: z.string().describe("XML string to be displayed on draw.io")
|
xml: z.string().describe("XML string to be displayed on draw.io")
|
||||||
})
|
})
|
||||||
@@ -269,15 +350,20 @@ IMPORTANT: Keep edits concise:
|
|||||||
return 'unknown error';
|
return 'unknown error';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof error === 'string') {
|
const errorString = typeof error === 'string'
|
||||||
return error;
|
? error
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error);
|
||||||
|
|
||||||
|
// Check for image not supported error (e.g., DeepSeek models)
|
||||||
|
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.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
return errorString;
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.toUIMessageStreamResponse({
|
return result.toUIMessageStreamResponse({
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ Draw.io files contain two special cells that are always present:
|
|||||||
4. Define parent relationships correctly
|
4. Define parent relationships correctly
|
||||||
5. Use `mxGeometry` elements to position shapes
|
5. Use `mxGeometry` elements to position shapes
|
||||||
6. For connectors, specify `source` and `target` attributes
|
6. For connectors, specify `source` and `target` attributes
|
||||||
|
7. **CRITICAL: All mxCell elements must be DIRECT children of `<root>`. NEVER nest mxCell inside another mxCell.**
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
@@ -234,12 +235,33 @@ To group elements, create a parent cell and set other cells' `parent` attribute
|
|||||||
|
|
||||||
### Swimlanes
|
### Swimlanes
|
||||||
|
|
||||||
Swimlanes use the `swimlane` shape style:
|
Swimlanes use the `swimlane` shape style. **IMPORTANT: All mxCell elements (swimlanes, steps, and edges) must be siblings under `<root>`. Edges are NOT nested inside swimlanes or steps.**
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<mxCell id="20" value="Swimlane 1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
<root>
|
||||||
<mxGeometry x="200" y="200" width="140" height="120" as="geometry" />
|
<mxCell id="0"/>
|
||||||
</mxCell>
|
<mxCell id="1" parent="0"/>
|
||||||
|
<!-- Swimlane 1 -->
|
||||||
|
<mxCell id="lane1" value="Frontend" style="swimlane;startSize=30;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="40" width="200" height="300" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Swimlane 2 -->
|
||||||
|
<mxCell id="lane2" value="Backend" style="swimlane;startSize=30;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="280" y="40" width="200" height="300" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Step inside lane1 (parent="lane1") -->
|
||||||
|
<mxCell id="step1" value="Send Request" style="rounded=1;" vertex="1" parent="lane1">
|
||||||
|
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Step inside lane2 (parent="lane2") -->
|
||||||
|
<mxCell id="step2" value="Process" style="rounded=1;" vertex="1" parent="lane2">
|
||||||
|
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Edge connecting step1 to step2 (sibling element, NOT nested inside steps) -->
|
||||||
|
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
|
|||||||
253
app/globals.css
253
app/globals.css
@@ -7,8 +7,8 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-mono: var(--font-geist-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);
|
||||||
@@ -45,72 +45,102 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.75rem;
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
/* Clean Light Modern Palette */
|
||||||
|
--background: oklch(0.985 0.002 240);
|
||||||
|
--foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.23 0.02 260);
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
/* Dark primary - slightly lighter */
|
||||||
--secondary: oklch(0.97 0 0);
|
--primary: oklch(0.35 0.01 260);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
/* Warm gray secondary */
|
||||||
--accent: oklch(0.97 0 0);
|
--secondary: oklch(0.96 0.005 260);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.35 0.02 260);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
/* Light muted tones */
|
||||||
--input: oklch(0.922 0 0);
|
--muted: oklch(0.965 0.005 260);
|
||||||
--ring: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.50 0.02 260);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
/* Soft lavender accent */
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--accent: oklch(0.94 0.03 280);
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--accent-foreground: oklch(0.35 0.08 270);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
/* Coral destructive */
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--destructive: oklch(0.60 0.20 25);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
/* Subtle borders */
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--border: oklch(0.92 0.01 260);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--input: oklch(0.94 0.01 260);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--ring: oklch(0.25 0.01 260);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
|
/* Chart colors - harmonious palette */
|
||||||
|
--chart-1: oklch(0.55 0.18 265);
|
||||||
|
--chart-2: oklch(0.65 0.15 170);
|
||||||
|
--chart-3: oklch(0.70 0.18 45);
|
||||||
|
--chart-4: oklch(0.60 0.20 330);
|
||||||
|
--chart-5: oklch(0.50 0.15 200);
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar: oklch(0.99 0.002 260);
|
||||||
|
--sidebar-foreground: oklch(0.23 0.02 260);
|
||||||
|
--sidebar-primary: oklch(0.55 0.18 265);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||||
|
--sidebar-accent: oklch(0.96 0.02 270);
|
||||||
|
--sidebar-accent-foreground: oklch(0.35 0.05 265);
|
||||||
|
--sidebar-border: oklch(0.93 0.01 260);
|
||||||
|
--sidebar-ring: oklch(0.55 0.18 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.15 0.015 260);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.95 0.01 260);
|
||||||
--card: oklch(0.205 0 0);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card: oklch(0.20 0.015 260);
|
||||||
--popover: oklch(0.205 0 0);
|
--card-foreground: oklch(0.95 0.01 260);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
--popover: oklch(0.20 0.015 260);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--popover-foreground: oklch(0.95 0.01 260);
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--primary: oklch(0.70 0.16 265);
|
||||||
--muted: oklch(0.269 0 0);
|
--primary-foreground: oklch(0.15 0.02 260);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
--secondary: oklch(0.25 0.015 260);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.90 0.01 260);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
--muted: oklch(0.25 0.015 260);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--muted-foreground: oklch(0.65 0.02 260);
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--accent: oklch(0.30 0.04 280);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--accent-foreground: oklch(0.90 0.03 270);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--destructive: oklch(0.65 0.22 25);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
--border: oklch(0.28 0.015 260);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--input: oklch(0.25 0.015 260);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--ring: oklch(0.70 0.16 265);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--chart-1: oklch(0.70 0.16 265);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--chart-2: oklch(0.70 0.13 170);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--chart-3: oklch(0.75 0.16 45);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--chart-4: oklch(0.70 0.18 330);
|
||||||
|
--chart-5: oklch(0.60 0.13 200);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.18 0.015 260);
|
||||||
|
--sidebar-foreground: oklch(0.95 0.01 260);
|
||||||
|
--sidebar-primary: oklch(0.70 0.16 265);
|
||||||
|
--sidebar-primary-foreground: oklch(0.15 0.02 260);
|
||||||
|
--sidebar-accent: oklch(0.25 0.03 270);
|
||||||
|
--sidebar-accent-foreground: oklch(0.90 0.02 265);
|
||||||
|
--sidebar-border: oklch(0.28 0.015 260);
|
||||||
|
--sidebar-ring: oklch(0.70 0.16 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -118,6 +148,101 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar-thin {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: oklch(0.85 0.01 260) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
|
background-color: oklch(0.85 0.01 260);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: oklch(0.75 0.01 260);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth page transitions */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slideInRight 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message bubble animations */
|
||||||
|
@keyframes messageIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-message-in {
|
||||||
|
animation: messageIn 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle floating shadow for cards */
|
||||||
|
.shadow-soft {
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px oklch(0.23 0.02 260 / 0.04),
|
||||||
|
0 4px 12px oklch(0.23 0.02 260 / 0.06),
|
||||||
|
0 8px 24px oklch(0.23 0.02 260 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-soft-lg {
|
||||||
|
box-shadow:
|
||||||
|
0 2px 4px oklch(0.23 0.02 260 / 0.04),
|
||||||
|
0 8px 20px oklch(0.23 0.02 260 / 0.08),
|
||||||
|
0 16px 40px oklch(0.23 0.02 260 / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient text utility */
|
||||||
|
.text-gradient-primary {
|
||||||
|
background: linear-gradient(135deg, oklch(0.55 0.18 265), oklch(0.60 0.20 290));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
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 geistSans = Geist({
|
const plusJakarta = Plus_Jakarta_Sans({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "600", "700"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-mono",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -90,12 +93,15 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<DiagramProvider>{children}</DiagramProvider>
|
<DiagramProvider>{children}</DiagramProvider>
|
||||||
|
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
|
{process.env.NEXT_PUBLIC_GA_ID && (
|
||||||
|
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
||||||
|
)}
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
73
app/page.tsx
73
app/page.tsx
@@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { DrawIoEmbed } from "react-drawio";
|
import { DrawIoEmbed } from "react-drawio";
|
||||||
import ChatPanel from "@/components/chat-panel";
|
import ChatPanel from "@/components/chat-panel";
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { useDiagram } from "@/contexts/diagram-context";
|
||||||
|
import { Monitor } from "lucide-react";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport } = useDiagram();
|
const { drawioRef, handleDiagramExport } = useDiagram();
|
||||||
@@ -14,17 +15,11 @@ export default function Home() {
|
|||||||
setIsMobile(window.innerWidth < 768);
|
setIsMobile(window.innerWidth < 768);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check on mount
|
|
||||||
checkMobile();
|
checkMobile();
|
||||||
|
|
||||||
// Add event listener for resize
|
|
||||||
window.addEventListener("resize", checkMobile);
|
window.addEventListener("resize", checkMobile);
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => window.removeEventListener("resize", checkMobile);
|
return () => window.removeEventListener("resize", checkMobile);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Add keyboard shortcut for toggling chat panel (Ctrl+B)
|
|
||||||
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') {
|
||||||
@@ -34,42 +29,56 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-100 relative">
|
<div className="flex h-screen bg-background relative overflow-hidden">
|
||||||
{/* Mobile warning overlay - keeps components mounted */}
|
{/* Mobile warning overlay */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-gray-100">
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background">
|
||||||
<div className="text-center p-8">
|
<div className="text-center p-8 max-w-sm mx-auto animate-fade-in">
|
||||||
<h1 className="text-2xl font-semibold text-gray-800">
|
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-6">
|
||||||
Please open this application on a desktop or laptop
|
<Monitor className="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-semibold text-foreground mb-3">
|
||||||
|
Desktop Required
|
||||||
</h1>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`${isChatVisible ? 'w-2/3' : 'w-full'} p-1 h-full relative transition-all duration-300 ease-in-out`}>
|
{/* Draw.io Canvas */}
|
||||||
<DrawIoEmbed
|
<div
|
||||||
ref={drawioRef}
|
className={`${isChatVisible ? 'w-2/3' : 'w-full'} h-full relative transition-all duration-300 ease-out`}
|
||||||
onExport={handleDiagramExport}
|
>
|
||||||
urlParameters={{
|
<div className="absolute inset-2 rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
||||||
spin: true,
|
<DrawIoEmbed
|
||||||
libraries: false,
|
ref={drawioRef}
|
||||||
saveAndExit: false,
|
onExport={handleDiagramExport}
|
||||||
noExitBtn: true,
|
urlParameters={{
|
||||||
}}
|
spin: true,
|
||||||
/>
|
libraries: false,
|
||||||
|
saveAndExit: false,
|
||||||
|
noExitBtn: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${isChatVisible ? 'w-1/3' : 'w-12'} h-full p-1 transition-all duration-300 ease-in-out`}>
|
|
||||||
<ChatPanel
|
{/* Chat Panel */}
|
||||||
isVisible={isChatVisible}
|
<div
|
||||||
onToggleVisibility={() => setIsChatVisible(!isChatVisible)}
|
className={`${isChatVisible ? 'w-1/3' : 'w-12'} h-full transition-all duration-300 ease-out`}
|
||||||
/>
|
>
|
||||||
|
<div className="h-full py-2 pr-2">
|
||||||
|
<ChatPanel
|
||||||
|
isVisible={isChatVisible}
|
||||||
|
onToggleVisibility={() => setIsChatVisible(!isChatVisible)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Zap, Cloud, GitBranch, Palette } from "lucide-react";
|
||||||
|
|
||||||
|
interface ExampleCardProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<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">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ExamplePanel({
|
export default function ExamplePanel({
|
||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
@@ -5,80 +39,85 @@ export default function ExamplePanel({
|
|||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void;
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
}) {
|
}) {
|
||||||
// New handler for the "Replicate this flowchart" button
|
|
||||||
const handleReplicateFlowchart = async () => {
|
const handleReplicateFlowchart = async () => {
|
||||||
setInput("Replicate this flowchart.");
|
setInput("Replicate this flowchart.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the example image
|
|
||||||
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" });
|
||||||
|
|
||||||
// Set the file to the files state
|
|
||||||
setFiles([file]);
|
setFiles([file]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading example image:", error);
|
console.error("Error loading example image:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handler for the "Replicate this in aws style" button
|
|
||||||
const handleReplicateArchitecture = async () => {
|
const handleReplicateArchitecture = async () => {
|
||||||
setInput("Replicate this in aws style");
|
setInput("Replicate this in aws style");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the architecture image
|
|
||||||
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",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the file to the files state
|
|
||||||
setFiles([file]);
|
setFiles([file]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading architecture image:", error);
|
console.error("Error loading architecture image:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-2 border-t border-b border-gray-100">
|
<div className="py-6 px-2 animate-fade-in">
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
{/* Welcome section */}
|
||||||
{" "}
|
<div className="text-center mb-6">
|
||||||
Start a conversation to generate or modify diagrams.
|
<h2 className="text-lg font-semibold text-foreground mb-2">
|
||||||
</p>
|
Create diagrams with AI
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
</h2>
|
||||||
{" "}
|
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
||||||
You can also upload images to use as references.
|
Describe what you want to create or upload an image to replicate
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
</div>
|
||||||
Try these examples{" "}
|
|
||||||
<span className="text-xs text-gray-400">(cached for instant response)</span>:
|
{/* Examples grid */}
|
||||||
</p>
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap gap-5">
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1">
|
||||||
<button
|
Quick Examples
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
</p>
|
||||||
onClick={() => setInput("Give me a **animated connector** diagram of transformer's architecture")}
|
|
||||||
>
|
<div className="grid gap-2">
|
||||||
Draw diagram with Animated Connectors
|
<ExampleCard
|
||||||
</button>
|
icon={<Zap className="w-4 h-4 text-primary" />}
|
||||||
<button
|
title="Animated Diagram"
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
description="Draw a transformer architecture with animated connectors"
|
||||||
onClick={handleReplicateArchitecture}
|
onClick={() => setInput("Give me a **animated connector** diagram of transformer's architecture")}
|
||||||
>
|
/>
|
||||||
Create AWS architecture
|
|
||||||
</button>
|
<ExampleCard
|
||||||
<button
|
icon={<Cloud className="w-4 h-4 text-primary" />}
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
title="AWS Architecture"
|
||||||
onClick={handleReplicateFlowchart}
|
description="Create a cloud architecture diagram with AWS icons"
|
||||||
>
|
onClick={handleReplicateArchitecture}
|
||||||
Replicate flowchart
|
/>
|
||||||
</button>
|
|
||||||
<button
|
<ExampleCard
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
icon={<GitBranch className="w-4 h-4 text-primary" />}
|
||||||
onClick={() => setInput("Draw a cat for me")}
|
title="Replicate Flowchart"
|
||||||
>
|
description="Upload and replicate an existing flowchart"
|
||||||
Draw a cat
|
onClick={handleReplicateFlowchart}
|
||||||
</button>
|
/>
|
||||||
|
|
||||||
|
<ExampleCard
|
||||||
|
icon={<Palette className="w-4 h-4 text-primary" />}
|
||||||
|
title="Creative Drawing"
|
||||||
|
description="Draw something fun and creative"
|
||||||
|
onClick={() => setInput("Draw a cat for me")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-muted-foreground/60 text-center mt-4">
|
||||||
|
Examples are cached for instant response
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import React, { useCallback, useRef, useEffect, useState } from "react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal";
|
import { ResetWarningModal } from "@/components/reset-warning-modal";
|
||||||
|
import { SaveDialog } from "@/components/save-dialog";
|
||||||
import {
|
import {
|
||||||
Loader2,
|
Loader2,
|
||||||
Send,
|
Send,
|
||||||
RotateCcw,
|
Trash2,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
History,
|
History,
|
||||||
|
Download,
|
||||||
|
Paperclip,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
||||||
import { FilePreviewList } from "./file-preview-list";
|
import { FilePreviewList } from "./file-preview-list";
|
||||||
@@ -39,19 +42,19 @@ export function ChatInput({
|
|||||||
showHistory = false,
|
showHistory = false,
|
||||||
onToggleHistory = () => {},
|
onToggleHistory = () => {},
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory } = 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);
|
||||||
|
|
||||||
// Debug: Log status changes
|
|
||||||
const isDisabled = status === "streaming" || status === "submitted";
|
const isDisabled = status === "streaming" || status === "submitted";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled);
|
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled);
|
||||||
}, [status, isDisabled]);
|
}, [status, isDisabled]);
|
||||||
|
|
||||||
// Auto-resize textarea based on content
|
|
||||||
const adjustTextareaHeight = useCallback(() => {
|
const adjustTextareaHeight = useCallback(() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
@@ -64,7 +67,6 @@ export function ChatInput({
|
|||||||
adjustTextareaHeight();
|
adjustTextareaHeight();
|
||||||
}, [input, adjustTextareaHeight]);
|
}, [input, adjustTextareaHeight]);
|
||||||
|
|
||||||
// Handle keyboard shortcuts and paste events
|
|
||||||
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();
|
||||||
@@ -75,7 +77,6 @@ export function ChatInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle clipboard paste
|
|
||||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
if (isDisabled) return;
|
if (isDisabled) return;
|
||||||
|
|
||||||
@@ -89,7 +90,6 @@ export function ChatInput({
|
|||||||
imageItems.map(async (item) => {
|
imageItems.map(async (item) => {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
// Create a new file with a unique name
|
|
||||||
return new File(
|
return new File(
|
||||||
[file],
|
[file],
|
||||||
`pasted-image-${Date.now()}.${file.type.split("/")[1]}`,
|
`pasted-image-${Date.now()}.${file.type.split("/")[1]}`,
|
||||||
@@ -109,13 +109,11 @@ export function ChatInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle file changes
|
|
||||||
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 || []);
|
||||||
onFileChange([...files, ...newFiles]);
|
onFileChange([...files, ...newFiles]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove individual file
|
|
||||||
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) {
|
||||||
@@ -123,12 +121,10 @@ export function ChatInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trigger file input click
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle drag events
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -150,7 +146,6 @@ export function ChatInput({
|
|||||||
|
|
||||||
const droppedFiles = e.dataTransfer.files;
|
const droppedFiles = e.dataTransfer.files;
|
||||||
|
|
||||||
// Only process image files
|
|
||||||
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
||||||
file.type.startsWith("image/")
|
file.type.startsWith("image/")
|
||||||
);
|
);
|
||||||
@@ -160,7 +155,6 @@ export function ChatInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle clearing conversation and diagram
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onClearChat();
|
onClearChat();
|
||||||
setShowClearDialog(false);
|
setShowClearDialog(false);
|
||||||
@@ -169,112 +163,140 @@ export function ChatInput({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
className={`w-full space-y-2 ${
|
className={`w-full transition-all duration-200 ${
|
||||||
isDragging
|
isDragging
|
||||||
? "border-2 border-dashed border-primary p-4 rounded-lg bg-muted/20"
|
? "ring-2 ring-primary ring-offset-2 rounded-2xl"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} />
|
{/* File previews */}
|
||||||
|
{files.length > 0 && (
|
||||||
<Textarea
|
<div className="mb-3">
|
||||||
ref={textareaRef}
|
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} />
|
||||||
value={input}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
placeholder="Describe what changes you want to make to the diagram
|
|
||||||
or upload(paste) an image to replicate a diagram.
|
|
||||||
(Press Cmd/Ctrl + Enter to send)"
|
|
||||||
disabled={isDisabled}
|
|
||||||
aria-label="Chat input"
|
|
||||||
className="min-h-[80px] resize-none transition-all duration-200 px-1 py-0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="mr-auto">
|
|
||||||
<ButtonWithTooltip
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setShowClearDialog(true)}
|
|
||||||
tooltipContent="Clear current conversation and diagram"
|
|
||||||
>
|
|
||||||
<RotateCcw className="mr-2 h-4 w-4" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
|
||||||
{/* Warning Modal */}
|
|
||||||
<ResetWarningModal
|
|
||||||
open={showClearDialog}
|
|
||||||
onOpenChange={setShowClearDialog}
|
|
||||||
onClear={handleClear}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HistoryDialog
|
|
||||||
showHistory={showHistory}
|
|
||||||
onToggleHistory={onToggleHistory}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
)}
|
||||||
{/* History Button */}
|
|
||||||
<ButtonWithTooltip
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => onToggleHistory(true)}
|
|
||||||
disabled={
|
|
||||||
isDisabled ||
|
|
||||||
diagramHistory.length === 0
|
|
||||||
}
|
|
||||||
title="Diagram History"
|
|
||||||
tooltipContent="View diagram history"
|
|
||||||
>
|
|
||||||
<History className="h-4 w-4" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
|
|
||||||
<Button
|
{/* Input container */}
|
||||||
type="button"
|
<div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200">
|
||||||
variant="outline"
|
<Textarea
|
||||||
size="icon"
|
ref={textareaRef}
|
||||||
onClick={triggerFileInput}
|
value={input}
|
||||||
disabled={isDisabled}
|
onChange={onChange}
|
||||||
title="Upload image"
|
onKeyDown={handleKeyDown}
|
||||||
>
|
onPaste={handlePaste}
|
||||||
<ImageIcon className="h-4 w-4" />
|
placeholder="Describe your diagram or paste an image..."
|
||||||
</Button>
|
disabled={isDisabled}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
|
||||||
<input
|
{/* Action bar */}
|
||||||
type="file"
|
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
|
||||||
ref={fileInputRef}
|
{/* Left actions */}
|
||||||
className="hidden"
|
<div className="flex items-center gap-1">
|
||||||
onChange={handleFileChange}
|
<ButtonWithTooltip
|
||||||
accept="image/*"
|
type="button"
|
||||||
multiple
|
variant="ghost"
|
||||||
disabled={isDisabled}
|
size="sm"
|
||||||
/>
|
onClick={() => setShowClearDialog(true)}
|
||||||
|
tooltipContent="Clear conversation"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
|
<ResetWarningModal
|
||||||
|
open={showClearDialog}
|
||||||
|
onOpenChange={setShowClearDialog}
|
||||||
|
onClear={handleClear}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HistoryDialog
|
||||||
|
showHistory={showHistory}
|
||||||
|
onToggleHistory={onToggleHistory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right actions */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ButtonWithTooltip
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onToggleHistory(true)}
|
||||||
|
disabled={isDisabled || diagramHistory.length === 0}
|
||||||
|
tooltipContent="Diagram history"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<History className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
|
<ButtonWithTooltip
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSaveDialog(true)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
tooltipContent="Save diagram"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
|
<SaveDialog
|
||||||
|
open={showSaveDialog}
|
||||||
|
onOpenChange={setShowSaveDialog}
|
||||||
|
onSave={saveDiagramToFile}
|
||||||
|
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ButtonWithTooltip
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={triggerFileInput}
|
||||||
|
disabled={isDisabled}
|
||||||
|
tooltipContent="Upload image"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ImageIcon className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isDisabled || !input.trim()}
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||||
|
aria-label={isDisabled ? "Sending..." : "Send message"}
|
||||||
|
>
|
||||||
|
{isDisabled ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="h-4 w-4 mr-1.5" />
|
||||||
|
Send
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isDisabled || !input.trim()}
|
|
||||||
className="transition-opacity"
|
|
||||||
aria-label={
|
|
||||||
isDisabled
|
|
||||||
? "Sending message..."
|
|
||||||
: "Send message"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isDisabled ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Send className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,52 @@ import Image from "next/image";
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import ExamplePanel from "./chat-example-panel";
|
import ExamplePanel from "./chat-example-panel";
|
||||||
import { UIMessage } from "ai";
|
import { UIMessage } from "ai";
|
||||||
import { convertToLegalXml, replaceNodes } from "@/lib/utils";
|
import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils";
|
||||||
import { Copy, Check, X } from "lucide-react";
|
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus } from "lucide-react";
|
||||||
|
import { CodeBlock } from "./code-block";
|
||||||
|
|
||||||
|
interface EditPair {
|
||||||
|
search: string;
|
||||||
|
replace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{edits.map((edit, index) => (
|
||||||
|
<div key={index} className="rounded-lg border border-border/50 overflow-hidden bg-background/50">
|
||||||
|
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
|
Change {index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border/30">
|
||||||
|
{/* Search (old) */}
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
|
<Minus className="w-3 h-3 text-red-500" />
|
||||||
|
<span className="text-[10px] font-medium text-red-600 uppercase tracking-wide">Remove</span>
|
||||||
|
</div>
|
||||||
|
<pre className="text-[11px] font-mono text-red-700 bg-red-50 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
|
{edit.search}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
{/* Replace (new) */}
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
|
<Plus className="w-3 h-3 text-green-500" />
|
||||||
|
<span className="text-[10px] font-medium text-green-600 uppercase tracking-wide">Add</span>
|
||||||
|
</div>
|
||||||
|
<pre className="text-[11px] font-mono text-green-700 bg-green-50 rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
|
{edit.replace}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { useDiagram } from "@/contexts/diagram-context";
|
||||||
|
|
||||||
@@ -58,9 +102,15 @@ export function ChatMessageDisplay({
|
|||||||
const currentXml = xml || "";
|
const currentXml = xml || "";
|
||||||
const convertedXml = convertToLegalXml(currentXml);
|
const convertedXml = convertToLegalXml(currentXml);
|
||||||
if (convertedXml !== previousXML.current) {
|
if (convertedXml !== previousXML.current) {
|
||||||
previousXML.current = convertedXml;
|
|
||||||
const replacedXML = replaceNodes(chartXML, convertedXml);
|
const replacedXML = replaceNodes(chartXML, convertedXml);
|
||||||
onDisplayChart(replacedXML);
|
|
||||||
|
const validationError = validateMxCellStructure(replacedXML);
|
||||||
|
if (!validationError) {
|
||||||
|
previousXML.current = convertedXml;
|
||||||
|
onDisplayChart(replacedXML);
|
||||||
|
} else {
|
||||||
|
console.error("[ChatMessageDisplay] XML validation failed:", validationError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[chartXML, onDisplayChart]
|
[chartXML, onDisplayChart]
|
||||||
@@ -72,7 +122,6 @@ export function ChatMessageDisplay({
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Handle tool invocations and update diagram when needed
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
if (message.parts) {
|
if (message.parts) {
|
||||||
@@ -80,7 +129,6 @@ export function ChatMessageDisplay({
|
|||||||
if (part.type?.startsWith("tool-")) {
|
if (part.type?.startsWith("tool-")) {
|
||||||
const { toolCallId, state } = part;
|
const { toolCallId, state } = part;
|
||||||
|
|
||||||
// Auto-collapse args when diagrams are generated
|
|
||||||
if (state === "output-available") {
|
if (state === "output-available") {
|
||||||
setExpandedTools((prev) => ({
|
setExpandedTools((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -88,20 +136,16 @@ export function ChatMessageDisplay({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle diagram updates for display_diagram tool
|
|
||||||
if (
|
if (
|
||||||
part.type === "tool-display_diagram" &&
|
part.type === "tool-display_diagram" &&
|
||||||
part.input?.xml
|
part.input?.xml
|
||||||
) {
|
) {
|
||||||
// For streaming input, always update to show streaming
|
|
||||||
if (
|
if (
|
||||||
state === "input-streaming" ||
|
state === "input-streaming" ||
|
||||||
state === "input-available"
|
state === "input-available"
|
||||||
) {
|
) {
|
||||||
handleDisplayChart(part.input.xml);
|
handleDisplayChart(part.input.xml);
|
||||||
}
|
} else if (
|
||||||
// For completed calls, only update if not processed yet
|
|
||||||
else if (
|
|
||||||
state === "output-available" &&
|
state === "output-available" &&
|
||||||
!processedToolCalls.current.has(toolCallId)
|
!processedToolCalls.current.has(toolCallId)
|
||||||
) {
|
) {
|
||||||
@@ -128,125 +172,163 @@ export function ChatMessageDisplay({
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getToolDisplayName = (name: string) => {
|
||||||
|
switch (name) {
|
||||||
|
case "display_diagram":
|
||||||
|
return "Generate Diagram";
|
||||||
|
case "edit_diagram":
|
||||||
|
return "Edit Diagram";
|
||||||
|
default:
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={callId}
|
key={callId}
|
||||||
className="p-4 my-2 text-gray-500 border border-gray-300 rounded"
|
className="my-3 rounded-xl border border-border/60 bg-muted/30 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex items-center justify-between px-4 py-3 bg-muted/50">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-xs">Tool: {toolName}</div>
|
<div className="w-6 h-6 rounded-md bg-primary/10 flex items-center justify-center">
|
||||||
|
<Cpu className="w-3.5 h-3.5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-foreground/80">
|
||||||
|
{getToolDisplayName(toolName)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{state === "input-streaming" && (
|
||||||
|
<div className="h-4 w-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
)}
|
||||||
|
{state === "output-available" && (
|
||||||
|
<span className="text-xs font-medium text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
|
Complete
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{state === "output-error" && (
|
||||||
|
<span className="text-xs font-medium text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
|
||||||
|
Error
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{input && Object.keys(input).length > 0 && (
|
{input && Object.keys(input).length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={toggleExpanded}
|
onClick={toggleExpanded}
|
||||||
className="text-xs text-gray-500 hover:text-gray-700"
|
className="p-1 rounded hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
{isExpanded ? "Hide Args" : "Show Args"}
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{input && isExpanded && (
|
</div>
|
||||||
<div className="mt-1 font-mono text-xs overflow-hidden">
|
{input && isExpanded && (
|
||||||
{typeof input === "object" &&
|
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
||||||
Object.keys(input).length > 0 &&
|
{typeof input === "object" && input.xml ? (
|
||||||
`Input: ${JSON.stringify(input, null, 2)}`}
|
<CodeBlock code={input.xml} language="xml" />
|
||||||
</div>
|
) : typeof input === "object" && input.edits && Array.isArray(input.edits) ? (
|
||||||
)}
|
<EditDiffDisplay edits={input.edits} />
|
||||||
<div className="mt-2 text-sm">
|
) : typeof input === "object" && Object.keys(input).length > 0 ? (
|
||||||
{state === "input-streaming" ? (
|
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
||||||
<div className="h-4 w-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
|
||||||
) : state === "output-available" ? (
|
|
||||||
<div className="text-green-600">
|
|
||||||
{output || (toolName === "display_diagram"
|
|
||||||
? "Diagram generated"
|
|
||||||
: toolName === "edit_diagram"
|
|
||||||
? "Diagram edited"
|
|
||||||
: "Tool executed")}
|
|
||||||
</div>
|
|
||||||
) : state === "output-error" ? (
|
|
||||||
<div className="text-red-600">
|
|
||||||
{output || (toolName === "display_diagram"
|
|
||||||
? "Error generating diagram"
|
|
||||||
: toolName === "edit_diagram"
|
|
||||||
? "Error editing diagram"
|
|
||||||
: "Tool error")}
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{output && state === "output-error" && (
|
||||||
|
<div className="px-4 py-3 border-t border-border/40 text-sm text-red-600">
|
||||||
|
{output}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full pr-4">
|
<ScrollArea className="h-full px-4 scrollbar-thin">
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<ExamplePanel setInput={setInput} setFiles={setFiles} />
|
<ExamplePanel setInput={setInput} setFiles={setFiles} />
|
||||||
) : (
|
) : (
|
||||||
messages.map((message) => {
|
<div className="py-4 space-y-4">
|
||||||
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
|
{messages.map((message, messageIndex) => {
|
||||||
return (
|
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
|
||||||
<div
|
return (
|
||||||
key={message.id}
|
|
||||||
className={`mb-4 flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
|
|
||||||
>
|
|
||||||
{message.role === "user" && userMessageText && (
|
|
||||||
<button
|
|
||||||
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors self-center mr-1"
|
|
||||||
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
|
|
||||||
>
|
|
||||||
{copiedMessageId === message.id ? (
|
|
||||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
|
||||||
) : copyFailedMessageId === message.id ? (
|
|
||||||
<X className="h-3.5 w-3.5 text-red-500" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={`px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${message.role === "user"
|
key={message.id}
|
||||||
? "bg-primary text-primary-foreground"
|
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"} animate-message-in`}
|
||||||
: "bg-muted text-muted-foreground"
|
style={{ animationDelay: `${messageIndex * 50}ms` }}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{message.parts?.map((part: any, index: number) => {
|
{message.role === "user" && userMessageText && (
|
||||||
switch (part.type) {
|
<button
|
||||||
case "text":
|
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
||||||
return (
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors self-center mr-2"
|
||||||
<div key={index}>{part.text}</div>
|
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
|
||||||
);
|
>
|
||||||
case "file":
|
{copiedMessageId === message.id ? (
|
||||||
return (
|
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||||
<div key={index} className="mt-2">
|
) : copyFailedMessageId === message.id ? (
|
||||||
<Image
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
src={part.url}
|
) : (
|
||||||
width={200}
|
<Copy className="h-3.5 w-3.5" />
|
||||||
height={200}
|
)}
|
||||||
alt={`Uploaded diagram or image for AI analysis`}
|
</button>
|
||||||
className="rounded-md border"
|
)}
|
||||||
style={{
|
<div className="max-w-[85%]">
|
||||||
objectFit: "contain",
|
{/* Text content in bubble */}
|
||||||
}}
|
{message.parts?.some((part: any) => part.type === "text" || part.type === "file") && (
|
||||||
/>
|
<div
|
||||||
</div>
|
className={`px-4 py-3 text-sm leading-relaxed ${
|
||||||
);
|
message.role === "user"
|
||||||
default:
|
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
||||||
if (part.type?.startsWith("tool-")) {
|
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||||
return renderToolPart(part);
|
}`}
|
||||||
}
|
>
|
||||||
return null;
|
{message.parts?.map((part: any, index: number) => {
|
||||||
}
|
switch (part.type) {
|
||||||
})}
|
case "text":
|
||||||
|
return (
|
||||||
|
<div key={index} className="whitespace-pre-wrap break-words">
|
||||||
|
{part.text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "file":
|
||||||
|
return (
|
||||||
|
<div key={index} className="mt-2">
|
||||||
|
<Image
|
||||||
|
src={part.url}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
alt={`Uploaded diagram or image for AI analysis`}
|
||||||
|
className="rounded-lg border border-white/20"
|
||||||
|
style={{
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Tool calls outside bubble */}
|
||||||
|
{message.parts?.map((part: any) => {
|
||||||
|
if (part.type?.startsWith("tool-")) {
|
||||||
|
return renderToolPart(part);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-red-500 text-sm mt-2">
|
<div className="mx-4 mb-4 p-4 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm">
|
||||||
Error: {error.message}
|
<span className="font-medium">Error:</span> {error.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
|
|||||||
@@ -5,20 +5,14 @@ import { useRef, useEffect, useState } from "react";
|
|||||||
import { FaGithub } from "react-icons/fa";
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { PanelRightClose, PanelRightOpen } from "lucide-react";
|
import { PanelRightClose, PanelRightOpen } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
import { DefaultChatTransport } from "ai";
|
import { DefaultChatTransport } from "ai";
|
||||||
import { ChatInput } from "@/components/chat-input";
|
import { ChatInput } from "@/components/chat-input";
|
||||||
import { ChatMessageDisplay } from "./chat-message-display";
|
import { ChatMessageDisplay } from "./chat-message-display";
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { useDiagram } from "@/contexts/diagram-context";
|
||||||
import { replaceNodes, formatXML } from "@/lib/utils";
|
import { replaceNodes, formatXML, validateMxCellStructure } from "@/lib/utils";
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
@@ -26,46 +20,50 @@ interface ChatPanelProps {
|
|||||||
onToggleVisibility: () => void;
|
onToggleVisibility: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatPanel({ isVisible, onToggleVisibility }: ChatPanelProps) {
|
export default function ChatPanel({
|
||||||
|
isVisible,
|
||||||
|
onToggleVisibility,
|
||||||
|
}: ChatPanelProps) {
|
||||||
const {
|
const {
|
||||||
loadDiagram: onDisplayChart,
|
loadDiagram: onDisplayChart,
|
||||||
handleExport: onExport,
|
handleExport: onExport,
|
||||||
|
handleExportWithoutHistory,
|
||||||
resolverRef,
|
resolverRef,
|
||||||
chartXML,
|
chartXML,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
} = useDiagram();
|
} = useDiagram();
|
||||||
|
|
||||||
const onFetchChart = () => {
|
const onFetchChart = (saveToHistory = true) => {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
new Promise<string>((resolve) => {
|
new Promise<string>((resolve) => {
|
||||||
if (resolverRef && "current" in resolverRef) {
|
if (resolverRef && "current" in resolverRef) {
|
||||||
resolverRef.current = resolve;
|
resolverRef.current = resolve;
|
||||||
}
|
}
|
||||||
onExport();
|
if (saveToHistory) {
|
||||||
|
onExport();
|
||||||
|
} else {
|
||||||
|
handleExportWithoutHistory();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
new Promise<string>((_, reject) =>
|
new Promise<string>((_, reject) =>
|
||||||
setTimeout(() => reject(new Error("Chart export timed out after 10 seconds")), 10000)
|
setTimeout(
|
||||||
)
|
() =>
|
||||||
|
reject(
|
||||||
|
new Error("Chart export timed out after 10 seconds")
|
||||||
|
),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
// Add a step counter to track updates
|
|
||||||
|
|
||||||
// Add state for file attachments
|
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
// Add state for showing the history dialog
|
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
|
|
||||||
// Convert File[] to FileList for experimental_attachments
|
|
||||||
const createFileList = (files: File[]): FileList => {
|
|
||||||
const dt = new DataTransfer();
|
|
||||||
files.forEach((file) => dt.items.add(file));
|
|
||||||
return dt.files;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add state for input management
|
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
|
|
||||||
// Remove the currentXmlRef and related useEffect
|
// Generate a unique session ID for Langfuse tracing
|
||||||
|
const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
||||||
|
|
||||||
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
||||||
useChat({
|
useChat({
|
||||||
transport: new DefaultChatTransport({
|
transport: new DefaultChatTransport({
|
||||||
@@ -73,27 +71,36 @@ export default function ChatPanel({ isVisible, onToggleVisibility }: ChatPanelPr
|
|||||||
}),
|
}),
|
||||||
async onToolCall({ toolCall }) {
|
async onToolCall({ toolCall }) {
|
||||||
if (toolCall.toolName === "display_diagram") {
|
if (toolCall.toolName === "display_diagram") {
|
||||||
// Diagram is handled streamingly in the ChatMessageDisplay component
|
const { xml } = toolCall.input as { xml: string };
|
||||||
addToolResult({
|
|
||||||
tool: "display_diagram",
|
const validationError = validateMxCellStructure(xml);
|
||||||
toolCallId: toolCall.toolCallId,
|
|
||||||
output: "Successfully displayed the diagram.",
|
if (validationError) {
|
||||||
});
|
addToolResult({
|
||||||
|
tool: "display_diagram",
|
||||||
|
toolCallId: toolCall.toolCallId,
|
||||||
|
output: validationError,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addToolResult({
|
||||||
|
tool: "display_diagram",
|
||||||
|
toolCallId: toolCall.toolCallId,
|
||||||
|
output: "Successfully displayed the diagram.",
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (toolCall.toolName === "edit_diagram") {
|
} else if (toolCall.toolName === "edit_diagram") {
|
||||||
const { edits } = toolCall.input as {
|
const { edits } = toolCall.input as {
|
||||||
edits: Array<{ search: string; replace: string }>;
|
edits: Array<{ search: string; replace: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentXml = '';
|
let currentXml = "";
|
||||||
try {
|
try {
|
||||||
// Fetch current chart XML
|
// Fetch without saving to history - edit_diagram shouldn't create history entry
|
||||||
currentXml = await onFetchChart();
|
currentXml = await onFetchChart(false);
|
||||||
|
|
||||||
// Apply edits using the utility function
|
|
||||||
const { replaceXMLParts } = await import("@/lib/utils");
|
const { replaceXMLParts } = await import("@/lib/utils");
|
||||||
const editedXml = replaceXMLParts(currentXml, edits);
|
const editedXml = replaceXMLParts(currentXml, edits);
|
||||||
|
|
||||||
// Load the edited diagram
|
|
||||||
onDisplayChart(editedXml);
|
onDisplayChart(editedXml);
|
||||||
|
|
||||||
addToolResult({
|
addToolResult({
|
||||||
@@ -104,9 +111,11 @@ export default function ChatPanel({ isVisible, onToggleVisibility }: ChatPanelPr
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Edit diagram failed:", error);
|
console.error("Edit diagram failed:", error);
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: String(error);
|
||||||
|
|
||||||
// Provide detailed error with current diagram XML
|
|
||||||
addToolResult({
|
addToolResult({
|
||||||
tool: "edit_diagram",
|
tool: "edit_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
@@ -126,17 +135,17 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
console.error("Chat error:", error);
|
console.error("Chat error:", error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
// Scroll to bottom when messages change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Debug: Log status changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[ChatPanel] Status changed to:', status);
|
console.log("[ChatPanel] Status changed to:", status);
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -144,16 +153,11 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
const isProcessing = status === "streaming" || status === "submitted";
|
const isProcessing = status === "streaming" || status === "submitted";
|
||||||
if (input.trim() && !isProcessing) {
|
if (input.trim() && !isProcessing) {
|
||||||
try {
|
try {
|
||||||
// Fetch chart data before sending message
|
|
||||||
let chartXml = await onFetchChart();
|
let chartXml = await onFetchChart();
|
||||||
|
|
||||||
// Format the XML to ensure consistency
|
|
||||||
chartXml = formatXML(chartXml);
|
chartXml = formatXML(chartXml);
|
||||||
|
|
||||||
// Create message parts
|
|
||||||
const parts: any[] = [{ type: "text", text: input }];
|
const parts: any[] = [{ type: "text", text: input }];
|
||||||
|
|
||||||
// Add file parts if files exist
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -176,11 +180,11 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
{
|
{
|
||||||
body: {
|
body: {
|
||||||
xml: chartXml,
|
xml: chartXml,
|
||||||
|
sessionId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear input and files after submission
|
|
||||||
setInput("");
|
setInput("");
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -189,79 +193,102 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle input change
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
) => {
|
) => {
|
||||||
setInput(e.target.value);
|
setInput(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to handle file changes
|
|
||||||
const handleFileChange = (newFiles: File[]) => {
|
const handleFileChange = (newFiles: File[]) => {
|
||||||
setFiles(newFiles);
|
setFiles(newFiles);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collapsed view when chat is hidden
|
// Collapsed view
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return (
|
return (
|
||||||
<Card className="h-full flex flex-col rounded-none py-0 gap-0 items-center justify-start pt-4">
|
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent="Show chat panel (Ctrl+B)"
|
tooltipContent="Show chat panel (Ctrl+B)"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onToggleVisibility}
|
onClick={onToggleVisibility}
|
||||||
|
className="hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
<PanelRightOpen className="h-5 w-5" />
|
<PanelRightOpen className="h-5 w-5 text-muted-foreground" />
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<div
|
<div
|
||||||
className="text-sm text-gray-500 mt-8"
|
className="text-sm font-medium text-muted-foreground mt-8 tracking-wide"
|
||||||
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
|
style={{
|
||||||
|
writingMode: "vertical-rl",
|
||||||
|
transform: "rotate(180deg)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Chat
|
AI Chat
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full view when chat is visible
|
// Full view
|
||||||
return (
|
return (
|
||||||
<Card className="h-full flex flex-col rounded-none py-0 gap-0">
|
<div className="h-full flex flex-col bg-card shadow-soft animate-slide-in-right rounded-xl border border-border/30">
|
||||||
<CardHeader className="p-4 flex flex-row justify-between items-center">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3">
|
<header className="px-5 py-4 border-b border-border/50">
|
||||||
<CardTitle>Next-AI-Drawio</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<Link href="/about" className="text-sm text-gray-600 hover:text-gray-900 transition-colors">
|
<div className="flex items-center gap-3">
|
||||||
About
|
<div className="flex items-center gap-2">
|
||||||
</Link>
|
<Image
|
||||||
|
src="/favicon.ico"
|
||||||
|
alt="Next AI Drawio"
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<h1 className="text-base font-semibold tracking-tight whitespace-nowrap">
|
||||||
|
Next AI Drawio
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
<ButtonWithTooltip
|
||||||
|
tooltipContent="Hide chat panel (Ctrl+B)"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onToggleVisibility}
|
||||||
|
className="hover:bg-accent"
|
||||||
|
>
|
||||||
|
<PanelRightClose className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
</header>
|
||||||
<ButtonWithTooltip
|
|
||||||
tooltipContent="Hide chat panel (Ctrl+B)"
|
{/* Messages */}
|
||||||
variant="ghost"
|
<main className="flex-1 overflow-hidden">
|
||||||
size="icon"
|
|
||||||
onClick={onToggleVisibility}
|
|
||||||
>
|
|
||||||
<PanelRightClose className="h-5 w-5" />
|
|
||||||
</ButtonWithTooltip>
|
|
||||||
<a
|
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
|
||||||
>
|
|
||||||
<FaGithub className="w-6 h-6" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-grow overflow-hidden px-2">
|
|
||||||
<ChatMessageDisplay
|
<ChatMessageDisplay
|
||||||
messages={messages}
|
messages={messages}
|
||||||
error={error}
|
error={error}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
setFiles={handleFileChange}
|
setFiles={handleFileChange}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</main>
|
||||||
|
|
||||||
<CardFooter className="p-2">
|
{/* Input */}
|
||||||
|
<footer className="p-4 border-t border-border/50 bg-card/50">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
input={input}
|
input={input}
|
||||||
status={status}
|
status={status}
|
||||||
@@ -270,13 +297,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
onClearChat={() => {
|
onClearChat={() => {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
clearDiagram();
|
clearDiagram();
|
||||||
|
setSessionId(`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
||||||
}}
|
}}
|
||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
showHistory={showHistory}
|
showHistory={showHistory}
|
||||||
onToggleHistory={setShowHistory}
|
onToggleHistory={setShowHistory}
|
||||||
/>
|
/>
|
||||||
</CardFooter>
|
</footer>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
39
components/code-block.tsx
Normal file
39
components/code-block.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Highlight, themes } from "prism-react-renderer";
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
code: string;
|
||||||
|
language?: "xml" | "json";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden w-full">
|
||||||
|
<Highlight theme={themes.github} code={code} language={language}>
|
||||||
|
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
||||||
|
<pre
|
||||||
|
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
fontFamily: "var(--font-mono), ui-monospace, monospace",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
wordBreak: "break-all",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tokens.map((line, i) => (
|
||||||
|
<div key={i} {...getLineProps({ line })} style={{ wordBreak: "break-all" }}>
|
||||||
|
{line.map((token, key) => (
|
||||||
|
<span key={key} {...getTokenProps({ token })} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,6 +23,19 @@ export function HistoryDialog({
|
|||||||
onToggleHistory,
|
onToggleHistory,
|
||||||
}: HistoryDialogProps) {
|
}: HistoryDialogProps) {
|
||||||
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram();
|
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSelectedIndex(null);
|
||||||
|
onToggleHistory(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmRestore = () => {
|
||||||
|
if (selectedIndex !== null) {
|
||||||
|
onDisplayChart(diagramHistory[selectedIndex].xml);
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
||||||
@@ -45,11 +59,12 @@ export function HistoryDialog({
|
|||||||
{diagramHistory.map((item, index) => (
|
{diagramHistory.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border rounded-md p-2 cursor-pointer hover:border-primary transition-colors"
|
className={`border rounded-md p-2 cursor-pointer hover:border-primary transition-colors ${
|
||||||
onClick={() => {
|
selectedIndex === index
|
||||||
onDisplayChart(item.xml);
|
? "border-primary ring-2 ring-primary"
|
||||||
onToggleHistory(false);
|
: ""
|
||||||
}}
|
}`}
|
||||||
|
onClick={() => setSelectedIndex(index)}
|
||||||
>
|
>
|
||||||
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
|
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
@@ -69,12 +84,29 @@ export function HistoryDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
{selectedIndex !== null ? (
|
||||||
variant="outline"
|
<>
|
||||||
onClick={() => onToggleHistory(false)}
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
>
|
Restore to Version {selectedIndex + 1}?
|
||||||
Close
|
</div>
|
||||||
</Button>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setSelectedIndex(null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmRestore}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
80
components/save-dialog.tsx
Normal file
80
components/save-dialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface SaveDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (filename: string) => void;
|
||||||
|
defaultFilename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
defaultFilename,
|
||||||
|
}: SaveDialogProps) {
|
||||||
|
const [filename, setFilename] = useState(defaultFilename);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFilename(defaultFilename);
|
||||||
|
}
|
||||||
|
}, [open, defaultFilename]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const finalFilename = filename.trim() || defaultFilename;
|
||||||
|
onSave(finalFilename);
|
||||||
|
onOpenChange(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>Save Diagram</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Filename</label>
|
||||||
|
<div className="flex items-stretch">
|
||||||
|
<Input
|
||||||
|
value={filename}
|
||||||
|
onChange={(e) => setFilename(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter filename"
|
||||||
|
autoFocus
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
className="rounded-r-none border-r-0 focus-visible:z-10"
|
||||||
|
/>
|
||||||
|
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
|
||||||
|
.drawio
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>Save</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,10 +10,12 @@ interface DiagramContextType {
|
|||||||
diagramHistory: { svg: string; xml: string }[];
|
diagramHistory: { svg: string; xml: string }[];
|
||||||
loadDiagram: (chart: string) => void;
|
loadDiagram: (chart: string) => void;
|
||||||
handleExport: () => void;
|
handleExport: () => 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: (filename: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
||||||
@@ -26,9 +28,24 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
>([]);
|
>([]);
|
||||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null);
|
const drawioRef = useRef<DrawIoEmbedRef | null>(null);
|
||||||
const resolverRef = useRef<((value: string) => void) | null>(null);
|
const resolverRef = useRef<((value: string) => void) | null>(null);
|
||||||
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
|
const expectHistoryExportRef = useRef<boolean>(false);
|
||||||
|
// Track if we're expecting an export for file save
|
||||||
|
const saveResolverRef = useRef<((xml: string) => void) | null>(null);
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
|
// Mark that this export should be saved to history
|
||||||
|
expectHistoryExportRef.current = true;
|
||||||
|
drawioRef.current.exportDiagram({
|
||||||
|
format: "xmlsvg",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExportWithoutHistory = () => {
|
||||||
|
if (drawioRef.current) {
|
||||||
|
// Export without saving to history (for edit_diagram fetching current state)
|
||||||
drawioRef.current.exportDiagram({
|
drawioRef.current.exportDiagram({
|
||||||
format: "xmlsvg",
|
format: "xmlsvg",
|
||||||
});
|
});
|
||||||
@@ -47,17 +64,29 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const extractedXML = extractDiagramXML(data.data);
|
const extractedXML = extractDiagramXML(data.data);
|
||||||
setChartXML(extractedXML);
|
setChartXML(extractedXML);
|
||||||
setLatestSvg(data.data);
|
setLatestSvg(data.data);
|
||||||
setDiagramHistory((prev) => [
|
|
||||||
...prev,
|
// Only add to history if this was a user-initiated export
|
||||||
{
|
if (expectHistoryExportRef.current) {
|
||||||
svg: data.data,
|
setDiagramHistory((prev) => [
|
||||||
xml: extractedXML,
|
...prev,
|
||||||
},
|
{
|
||||||
]);
|
svg: data.data,
|
||||||
|
xml: extractedXML,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expectHistoryExportRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (resolverRef.current) {
|
if (resolverRef.current) {
|
||||||
resolverRef.current(extractedXML);
|
resolverRef.current(extractedXML);
|
||||||
resolverRef.current = null;
|
resolverRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle save to file if requested
|
||||||
|
if (saveResolverRef.current) {
|
||||||
|
saveResolverRef.current(extractedXML);
|
||||||
|
saveResolverRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearDiagram = () => {
|
const clearDiagram = () => {
|
||||||
@@ -68,6 +97,35 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setDiagramHistory([]);
|
setDiagramHistory([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveDiagramToFile = (filename: string) => {
|
||||||
|
if (!drawioRef.current) {
|
||||||
|
console.warn("Draw.io editor not ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export diagram and save when export completes
|
||||||
|
drawioRef.current.exportDiagram({ format: "xmlsvg" });
|
||||||
|
saveResolverRef.current = (xml: string) => {
|
||||||
|
// Wrap in proper .drawio format
|
||||||
|
let fileContent = xml;
|
||||||
|
if (!xml.includes("<mxfile")) {
|
||||||
|
fileContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([fileContent], { type: "application/xml" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
// Add .drawio extension if not present
|
||||||
|
a.download = filename.endsWith(".drawio") ? filename : `${filename}.drawio`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
// Delay URL revocation to ensure download completes
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiagramContext.Provider
|
<DiagramContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -76,10 +134,12 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
diagramHistory,
|
diagramHistory,
|
||||||
loadDiagram,
|
loadDiagram,
|
||||||
handleExport,
|
handleExport,
|
||||||
|
handleExportWithoutHistory,
|
||||||
resolverRef,
|
resolverRef,
|
||||||
drawioRef,
|
drawioRef,
|
||||||
handleDiagramExport,
|
handleDiagramExport,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
|
saveDiagramToFile,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -41,3 +41,9 @@ 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
22
instrumentation.ts
Normal file
22
instrumentation.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tracerProvider = new NodeTracerProvider({
|
||||||
|
spanProcessors: [langfuseSpanProcessor],
|
||||||
|
});
|
||||||
|
|
||||||
|
tracerProvider.register();
|
||||||
|
}
|
||||||
103
lib/utils.ts
103
lib/utils.ts
@@ -306,6 +306,109 @@ export function replaceXMLParts(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates draw.io XML structure for common issues
|
||||||
|
* @param xml - The XML string to validate
|
||||||
|
* @returns null if valid, error message string if invalid
|
||||||
|
*/
|
||||||
|
export function validateMxCellStructure(xml: string): string | null {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
|
|
||||||
|
// Check for XML parsing errors (includes unescaped special characters)
|
||||||
|
const parseError = doc.querySelector('parsererror');
|
||||||
|
if (parseError) {
|
||||||
|
return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all mxCell elements once for all validations
|
||||||
|
const allCells = doc.querySelectorAll('mxCell');
|
||||||
|
|
||||||
|
// Single pass: collect IDs, check for duplicates, nesting, orphans, and invalid parents
|
||||||
|
const cellIds = new Set<string>();
|
||||||
|
const duplicateIds: string[] = [];
|
||||||
|
const nestedCells: string[] = [];
|
||||||
|
const orphanCells: string[] = [];
|
||||||
|
const invalidParents: { id: string; parent: string }[] = [];
|
||||||
|
const edgesToValidate: { id: string; source: string | null; target: string | null }[] = [];
|
||||||
|
|
||||||
|
allCells.forEach(cell => {
|
||||||
|
const id = cell.getAttribute('id');
|
||||||
|
const parent = cell.getAttribute('parent');
|
||||||
|
const isEdge = cell.getAttribute('edge') === '1';
|
||||||
|
|
||||||
|
// Check for duplicate IDs
|
||||||
|
if (id) {
|
||||||
|
if (cellIds.has(id)) {
|
||||||
|
duplicateIds.push(id);
|
||||||
|
} else {
|
||||||
|
cellIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nested mxCell (parent element is also mxCell)
|
||||||
|
if (cell.parentElement?.tagName === 'mxCell') {
|
||||||
|
nestedCells.push(id || 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check parent attribute (skip root cell id="0")
|
||||||
|
if (id !== '0') {
|
||||||
|
if (!parent) {
|
||||||
|
if (id) orphanCells.push(id);
|
||||||
|
} else {
|
||||||
|
// Store for later validation (after all IDs collected)
|
||||||
|
invalidParents.push({ id: id || 'unknown', parent });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect edges for connection validation
|
||||||
|
if (isEdge) {
|
||||||
|
edgesToValidate.push({
|
||||||
|
id: id || 'unknown',
|
||||||
|
source: cell.getAttribute('source'),
|
||||||
|
target: cell.getAttribute('target')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return errors in priority order
|
||||||
|
if (nestedCells.length > 0) {
|
||||||
|
return `Invalid XML: Found nested mxCell elements (IDs: ${nestedCells.slice(0, 3).join(', ')}). All mxCell elements must be direct children of <root>, never nested inside other mxCell elements. Please regenerate the diagram with correct structure.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateIds.length > 0) {
|
||||||
|
return `Invalid XML: Found duplicate cell IDs (${duplicateIds.slice(0, 3).join(', ')}). Each mxCell must have a unique ID. Please regenerate the diagram with unique IDs for all elements.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orphanCells.length > 0) {
|
||||||
|
return `Invalid XML: Found cells without parent attribute (IDs: ${orphanCells.slice(0, 3).join(', ')}). All mxCell elements (except id="0") must have a parent attribute. Please regenerate the diagram with proper parent references.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate parent references (now that all IDs are collected)
|
||||||
|
const badParents = invalidParents.filter(p => !cellIds.has(p.parent));
|
||||||
|
if (badParents.length > 0) {
|
||||||
|
const details = badParents.slice(0, 3).map(p => `${p.id} (parent: ${p.parent})`).join(', ');
|
||||||
|
return `Invalid XML: Found cells with invalid parent references (${details}). Parent IDs must reference existing cells. Please regenerate the diagram with valid parent references.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate edge connections
|
||||||
|
const invalidConnections: string[] = [];
|
||||||
|
edgesToValidate.forEach(edge => {
|
||||||
|
if (edge.source && !cellIds.has(edge.source)) {
|
||||||
|
invalidConnections.push(`${edge.id} (source: ${edge.source})`);
|
||||||
|
}
|
||||||
|
if (edge.target && !cellIds.has(edge.target)) {
|
||||||
|
invalidConnections.push(`${edge.id} (target: ${edge.target})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invalidConnections.length > 0) {
|
||||||
|
return `Invalid XML: Found edges with invalid source/target references (${invalidConnections.slice(0, 3).join(', ')}). Edge source and target must reference existing cell IDs. Please regenerate the diagram with valid edge connections.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function extractDiagramXML(xml_svg_string: string): string {
|
export function extractDiagramXML(xml_svg_string: string): string {
|
||||||
try {
|
try {
|
||||||
// 1. Parse the SVG string (using built-in DOMParser in a browser-like environment)
|
// 1. Parse the SVG string (using built-in DOMParser in a browser-like environment)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
output: 'standalone',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
370
package-lock.json
generated
370
package-lock.json
generated
@@ -15,7 +15,10 @@
|
|||||||
"@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.22",
|
"@ai-sdk/react": "^2.0.22",
|
||||||
|
"@langfuse/otel": "^4.4.4",
|
||||||
|
"@next/third-parties": "^16.0.6",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
"next": "15.2.3",
|
"next": "15.2.3",
|
||||||
"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",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-drawio": "^1.0.3",
|
"react-drawio": "^1.0.3",
|
||||||
@@ -1590,6 +1594,33 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@langfuse/core": {
|
||||||
|
"version": "4.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@langfuse/core/-/core-4.4.4.tgz",
|
||||||
|
"integrity": "sha512-hmtMNAOIsvDwT/xld0CJPXrIsakETbelSmAOGEY07faKtKdJy/BGjxexBbfAWLPgAC6wqC2fK2ByaYCGgC7MBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@langfuse/otel": {
|
||||||
|
"version": "4.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@langfuse/otel/-/otel-4.4.4.tgz",
|
||||||
|
"integrity": "sha512-b6VfVZUf5U1HrbPicUNZ0jYuR2QmnGhJpOV85Kbonzkslu60XggzjKi1UzTAVCA0sVzABK7WvqWvojpk02bfAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@langfuse/core": "^4.4.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/core": "^2.0.1",
|
||||||
|
"@opentelemetry/exporter-trace-otlp-http": ">=0.202.0 <1.0.0",
|
||||||
|
"@opentelemetry/sdk-trace-base": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@@ -1747,6 +1778,19 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@next/third-parties": {
|
||||||
|
"version": "16.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-16.0.6.tgz",
|
||||||
|
"integrity": "sha512-yYZkmgc3YuMsvouklk3R3oDEmzq1rEiEm/5wGHjTfyTCsRrrD3jBX84xrMtEN7vVWbWXXWbV0SZ5TfkgeMLGWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"third-party-capital": "1.0.20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0",
|
||||||
|
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -1845,6 +1889,273 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@opentelemetry/api-logs": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/context-async-hooks": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/core": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/exporter-trace-otlp-http": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/otlp-exporter-base": "0.208.0",
|
||||||
|
"@opentelemetry/otlp-transformer": "0.208.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0",
|
||||||
|
"@opentelemetry/sdk-trace-base": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/otlp-exporter-base": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/otlp-transformer": "0.208.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/otlp-transformer": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api-logs": "0.208.0",
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0",
|
||||||
|
"@opentelemetry/sdk-logs": "0.208.0",
|
||||||
|
"@opentelemetry/sdk-metrics": "2.2.0",
|
||||||
|
"@opentelemetry/sdk-trace-base": "2.2.0",
|
||||||
|
"protobufjs": "^7.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/resources": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-logs": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api-logs": "0.208.0",
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-metrics": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-trace-node": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/context-async-hooks": "2.2.0",
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/sdk-trace-base": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/semantic-conventions": {
|
||||||
|
"version": "1.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
|
||||||
|
"integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/codegen": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/fetch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.1",
|
||||||
|
"@protobufjs/inquire": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/float": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/inquire": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/path": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/pool": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/utf8": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||||
@@ -2765,7 +3076,6 @@
|
|||||||
"version": "20.17.30",
|
"version": "20.17.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@@ -2778,6 +3088,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prismjs": {
|
||||||
|
"version": "1.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
|
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
|
||||||
@@ -6484,6 +6800,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/longest-streak": {
|
"node_modules/longest-streak": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||||
@@ -7853,6 +8176,19 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prism-react-renderer": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prismjs": "^1.26.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -7865,6 +8201,31 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "7.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||||
|
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
"@protobufjs/base64": "^1.1.2",
|
||||||
|
"@protobufjs/codegen": "^2.0.4",
|
||||||
|
"@protobufjs/eventemitter": "^1.1.0",
|
||||||
|
"@protobufjs/fetch": "^1.1.0",
|
||||||
|
"@protobufjs/float": "^1.0.2",
|
||||||
|
"@protobufjs/inquire": "^1.1.0",
|
||||||
|
"@protobufjs/path": "^1.1.2",
|
||||||
|
"@protobufjs/pool": "^1.1.0",
|
||||||
|
"@protobufjs/utf8": "^1.1.0",
|
||||||
|
"@types/node": ">=13.7.0",
|
||||||
|
"long": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -8752,6 +9113,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/third-party-capital": {
|
||||||
|
"version": "1.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz",
|
||||||
|
"integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/throttleit": {
|
"node_modules/throttleit": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||||
@@ -9074,7 +9441,6 @@
|
|||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unified": {
|
"node_modules/unified": {
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
"@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.22",
|
"@ai-sdk/react": "^2.0.22",
|
||||||
|
"@langfuse/otel": "^4.4.4",
|
||||||
|
"@next/third-parties": "^16.0.6",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
@@ -32,6 +35,7 @@
|
|||||||
"next": "15.2.3",
|
"next": "15.2.3",
|
||||||
"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",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-drawio": "^1.0.3",
|
"react-drawio": "^1.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user