mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
Compare commits
10 Commits
feat/enhan
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f2569a3b | ||
|
|
cbb92bd636 | ||
|
|
8d898d8adc | ||
|
|
1e0b1ed970 | ||
|
|
1d03d10ba8 | ||
|
|
e893bd60f9 | ||
|
|
9aaf9bf31f | ||
|
|
150eb1ff63 | ||
|
|
215a101f54 | ||
|
|
e00938d9d3 |
@@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
"next/core-web-vitals",
|
|
||||||
"next/typescript"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx lint-staged
|
||||||
23
.vscode/settings.json
vendored
Normal file
23
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit",
|
||||||
|
"source.organizeImports.biome": "explicit"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
CONTRIBUTING.md
Normal file
35
CONTRIBUTING.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/next-ai-draw-io.git
|
||||||
|
cd next-ai-draw-io
|
||||||
|
npm install
|
||||||
|
cp env.example .env.local
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
We use [Biome](https://biomejs.dev/) for linting and formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run format # Format code
|
||||||
|
npm run lint # Check lint errors
|
||||||
|
npm run check # Run all checks (CI)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-commit hooks via Husky will run Biome automatically on staged files.
|
||||||
|
|
||||||
|
For a better experience, install the [Biome VS Code extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) for real-time linting and format-on-save.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
1. Create a feature branch
|
||||||
|
2. Make changes and ensure `npm run check` passes
|
||||||
|
3. Submit PR against `main` with a clear description
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
Include steps to reproduce, expected vs actual behavior, and AI provider used.
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next"
|
||||||
import Link from "next/link";
|
import Image from "next/image"
|
||||||
import { FaGithub } from "react-icons/fa";
|
import Link from "next/link"
|
||||||
import Image from "next/image";
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "关于 - Next AI Draw.io",
|
title: "关于 - Next AI Draw.io",
|
||||||
description: "AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
|
description:
|
||||||
|
"AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
|
||||||
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
|
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function AboutCN() {
|
export default function AboutCN() {
|
||||||
return (
|
return (
|
||||||
@@ -16,14 +17,23 @@ export default function AboutCN() {
|
|||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link 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
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
编辑器
|
编辑器
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/about/cn" className="text-blue-600 font-semibold">
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
关于
|
关于
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
@@ -45,22 +55,41 @@ export default function AboutCN() {
|
|||||||
<article className="prose prose-lg max-w-none">
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
<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">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI驱动的图表创建工具 - 对话、绘制、可视化
|
AI驱动的图表创建工具 - 对话、绘制、可视化
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4 mt-4 text-sm">
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Link>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link href="/about/cn" className="text-blue-600 font-semibold">中文</Link>
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Link>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600">日本語</Link>
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<p className="text-amber-800">
|
<p className="text-amber-800">
|
||||||
本应用设计运行于 Claude Opus 4.5 以获得最佳性能。但由于流量超出预期,运行顶级模型的成本变得难以承受。为避免服务中断并控制成本,我已将后端切换至 Claude Haiku 4.5。
|
本应用设计运行于 Claude Opus 4.5
|
||||||
|
以获得最佳性能。但由于流量超出预期,运行顶级模型的成本变得难以承受。为避免服务中断并控制成本,我已将后端切换至
|
||||||
|
Claude Haiku 4.5。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,80 +98,167 @@ export default function AboutCN() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">功能特性</h2>
|
<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">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li><strong>LLM驱动的图表创建</strong>:利用大语言模型通过自然语言命令直接创建和操作draw.io图表</li>
|
<li>
|
||||||
<li><strong>基于图像的图表复制</strong>:上传现有图表或图像,让AI自动复制和增强</li>
|
<strong>LLM驱动的图表创建</strong>
|
||||||
<li><strong>图表历史记录</strong>:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本</li>
|
:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
||||||
<li><strong>交互式聊天界面</strong>:与AI实时对话来完善您的图表</li>
|
</li>
|
||||||
<li><strong>AWS架构图支持</strong>:专门支持生成AWS架构图</li>
|
<li>
|
||||||
<li><strong>动画连接器</strong>:在图表元素之间创建动态动画连接器,实现更好的可视化效果</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>
|
</ul>
|
||||||
|
|
||||||
{/* Examples */}
|
{/* Examples */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">示例</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
<p className="text-gray-700 mb-6">以下是一些示例提示词及其生成的图表:</p>
|
示例
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
以下是一些示例提示词及其生成的图表:
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* Animated Transformer */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">动画Transformer连接器</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
动画Transformer连接器
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>提示词:</strong> 给我一个带有<strong>动画连接器</strong>的Transformer架构图。
|
<strong>提示词:</strong> 给我一个带有
|
||||||
|
<strong>动画连接器</strong>的Transformer架构图。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/animated_connectors.svg" alt="带动画连接器的Transformer架构" width={480} height={360} className="mx-auto" />
|
<Image
|
||||||
|
src="/animated_connectors.svg"
|
||||||
|
alt="带动画连接器的Transformer架构"
|
||||||
|
width={480}
|
||||||
|
height={360}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP架构图</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
GCP架构图
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用<strong>GCP图标</strong>生成一个GCP架构图。用户连接到托管在实例上的前端。
|
<strong>提示词:</strong> 使用
|
||||||
|
<strong>GCP图标</strong>
|
||||||
|
生成一个GCP架构图。用户连接到托管在实例上的前端。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/gcp_demo.svg" alt="GCP架构图" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/gcp_demo.svg"
|
||||||
|
alt="GCP架构图"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS架构图</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
AWS架构图
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用<strong>AWS图标</strong>生成一个AWS架构图。用户连接到托管在实例上的前端。
|
<strong>提示词:</strong> 使用
|
||||||
|
<strong>AWS图标</strong>
|
||||||
|
生成一个AWS架构图。用户连接到托管在实例上的前端。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/aws_demo.svg" alt="AWS架构图" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/aws_demo.svg"
|
||||||
|
alt="AWS架构图"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure架构图</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Azure架构图
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 使用<strong>Azure图标</strong>生成一个Azure架构图。用户连接到托管在实例上的前端。
|
<strong>提示词:</strong> 使用
|
||||||
|
<strong>Azure图标</strong>
|
||||||
|
生成一个Azure架构图。用户连接到托管在实例上的前端。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/azure_demo.svg" alt="Azure架构图" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/azure_demo.svg"
|
||||||
|
alt="Azure架构图"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">猫咪素描</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
猫咪素描
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>提示词:</strong> 给我画一只可爱的猫。
|
<strong>提示词:</strong>{" "}
|
||||||
|
给我画一只可爱的猫。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/cat_demo.svg" alt="猫咪绘图" width={240} height={240} className="mx-auto" />
|
<Image
|
||||||
|
src="/cat_demo.svg"
|
||||||
|
alt="猫咪绘图"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">工作原理</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
工作原理
|
||||||
|
</h2>
|
||||||
<p className="text-gray-700 mb-4">本应用使用以下技术:</p>
|
<p className="text-gray-700 mb-4">本应用使用以下技术:</p>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li><strong>Next.js</strong>:用于前端框架和路由</li>
|
<li>
|
||||||
<li><strong>Vercel AI SDK</strong>(<code>ai</code> + <code>@ai-sdk/*</code>):用于流式AI响应和多提供商支持</li>
|
<strong>Next.js</strong>:用于前端框架和路由
|
||||||
<li><strong>react-drawio</strong>:用于图表表示和操作</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Vercel AI SDK</strong>(<code>ai</code> +{" "}
|
||||||
|
<code>@ai-sdk/*</code>
|
||||||
|
):用于流式AI响应和多提供商支持
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>react-drawio</strong>:用于图表表示和操作
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Multi-Provider Support */}
|
{/* Multi-Provider Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">多提供商支持</h2>
|
<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">
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
<li>AWS Bedrock(默认)</li>
|
<li>AWS Bedrock(默认)</li>
|
||||||
<li>OpenAI / OpenAI兼容API(通过 <code>OPENAI_BASE_URL</code>)</li>
|
<li>
|
||||||
|
OpenAI / OpenAI兼容API(通过{" "}
|
||||||
|
<code>OPENAI_BASE_URL</code>)
|
||||||
|
</li>
|
||||||
<li>Anthropic</li>
|
<li>Anthropic</li>
|
||||||
<li>Google AI</li>
|
<li>Google AI</li>
|
||||||
<li>Azure OpenAI</li>
|
<li>Azure OpenAI</li>
|
||||||
@@ -151,12 +267,15 @@ export default function AboutCN() {
|
|||||||
<li>DeepSeek</li>
|
<li>DeepSeek</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
注意:<code>claude-sonnet-4-5</code> 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
注意:<code>claude-sonnet-4-5</code>{" "}
|
||||||
|
已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<div className="flex items-center gap-4 mt-10 mb-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
<h2 className="text-2xl font-semibold text-gray-900">支持与联系</h2>
|
<h2 className="text-2xl font-semibold text-gray-900">
|
||||||
|
支持与联系
|
||||||
|
</h2>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/DayuanJiang/button"
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
title="Sponsor DayuanJiang"
|
title="Sponsor DayuanJiang"
|
||||||
@@ -167,14 +286,24 @@ export default function AboutCN() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
如果您觉得这个项目有用,请考虑{" "}
|
如果您觉得这个项目有用,请考虑{" "}
|
||||||
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
赞助
|
赞助
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
来帮助托管在线演示站点!
|
来帮助托管在线演示站点!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mt-2">
|
<p className="text-gray-700 mt-2">
|
||||||
如需支持或咨询,请在{" "}
|
如需支持或咨询,请在{" "}
|
||||||
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
GitHub仓库
|
GitHub仓库
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
上提交issue或联系:me[at]jiang.jp
|
上提交issue或联系:me[at]jiang.jp
|
||||||
@@ -201,5 +330,5 @@ export default function AboutCN() {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next"
|
||||||
import Link from "next/link";
|
import Image from "next/image"
|
||||||
import { FaGithub } from "react-icons/fa";
|
import Link from "next/link"
|
||||||
import Image from "next/image";
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "概要 - Next AI Draw.io",
|
title: "概要 - Next AI Draw.io",
|
||||||
description: "AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
|
description:
|
||||||
keywords: ["AIダイアグラム", "draw.io", "AWSアーキテクチャ", "GCPダイアグラム", "Azureダイアグラム", "LLM"],
|
"AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
|
||||||
};
|
keywords: [
|
||||||
|
"AIダイアグラム",
|
||||||
|
"draw.io",
|
||||||
|
"AWSアーキテクチャ",
|
||||||
|
"GCPダイアグラム",
|
||||||
|
"Azureダイアグラム",
|
||||||
|
"LLM",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
export default function AboutJA() {
|
export default function AboutJA() {
|
||||||
return (
|
return (
|
||||||
@@ -16,14 +24,23 @@ export default function AboutJA() {
|
|||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link 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
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
エディタ
|
エディタ
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/about/ja" className="text-blue-600 font-semibold">
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
概要
|
概要
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
@@ -45,22 +62,43 @@ export default function AboutJA() {
|
|||||||
<article className="prose prose-lg max-w-none">
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
<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">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI搭載のダイアグラム作成ツール - チャット、描画、可視化
|
AI搭載のダイアグラム作成ツール -
|
||||||
|
チャット、描画、可視化
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4 mt-4 text-sm">
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Link>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600">中文</Link>
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Link>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link href="/about/ja" className="text-blue-600 font-semibold">日本語</Link>
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<p className="text-amber-800">
|
<p className="text-amber-800">
|
||||||
本アプリは最高のパフォーマンスを発揮するため、Claude Opus 4.5 で動作するよう設計されています。しかし、予想以上のトラフィックにより、最上位モデルの運用コストが負担となっています。サービスの中断を避け、コストを管理するため、バックエンドを Claude Haiku 4.5 に切り替えました。
|
本アプリは最高のパフォーマンスを発揮するため、Claude
|
||||||
|
Opus 4.5
|
||||||
|
で動作するよう設計されています。しかし、予想以上のトラフィックにより、最上位モデルの運用コストが負担となっています。サービスの中断を避け、コストを管理するため、バックエンドを
|
||||||
|
Claude Haiku 4.5 に切り替えました。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,80 +107,176 @@ export default function AboutJA() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">機能</h2>
|
<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">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li><strong>LLM搭載のダイアグラム作成</strong>:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作</li>
|
<li>
|
||||||
<li><strong>画像ベースのダイアグラム複製</strong>:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化</li>
|
<strong>LLM搭載のダイアグラム作成</strong>
|
||||||
<li><strong>ダイアグラム履歴</strong>:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能</li>
|
:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
||||||
<li><strong>インタラクティブなチャットインターフェース</strong>:AIとリアルタイムでコミュニケーションしてダイアグラムを改善</li>
|
</li>
|
||||||
<li><strong>AWSアーキテクチャダイアグラムサポート</strong>:AWSアーキテクチャダイアグラムの生成を専門的にサポート</li>
|
<li>
|
||||||
<li><strong>アニメーションコネクタ</strong>:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成</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>
|
</ul>
|
||||||
|
|
||||||
{/* Examples */}
|
{/* Examples */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">例</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
<p className="text-gray-700 mb-6">以下はいくつかのプロンプト例と生成されたダイアグラムです:</p>
|
例
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* Animated Transformer */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">アニメーションTransformerコネクタ</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
アニメーションTransformerコネクタ
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>プロンプト:</strong> <strong>アニメーションコネクタ</strong>付きのTransformerアーキテクチャ図を作成してください。
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>アニメーションコネクタ</strong>
|
||||||
|
付きのTransformerアーキテクチャ図を作成してください。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width={480} height={360} className="mx-auto" />
|
<Image
|
||||||
|
src="/animated_connectors.svg"
|
||||||
|
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
|
||||||
|
width={480}
|
||||||
|
height={360}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCPアーキテクチャ図</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
GCPアーキテクチャ図
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong> <strong>GCPアイコン</strong>を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>GCPアイコン</strong>
|
||||||
|
を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/gcp_demo.svg" alt="GCPアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/gcp_demo.svg"
|
||||||
|
alt="GCPアーキテクチャ図"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWSアーキテクチャ図</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
AWSアーキテクチャ図
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong> <strong>AWSアイコン</strong>を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>AWSアイコン</strong>
|
||||||
|
を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/aws_demo.svg" alt="AWSアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/aws_demo.svg"
|
||||||
|
alt="AWSアーキテクチャ図"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azureアーキテクチャ図</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Azureアーキテクチャ図
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong> <strong>Azureアイコン</strong>を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>Azureアイコン</strong>
|
||||||
|
を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/azure_demo.svg" alt="Azureアーキテクチャ図" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/azure_demo.svg"
|
||||||
|
alt="Azureアーキテクチャ図"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">猫のスケッチ</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
猫のスケッチ
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>プロンプト:</strong> かわいい猫を描いてください。
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
かわいい猫を描いてください。
|
||||||
</p>
|
</p>
|
||||||
<Image src="/cat_demo.svg" alt="猫の絵" width={240} height={240} className="mx-auto" />
|
<Image
|
||||||
|
src="/cat_demo.svg"
|
||||||
|
alt="猫の絵"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">仕組み</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
<p className="text-gray-700 mb-4">本アプリケーションは以下の技術を使用しています:</p>
|
仕組み
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4">
|
||||||
|
本アプリケーションは以下の技術を使用しています:
|
||||||
|
</p>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li><strong>Next.js</strong>:フロントエンドフレームワークとルーティング</li>
|
<li>
|
||||||
<li><strong>Vercel AI SDK</strong>(<code>ai</code> + <code>@ai-sdk/*</code>):ストリーミングAIレスポンスとマルチプロバイダーサポート</li>
|
<strong>Next.js</strong>
|
||||||
<li><strong>react-drawio</strong>:ダイアグラムの表現と操作</li>
|
:フロントエンドフレームワークとルーティング
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Vercel AI SDK</strong>(<code>ai</code> +{" "}
|
||||||
|
<code>@ai-sdk/*</code>
|
||||||
|
):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>react-drawio</strong>
|
||||||
|
:ダイアグラムの表現と操作
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Multi-Provider Support */}
|
{/* Multi-Provider Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">マルチプロバイダーサポート</h2>
|
<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">
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
<li>AWS Bedrock(デフォルト)</li>
|
<li>AWS Bedrock(デフォルト)</li>
|
||||||
<li>OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>経由)</li>
|
<li>
|
||||||
|
OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>
|
||||||
|
経由)
|
||||||
|
</li>
|
||||||
<li>Anthropic</li>
|
<li>Anthropic</li>
|
||||||
<li>Google AI</li>
|
<li>Google AI</li>
|
||||||
<li>Azure OpenAI</li>
|
<li>Azure OpenAI</li>
|
||||||
@@ -151,12 +285,15 @@ export default function AboutJA() {
|
|||||||
<li>DeepSeek</li>
|
<li>DeepSeek</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
注:<code>claude-sonnet-4-5</code>はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
注:<code>claude-sonnet-4-5</code>
|
||||||
|
はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<div className="flex items-center gap-4 mt-10 mb-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
<h2 className="text-2xl font-semibold text-gray-900">サポート&お問い合わせ</h2>
|
<h2 className="text-2xl font-semibold text-gray-900">
|
||||||
|
サポート&お問い合わせ
|
||||||
|
</h2>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/DayuanJiang/button"
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
title="Sponsor DayuanJiang"
|
title="Sponsor DayuanJiang"
|
||||||
@@ -167,14 +304,24 @@ export default function AboutJA() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
||||||
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
スポンサー
|
スポンサー
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
をご検討ください!
|
をご検討ください!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mt-2">
|
<p className="text-gray-700 mt-2">
|
||||||
サポートやお問い合わせについては、{" "}
|
サポートやお問い合わせについては、{" "}
|
||||||
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
GitHubリポジトリ
|
GitHubリポジトリ
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
でissueを開くか、ご連絡ください:me[at]jiang.jp
|
でissueを開くか、ご連絡ください:me[at]jiang.jp
|
||||||
@@ -196,10 +343,11 @@ export default function AboutJA() {
|
|||||||
<footer className="bg-white border-t border-gray-200 mt-16">
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<p className="text-center text-gray-600 text-sm">
|
<p className="text-center text-gray-600 text-sm">
|
||||||
Next AI Draw.io - オープンソースAI搭載ダイアグラムジェネレーター
|
Next AI Draw.io -
|
||||||
|
オープンソースAI搭載ダイアグラムジェネレーター
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next"
|
||||||
import Link from "next/link";
|
import Image from "next/image"
|
||||||
import { FaGithub } from "react-icons/fa";
|
import Link from "next/link"
|
||||||
import Image from "next/image";
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "About - Next AI Draw.io",
|
title: "About - Next AI Draw.io",
|
||||||
description: "AI-Powered Diagram Creation Tool - Chat, Draw, Visualize. Create AWS, GCP, and Azure architecture diagrams with natural language.",
|
description:
|
||||||
keywords: ["AI diagram", "draw.io", "AWS architecture", "GCP diagram", "Azure diagram", "LLM"],
|
"AI-Powered Diagram Creation Tool - Chat, Draw, Visualize. Create AWS, GCP, and Azure architecture diagrams with natural language.",
|
||||||
};
|
keywords: [
|
||||||
|
"AI diagram",
|
||||||
|
"draw.io",
|
||||||
|
"AWS architecture",
|
||||||
|
"GCP diagram",
|
||||||
|
"Azure diagram",
|
||||||
|
"LLM",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
@@ -16,14 +24,23 @@ export default function About() {
|
|||||||
<header className="bg-white border-b border-gray-200">
|
<header className="bg-white border-b border-gray-200">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link 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
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
Editor
|
Editor
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/about" className="text-blue-600 font-semibold">
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
About
|
About
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
@@ -45,105 +62,236 @@ export default function About() {
|
|||||||
<article className="prose prose-lg max-w-none">
|
<article className="prose prose-lg max-w-none">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
|
<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">
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
AI-Powered Diagram Creation Tool - Chat, Draw, Visualize
|
AI-Powered Diagram Creation Tool - Chat, Draw,
|
||||||
|
Visualize
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4 mt-4 text-sm">
|
<div className="flex justify-center gap-4 mt-4 text-sm">
|
||||||
<Link href="/about" className="text-blue-600 font-semibold">English</Link>
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</Link>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600">中文</Link>
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</Link>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600">日本語</Link>
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-gray-600 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
<p className="text-amber-800">
|
<p className="text-amber-800">
|
||||||
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.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
A Next.js web application that integrates AI capabilities with draw.io diagrams.
|
A Next.js web application that integrates AI
|
||||||
Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
capabilities with draw.io diagrams. Create, modify, and
|
||||||
|
enhance diagrams through natural language commands and
|
||||||
|
AI-assisted visualization.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Features</h2>
|
<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">
|
<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>
|
||||||
<li><strong>Image-Based Diagram Replication</strong>: Upload existing diagrams or images and have the AI replicate and enhance them automatically</li>
|
<strong>LLM-Powered Diagram Creation</strong>:
|
||||||
<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>
|
Leverage Large Language Models to create and
|
||||||
<li><strong>Interactive Chat Interface</strong>: Communicate with AI to refine your diagrams in real-time</li>
|
manipulate draw.io diagrams directly through natural
|
||||||
<li><strong>AWS Architecture Diagram Support</strong>: Specialized support for generating AWS architecture diagrams</li>
|
language commands
|
||||||
<li><strong>Animated Connectors</strong>: Create dynamic and animated connectors between diagram elements for better visualization</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Image-Based Diagram Replication</strong>:
|
||||||
|
Upload existing diagrams or images and have the AI
|
||||||
|
replicate and enhance them automatically
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Diagram History</strong>: Comprehensive
|
||||||
|
version control that tracks all changes, allowing
|
||||||
|
you to view and restore previous versions of your
|
||||||
|
diagrams before the AI editing
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Interactive Chat Interface</strong>:
|
||||||
|
Communicate with AI to refine your diagrams in
|
||||||
|
real-time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>AWS Architecture Diagram Support</strong>:
|
||||||
|
Specialized support for generating AWS architecture
|
||||||
|
diagrams
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Animated Connectors</strong>: Create dynamic
|
||||||
|
and animated connectors between diagram elements for
|
||||||
|
better visualization
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Examples */}
|
{/* Examples */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Examples</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
<p className="text-gray-700 mb-6">Here are some example prompts and their generated diagrams:</p>
|
Examples
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
Here are some example prompts and their generated
|
||||||
|
diagrams:
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Animated Transformer */}
|
{/* Animated Transformer */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Animated Transformer Connectors</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Animated Transformer Connectors
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
<strong>Prompt:</strong> Give me an <strong>animated connector</strong> diagram of transformer's architecture.
|
<strong>Prompt:</strong> Give me an{" "}
|
||||||
|
<strong>animated connector</strong> diagram of
|
||||||
|
transformer's architecture.
|
||||||
</p>
|
</p>
|
||||||
<Image src="/animated_connectors.svg" alt="Transformer Architecture with Animated Connectors" width={480} height={360} className="mx-auto" />
|
<Image
|
||||||
|
src="/animated_connectors.svg"
|
||||||
|
alt="Transformer Architecture with Animated Connectors"
|
||||||
|
width={480}
|
||||||
|
height={360}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cloud Architecture Grid */}
|
{/* Cloud Architecture Grid */}
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP Architecture Diagram</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
GCP Architecture Diagram
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate a GCP architecture diagram with <strong>GCP icons</strong>. Users connect to a frontend hosted on an instance.
|
<strong>Prompt:</strong> Generate a GCP
|
||||||
|
architecture diagram with{" "}
|
||||||
|
<strong>GCP icons</strong>. Users connect to
|
||||||
|
a frontend hosted on an instance.
|
||||||
</p>
|
</p>
|
||||||
<Image src="/gcp_demo.svg" alt="GCP Architecture Diagram" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/gcp_demo.svg"
|
||||||
|
alt="GCP Architecture Diagram"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS Architecture Diagram</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
AWS Architecture Diagram
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate an AWS architecture diagram with <strong>AWS icons</strong>. Users connect to a frontend hosted on an instance.
|
<strong>Prompt:</strong> Generate an AWS
|
||||||
|
architecture diagram with{" "}
|
||||||
|
<strong>AWS icons</strong>. Users connect to
|
||||||
|
a frontend hosted on an instance.
|
||||||
</p>
|
</p>
|
||||||
<Image src="/aws_demo.svg" alt="AWS Architecture Diagram" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/aws_demo.svg"
|
||||||
|
alt="AWS Architecture Diagram"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure Architecture Diagram</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Azure Architecture Diagram
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Generate an Azure architecture diagram with <strong>Azure icons</strong>. Users connect to a frontend hosted on an instance.
|
<strong>Prompt:</strong> Generate an Azure
|
||||||
|
architecture diagram with{" "}
|
||||||
|
<strong>Azure icons</strong>. Users connect
|
||||||
|
to a frontend hosted on an instance.
|
||||||
</p>
|
</p>
|
||||||
<Image src="/azure_demo.svg" alt="Azure Architecture Diagram" width={400} height={300} className="mx-auto" />
|
<Image
|
||||||
|
src="/azure_demo.svg"
|
||||||
|
alt="Azure Architecture Diagram"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Cat Sketch</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Cat Sketch
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
<strong>Prompt:</strong> Draw a cute cat for me.
|
<strong>Prompt:</strong> Draw a cute cat for
|
||||||
|
me.
|
||||||
</p>
|
</p>
|
||||||
<Image src="/cat_demo.svg" alt="Cat Drawing" width={240} height={240} className="mx-auto" />
|
<Image
|
||||||
|
src="/cat_demo.svg"
|
||||||
|
alt="Cat Drawing"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">How It Works</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
<p className="text-gray-700 mb-4">The application uses the following technologies:</p>
|
How It Works
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4">
|
||||||
|
The application uses the following technologies:
|
||||||
|
</p>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
<li><strong>Next.js</strong>: For the frontend framework and routing</li>
|
<li>
|
||||||
<li><strong>Vercel AI SDK</strong> (<code>ai</code> + <code>@ai-sdk/*</code>): For streaming AI responses and multi-provider support</li>
|
<strong>Next.js</strong>: For the frontend framework
|
||||||
<li><strong>react-drawio</strong>: For diagram representation and manipulation</li>
|
and routing
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Vercel AI SDK</strong> (<code>ai</code> +{" "}
|
||||||
|
<code>@ai-sdk/*</code>): For streaming AI responses
|
||||||
|
and multi-provider support
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>react-drawio</strong>: For diagram
|
||||||
|
representation and manipulation
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
Diagrams are represented as XML that can be rendered in 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.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Multi-Provider Support */}
|
{/* Multi-Provider Support */}
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Multi-Provider Support</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
Multi-Provider Support
|
||||||
|
</h2>
|
||||||
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
<li>AWS Bedrock (default)</li>
|
<li>AWS Bedrock (default)</li>
|
||||||
<li>OpenAI / OpenAI-compatible APIs (via <code>OPENAI_BASE_URL</code>)</li>
|
<li>
|
||||||
|
OpenAI / OpenAI-compatible APIs (via{" "}
|
||||||
|
<code>OPENAI_BASE_URL</code>)
|
||||||
|
</li>
|
||||||
<li>Anthropic</li>
|
<li>Anthropic</li>
|
||||||
<li>Google AI</li>
|
<li>Google AI</li>
|
||||||
<li>Azure OpenAI</li>
|
<li>Azure OpenAI</li>
|
||||||
@@ -152,12 +300,17 @@ export default function About() {
|
|||||||
<li>DeepSeek</li>
|
<li>DeepSeek</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-gray-700 mt-4">
|
<p className="text-gray-700 mt-4">
|
||||||
Note that <code>claude-sonnet-4-5</code> has trained on draw.io diagrams with AWS logos, so if you want to create AWS architecture diagrams, this is the best choice.
|
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>
|
</p>
|
||||||
|
|
||||||
{/* Support */}
|
{/* Support */}
|
||||||
<div className="flex items-center gap-4 mt-10 mb-4">
|
<div className="flex items-center gap-4 mt-10 mb-4">
|
||||||
<h2 className="text-2xl font-semibold text-gray-900">Support & Contact</h2>
|
<h2 className="text-2xl font-semibold text-gray-900">
|
||||||
|
Support & Contact
|
||||||
|
</h2>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/DayuanJiang/button"
|
src="https://github.com/sponsors/DayuanJiang/button"
|
||||||
title="Sponsor DayuanJiang"
|
title="Sponsor DayuanJiang"
|
||||||
@@ -168,14 +321,24 @@ export default function About() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
If you find this project useful, please consider{" "}
|
If you find this project useful, please consider{" "}
|
||||||
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
sponsoring
|
sponsoring
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
to help host the live demo site!
|
to help host the live demo site!
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 mt-2">
|
<p className="text-gray-700 mt-2">
|
||||||
For support or inquiries, please open an issue on the{" "}
|
For support or inquiries, please open an issue on the{" "}
|
||||||
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
GitHub repository
|
GitHub repository
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
or contact: me[at]jiang.jp
|
or contact: me[at]jiang.jp
|
||||||
@@ -197,10 +360,11 @@ export default function About() {
|
|||||||
<footer className="bg-white border-t border-gray-200 mt-16">
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<p className="text-center text-gray-600 text-sm">
|
<p className="text-center text-gray-600 text-sm">
|
||||||
Next AI Draw.io - Open Source AI-Powered Diagram Generator
|
Next AI Draw.io - Open Source AI-Powered Diagram
|
||||||
|
Generator
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,247 +1,299 @@
|
|||||||
import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai';
|
import {
|
||||||
import { getAIModel } from '@/lib/ai-providers';
|
convertToModelMessages,
|
||||||
import { findCachedResponse } from '@/lib/cached-responses';
|
createUIMessageStream,
|
||||||
import { setTraceInput, setTraceOutput, getTelemetryConfig, wrapWithObserve } from '@/lib/langfuse';
|
createUIMessageStreamResponse,
|
||||||
import { getSystemPrompt } from '@/lib/system-prompts';
|
streamText,
|
||||||
import { z } from "zod";
|
} from "ai"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { getAIModel } from "@/lib/ai-providers"
|
||||||
|
import { findCachedResponse } from "@/lib/cached-responses"
|
||||||
|
import {
|
||||||
|
getTelemetryConfig,
|
||||||
|
setTraceInput,
|
||||||
|
setTraceOutput,
|
||||||
|
wrapWithObserve,
|
||||||
|
} from "@/lib/langfuse"
|
||||||
|
import { getSystemPrompt } from "@/lib/system-prompts"
|
||||||
|
|
||||||
export const maxDuration = 300;
|
export const maxDuration = 60
|
||||||
|
|
||||||
// File upload limits (must match client-side)
|
// File upload limits (must match client-side)
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||||
const MAX_FILES = 5;
|
const MAX_FILES = 5
|
||||||
|
|
||||||
// Helper function to validate file parts in messages
|
// Helper function to validate file parts in messages
|
||||||
function validateFileParts(messages: any[]): { valid: boolean; error?: string } {
|
function validateFileParts(messages: any[]): {
|
||||||
const lastMessage = messages[messages.length - 1];
|
valid: boolean
|
||||||
const fileParts = lastMessage?.parts?.filter((p: any) => p.type === 'file') || [];
|
error?: string
|
||||||
|
} {
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
const fileParts =
|
||||||
|
lastMessage?.parts?.filter((p: any) => p.type === "file") || []
|
||||||
|
|
||||||
if (fileParts.length > MAX_FILES) {
|
if (fileParts.length > MAX_FILES) {
|
||||||
return { valid: false, error: `Too many files. Maximum ${MAX_FILES} allowed.` };
|
return {
|
||||||
}
|
valid: false,
|
||||||
|
error: `Too many files. Maximum ${MAX_FILES} allowed.`,
|
||||||
for (const filePart of fileParts) {
|
|
||||||
// Data URLs format: data:image/png;base64,<data>
|
|
||||||
// Base64 increases size by ~33%, so we check the decoded size
|
|
||||||
if (filePart.url && filePart.url.startsWith('data:')) {
|
|
||||||
const base64Data = filePart.url.split(',')[1];
|
|
||||||
if (base64Data) {
|
|
||||||
const sizeInBytes = Math.ceil((base64Data.length * 3) / 4);
|
|
||||||
if (sizeInBytes > MAX_FILE_SIZE) {
|
|
||||||
return { valid: false, error: `File exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.` };
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true };
|
for (const filePart of fileParts) {
|
||||||
|
// Data URLs format: data:image/png;base64,<data>
|
||||||
|
// Base64 increases size by ~33%, so we check the decoded size
|
||||||
|
if (filePart.url?.startsWith("data:")) {
|
||||||
|
const base64Data = filePart.url.split(",")[1]
|
||||||
|
if (base64Data) {
|
||||||
|
const sizeInBytes = Math.ceil((base64Data.length * 3) / 4)
|
||||||
|
if (sizeInBytes > MAX_FILE_SIZE) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `File exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if diagram is minimal/empty
|
// Helper function to check if diagram is minimal/empty
|
||||||
function isMinimalDiagram(xml: string): boolean {
|
function isMinimalDiagram(xml: string): boolean {
|
||||||
const stripped = xml.replace(/\s/g, '');
|
const stripped = xml.replace(/\s/g, "")
|
||||||
return !stripped.includes('id="2"');
|
return !stripped.includes('id="2"')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to create cached stream response
|
// Helper function to create cached stream response
|
||||||
function createCachedStreamResponse(xml: string): Response {
|
function createCachedStreamResponse(xml: string): Response {
|
||||||
const toolCallId = `cached-${Date.now()}`;
|
const toolCallId = `cached-${Date.now()}`
|
||||||
|
|
||||||
const stream = createUIMessageStream({
|
const stream = createUIMessageStream({
|
||||||
execute: async ({ writer }) => {
|
execute: async ({ writer }) => {
|
||||||
writer.write({ type: 'start' });
|
writer.write({ type: "start" })
|
||||||
writer.write({ type: 'tool-input-start', toolCallId, toolName: 'display_diagram' });
|
writer.write({
|
||||||
writer.write({ type: 'tool-input-delta', toolCallId, inputTextDelta: xml });
|
type: "tool-input-start",
|
||||||
writer.write({ type: 'tool-input-available', toolCallId, toolName: 'display_diagram', input: { xml } });
|
toolCallId,
|
||||||
writer.write({ type: 'finish' });
|
toolName: "display_diagram",
|
||||||
},
|
})
|
||||||
});
|
writer.write({
|
||||||
|
type: "tool-input-delta",
|
||||||
|
toolCallId,
|
||||||
|
inputTextDelta: xml,
|
||||||
|
})
|
||||||
|
writer.write({
|
||||||
|
type: "tool-input-available",
|
||||||
|
toolCallId,
|
||||||
|
toolName: "display_diagram",
|
||||||
|
input: { xml },
|
||||||
|
})
|
||||||
|
writer.write({ type: "finish" })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return createUIMessageStreamResponse({ stream });
|
return createUIMessageStreamResponse({ stream })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner handler function
|
// Inner handler function
|
||||||
async function handleChatRequest(req: Request): Promise<Response> {
|
async function handleChatRequest(req: Request): Promise<Response> {
|
||||||
// Check for access code
|
// Check for access code
|
||||||
const accessCodes = process.env.ACCESS_CODE_LIST?.split(',').map(code => code.trim()).filter(Boolean) || [];
|
const accessCodes =
|
||||||
if (accessCodes.length > 0) {
|
process.env.ACCESS_CODE_LIST?.split(",")
|
||||||
const accessCodeHeader = req.headers.get('x-access-code');
|
.map((code) => code.trim())
|
||||||
if (!accessCodeHeader || !accessCodes.includes(accessCodeHeader)) {
|
.filter(Boolean) || []
|
||||||
return Response.json(
|
if (accessCodes.length > 0) {
|
||||||
{ error: 'Invalid or missing access code. Please configure it in Settings.' },
|
const accessCodeHeader = req.headers.get("x-access-code")
|
||||||
{ status: 401 }
|
if (!accessCodeHeader || !accessCodes.includes(accessCodeHeader)) {
|
||||||
);
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: "Invalid or missing access code. Please configure it in Settings.",
|
||||||
|
},
|
||||||
|
{ status: 401 },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const { messages, xml, sessionId } = await req.json();
|
const { messages, xml, sessionId } = await req.json()
|
||||||
|
|
||||||
// Get user IP for Langfuse tracking
|
// Get user IP for Langfuse tracking
|
||||||
const forwardedFor = req.headers.get('x-forwarded-for');
|
const forwardedFor = req.headers.get("x-forwarded-for")
|
||||||
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
const userId = forwardedFor?.split(",")[0]?.trim() || "anonymous"
|
||||||
|
|
||||||
// Validate sessionId for Langfuse (must be string, max 200 chars)
|
// Validate sessionId for Langfuse (must be string, max 200 chars)
|
||||||
const validSessionId = sessionId && typeof sessionId === 'string' && sessionId.length <= 200
|
const validSessionId =
|
||||||
? sessionId
|
sessionId && typeof sessionId === "string" && sessionId.length <= 200
|
||||||
: undefined;
|
? sessionId
|
||||||
|
: undefined
|
||||||
|
|
||||||
// Extract user input text for Langfuse trace
|
// Extract user input text for Langfuse trace
|
||||||
const currentMessage = messages[messages.length - 1];
|
const currentMessage = messages[messages.length - 1]
|
||||||
const userInputText = currentMessage?.parts?.find((p: any) => p.type === 'text')?.text || '';
|
const userInputText =
|
||||||
|
currentMessage?.parts?.find((p: any) => p.type === "text")?.text || ""
|
||||||
|
|
||||||
// Update Langfuse trace with input, session, and user
|
// Update Langfuse trace with input, session, and user
|
||||||
setTraceInput({
|
setTraceInput({
|
||||||
input: userInputText,
|
input: userInputText,
|
||||||
sessionId: validSessionId,
|
sessionId: validSessionId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
})
|
||||||
|
|
||||||
// === FILE VALIDATION START ===
|
// === FILE VALIDATION START ===
|
||||||
const fileValidation = validateFileParts(messages);
|
const fileValidation = validateFileParts(messages)
|
||||||
if (!fileValidation.valid) {
|
if (!fileValidation.valid) {
|
||||||
return Response.json({ error: fileValidation.error }, { status: 400 });
|
return Response.json({ error: fileValidation.error }, { status: 400 })
|
||||||
}
|
|
||||||
// === FILE VALIDATION END ===
|
|
||||||
|
|
||||||
// === CACHE CHECK START ===
|
|
||||||
const isFirstMessage = messages.length === 1;
|
|
||||||
const isEmptyDiagram = !xml || xml.trim() === '' || isMinimalDiagram(xml);
|
|
||||||
|
|
||||||
if (isFirstMessage && isEmptyDiagram) {
|
|
||||||
const lastMessage = messages[0];
|
|
||||||
const textPart = lastMessage.parts?.find((p: any) => p.type === 'text');
|
|
||||||
const filePart = lastMessage.parts?.find((p: any) => p.type === 'file');
|
|
||||||
|
|
||||||
const cached = findCachedResponse(textPart?.text || '', !!filePart);
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
console.log('[Cache] Returning cached response for:', textPart?.text);
|
|
||||||
return createCachedStreamResponse(cached.xml);
|
|
||||||
}
|
}
|
||||||
}
|
// === FILE VALIDATION END ===
|
||||||
// === CACHE CHECK END ===
|
|
||||||
|
|
||||||
// Get AI model from environment configuration
|
// === CACHE CHECK START ===
|
||||||
const { model, providerOptions, headers, modelId } = getAIModel();
|
const isFirstMessage = messages.length === 1
|
||||||
|
const isEmptyDiagram = !xml || xml.trim() === "" || isMinimalDiagram(xml)
|
||||||
|
|
||||||
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
if (isFirstMessage && isEmptyDiagram) {
|
||||||
const systemMessage = getSystemPrompt(modelId);
|
const lastMessage = messages[0]
|
||||||
|
const textPart = lastMessage.parts?.find((p: any) => p.type === "text")
|
||||||
|
const filePart = lastMessage.parts?.find((p: any) => p.type === "file")
|
||||||
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
const cached = findCachedResponse(textPart?.text || "", !!filePart)
|
||||||
|
|
||||||
// Extract text from the last message parts
|
if (cached) {
|
||||||
const lastMessageText = lastMessage.parts?.find((part: any) => part.type === 'text')?.text || '';
|
console.log(
|
||||||
|
"[Cache] Returning cached response for:",
|
||||||
|
textPart?.text,
|
||||||
|
)
|
||||||
|
return createCachedStreamResponse(cached.xml)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// === CACHE CHECK END ===
|
||||||
|
|
||||||
// Extract file parts (images) from the last message
|
// Get AI model from environment configuration
|
||||||
const fileParts = lastMessage.parts?.filter((part: any) => part.type === 'file') || [];
|
const { model, providerOptions, headers, modelId } = getAIModel()
|
||||||
|
|
||||||
// User input only - XML is now in a separate cached system message
|
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
|
||||||
const formattedUserInput = `User input:
|
const systemMessage = getSystemPrompt(modelId)
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
|
||||||
|
// Extract text from the last message parts
|
||||||
|
const lastMessageText =
|
||||||
|
lastMessage.parts?.find((part: any) => part.type === "text")?.text || ""
|
||||||
|
|
||||||
|
// Extract file parts (images) from the last message
|
||||||
|
const fileParts =
|
||||||
|
lastMessage.parts?.filter((part: any) => part.type === "file") || []
|
||||||
|
|
||||||
|
// User input only - XML is now in a separate cached system message
|
||||||
|
const formattedUserInput = `User input:
|
||||||
"""md
|
"""md
|
||||||
${lastMessageText}
|
${lastMessageText}
|
||||||
"""`;
|
"""`
|
||||||
|
|
||||||
// Convert UIMessages to ModelMessages and add system message
|
// Convert UIMessages to ModelMessages and add system message
|
||||||
const modelMessages = convertToModelMessages(messages);
|
const modelMessages = convertToModelMessages(messages)
|
||||||
|
|
||||||
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
// Filter out messages with empty content arrays (Bedrock API rejects these)
|
||||||
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
// This is a safety measure - ideally convertToModelMessages should handle all cases
|
||||||
let enhancedMessages = modelMessages.filter((msg: any) =>
|
let enhancedMessages = modelMessages.filter(
|
||||||
msg.content && Array.isArray(msg.content) && msg.content.length > 0
|
(msg: any) =>
|
||||||
);
|
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
|
||||||
|
)
|
||||||
|
|
||||||
// Update the last message with user input only (XML moved to separate cached system message)
|
// Update the last message with user input only (XML moved to separate cached system message)
|
||||||
if (enhancedMessages.length >= 1) {
|
if (enhancedMessages.length >= 1) {
|
||||||
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1];
|
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
|
||||||
if (lastModelMessage.role === 'user') {
|
if (lastModelMessage.role === "user") {
|
||||||
// Build content array with user input text and file parts
|
// Build content array with user input text and file parts
|
||||||
const contentParts: any[] = [
|
const contentParts: any[] = [
|
||||||
{ type: 'text', text: formattedUserInput }
|
{ type: "text", text: formattedUserInput },
|
||||||
];
|
]
|
||||||
|
|
||||||
// Add image parts back
|
// Add image parts back
|
||||||
for (const filePart of fileParts) {
|
for (const filePart of fileParts) {
|
||||||
contentParts.push({
|
contentParts.push({
|
||||||
type: 'image',
|
type: "image",
|
||||||
image: filePart.url,
|
image: filePart.url,
|
||||||
mimeType: filePart.mediaType
|
mimeType: filePart.mediaType,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
enhancedMessages = [
|
enhancedMessages = [
|
||||||
...enhancedMessages.slice(0, -1),
|
...enhancedMessages.slice(0, -1),
|
||||||
{ ...lastModelMessage, content: contentParts }
|
{ ...lastModelMessage, content: contentParts },
|
||||||
];
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add cache point to the last assistant message in conversation history
|
// Add cache point to the last assistant message in conversation history
|
||||||
// This caches the entire conversation prefix for subsequent requests
|
// This caches the entire conversation prefix for subsequent requests
|
||||||
// Strategy: system (cached) + history with last assistant (cached) + new user message
|
// Strategy: system (cached) + history with last assistant (cached) + new user message
|
||||||
if (enhancedMessages.length >= 2) {
|
if (enhancedMessages.length >= 2) {
|
||||||
// Find the last assistant message (should be second-to-last, before current user message)
|
// Find the last assistant message (should be second-to-last, before current user message)
|
||||||
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
|
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
|
||||||
if (enhancedMessages[i].role === 'assistant') {
|
if (enhancedMessages[i].role === "assistant") {
|
||||||
enhancedMessages[i] = {
|
enhancedMessages[i] = {
|
||||||
...enhancedMessages[i],
|
...enhancedMessages[i],
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
bedrock: { cachePoint: { type: 'default' } },
|
bedrock: { cachePoint: { type: "default" } },
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
break; // Only cache the last assistant message
|
break // Only cache the last assistant message
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// System messages with multiple cache breakpoints for optimal caching:
|
// System messages with multiple cache breakpoints for optimal caching:
|
||||||
// - Breakpoint 1: Static instructions (~1500 tokens) - rarely changes
|
// - Breakpoint 1: Static instructions (~1500 tokens) - rarely changes
|
||||||
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
|
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
|
||||||
// This allows: if only user message changes, both system caches are reused
|
// This allows: if only user message changes, both system caches are reused
|
||||||
// if XML changes, instruction cache is still reused
|
// if XML changes, instruction cache is still reused
|
||||||
const systemMessages = [
|
const systemMessages = [
|
||||||
// Cache breakpoint 1: Instructions (rarely change)
|
// Cache breakpoint 1: Instructions (rarely change)
|
||||||
{
|
{
|
||||||
role: 'system' as const,
|
role: "system" as const,
|
||||||
content: systemMessage,
|
content: systemMessage,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
bedrock: { cachePoint: { type: 'default' } },
|
bedrock: { cachePoint: { type: "default" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Cache breakpoint 2: Current diagram XML context
|
// Cache breakpoint 2: Current diagram XML context
|
||||||
{
|
{
|
||||||
role: 'system' as const,
|
role: "system" as const,
|
||||||
content: `Current diagram XML:\n"""xml\n${xml || ''}\n"""\nWhen using edit_diagram, COPY search patterns exactly from this XML - attribute order matters!`,
|
content: `Current diagram XML:\n"""xml\n${xml || ""}\n"""\nWhen using edit_diagram, COPY search patterns exactly from this XML - attribute order matters!`,
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
bedrock: { cachePoint: { type: 'default' } },
|
bedrock: { cachePoint: { type: "default" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
const allMessages = [...systemMessages, ...enhancedMessages];
|
const allMessages = [...systemMessages, ...enhancedMessages]
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model,
|
model,
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
...(providerOptions && { providerOptions }),
|
...(providerOptions && { providerOptions }),
|
||||||
...(headers && { headers }),
|
...(headers && { headers }),
|
||||||
// Langfuse telemetry config (returns undefined if not configured)
|
// Langfuse telemetry config (returns undefined if not configured)
|
||||||
...(getTelemetryConfig({ sessionId: validSessionId, userId }) && {
|
...(getTelemetryConfig({ sessionId: validSessionId, userId }) && {
|
||||||
experimental_telemetry: getTelemetryConfig({ sessionId: validSessionId, userId }),
|
experimental_telemetry: getTelemetryConfig({
|
||||||
}),
|
sessionId: validSessionId,
|
||||||
onFinish: ({ text, usage, providerMetadata }) => {
|
userId,
|
||||||
console.log('[Cache] Full providerMetadata:', JSON.stringify(providerMetadata, null, 2));
|
}),
|
||||||
console.log('[Cache] Usage:', JSON.stringify(usage, null, 2));
|
}),
|
||||||
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
|
onFinish: ({ text, usage, providerMetadata }) => {
|
||||||
// AI SDK uses inputTokens/outputTokens, Langfuse expects promptTokens/completionTokens
|
console.log(
|
||||||
setTraceOutput(text, {
|
"[Cache] Full providerMetadata:",
|
||||||
promptTokens: usage?.inputTokens,
|
JSON.stringify(providerMetadata, null, 2),
|
||||||
completionTokens: usage?.outputTokens,
|
)
|
||||||
});
|
console.log("[Cache] Usage:", JSON.stringify(usage, null, 2))
|
||||||
},
|
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
|
||||||
tools: {
|
// AI SDK uses inputTokens/outputTokens, Langfuse expects promptTokens/completionTokens
|
||||||
// Client-side tool that will be executed on the client
|
setTraceOutput(text, {
|
||||||
display_diagram: {
|
promptTokens: usage?.inputTokens,
|
||||||
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
|
completionTokens: usage?.outputTokens,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
// Client-side tool that will be executed on the client
|
||||||
|
display_diagram: {
|
||||||
|
description: `Display a diagram on draw.io. Pass the XML content inside <root> tags.
|
||||||
|
|
||||||
VALIDATION RULES (XML will be rejected if violated):
|
VALIDATION RULES (XML will be rejected if violated):
|
||||||
1. All mxCell elements must be DIRECT children of <root> - never nested
|
1. All mxCell elements must be DIRECT children of <root> - never nested
|
||||||
@@ -276,12 +328,14 @@ Notes:
|
|||||||
- For AWS diagrams, use **AWS 2025 icons**.
|
- For AWS diagrams, use **AWS 2025 icons**.
|
||||||
- For animated connectors, add "flowAnimation=1" to edge style.
|
- For animated connectors, add "flowAnimation=1" to edge style.
|
||||||
`,
|
`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
xml: z.string().describe("XML string to be displayed on draw.io")
|
xml: z
|
||||||
})
|
.string()
|
||||||
},
|
.describe("XML string to be displayed on draw.io"),
|
||||||
edit_diagram: {
|
}),
|
||||||
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
},
|
||||||
|
edit_diagram: {
|
||||||
|
description: `Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
||||||
CRITICAL: Copy-paste the EXACT search pattern from the "Current diagram XML" in system context. Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly.
|
CRITICAL: Copy-paste the EXACT search pattern from the "Current diagram XML" in system context. Do NOT reorder attributes or reformat - the attribute order in draw.io XML varies and you MUST match it exactly.
|
||||||
IMPORTANT: Keep edits concise:
|
IMPORTANT: Keep edits concise:
|
||||||
- COPY the exact mxCell line from the current XML (attribute order matters!)
|
- COPY the exact mxCell line from the current XML (attribute order matters!)
|
||||||
@@ -289,33 +343,48 @@ IMPORTANT: Keep edits concise:
|
|||||||
- Break large changes into multiple smaller edits
|
- Break large changes into multiple smaller edits
|
||||||
- Each search must contain complete lines (never truncate mid-line)
|
- Each search must contain complete lines (never truncate mid-line)
|
||||||
- First match only - be specific enough to target the right element`,
|
- First match only - be specific enough to target the right element`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
edits: z.array(z.object({
|
edits: z
|
||||||
search: z.string().describe("EXACT lines copied from current XML (preserve attribute order!)"),
|
.array(
|
||||||
replace: z.string().describe("Replacement lines")
|
z.object({
|
||||||
})).describe("Array of search/replace pairs to apply sequentially")
|
search: z
|
||||||
})
|
.string()
|
||||||
},
|
.describe(
|
||||||
},
|
"EXACT lines copied from current XML (preserve attribute order!)",
|
||||||
temperature: 0,
|
),
|
||||||
});
|
replace: z
|
||||||
|
.string()
|
||||||
|
.describe("Replacement lines"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.describe(
|
||||||
|
"Array of search/replace pairs to apply sequentially",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
temperature: 0,
|
||||||
|
})
|
||||||
|
|
||||||
return result.toUIMessageStreamResponse();
|
return result.toUIMessageStreamResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap handler with error handling
|
// Wrap handler with error handling
|
||||||
async function safeHandler(req: Request): Promise<Response> {
|
async function safeHandler(req: Request): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
return await handleChatRequest(req);
|
return await handleChatRequest(req)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in chat route:', error);
|
console.error("Error in chat route:", error)
|
||||||
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
return Response.json(
|
||||||
}
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap with Langfuse observe (if configured)
|
// Wrap with Langfuse observe (if configured)
|
||||||
const observedHandler = wrapWithObserve(safeHandler);
|
const observedHandler = wrapWithObserve(safeHandler)
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
return observedHandler(req);
|
return observedHandler(req)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const accessCodes = process.env.ACCESS_CODE_LIST?.split(',').map(code => code.trim()).filter(Boolean) || [];
|
const accessCodes =
|
||||||
|
process.env.ACCESS_CODE_LIST?.split(",")
|
||||||
|
.map((code) => code.trim())
|
||||||
|
.filter(Boolean) || []
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
accessCodeRequired: accessCodes.length > 0,
|
accessCodeRequired: accessCodes.length > 0,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +1,112 @@
|
|||||||
import { getLangfuseClient } from '@/lib/langfuse';
|
import { randomUUID } from "crypto"
|
||||||
import { randomUUID } from 'crypto';
|
import { z } from "zod"
|
||||||
import { z } from 'zod';
|
import { getLangfuseClient } from "@/lib/langfuse"
|
||||||
|
|
||||||
const feedbackSchema = z.object({
|
const feedbackSchema = z.object({
|
||||||
messageId: z.string().min(1).max(200),
|
messageId: z.string().min(1).max(200),
|
||||||
feedback: z.enum(['good', 'bad']),
|
feedback: z.enum(["good", "bad"]),
|
||||||
sessionId: z.string().min(1).max(200).optional(),
|
sessionId: z.string().min(1).max(200).optional(),
|
||||||
});
|
})
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const langfuse = getLangfuseClient();
|
const langfuse = getLangfuseClient()
|
||||||
if (!langfuse) {
|
if (!langfuse) {
|
||||||
return Response.json({ success: true, logged: false });
|
return Response.json({ success: true, logged: false })
|
||||||
}
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = feedbackSchema.parse(await req.json());
|
|
||||||
} catch {
|
|
||||||
return Response.json({ success: false, error: 'Invalid input' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { messageId, feedback, sessionId } = data;
|
|
||||||
|
|
||||||
// Get user IP for tracking
|
|
||||||
const forwardedFor = req.headers.get('x-forwarded-for');
|
|
||||||
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the most recent chat trace for this session to attach the score to
|
|
||||||
const tracesResponse = await langfuse.api.trace.list({
|
|
||||||
sessionId,
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const traces = tracesResponse.data || [];
|
|
||||||
const latestTrace = traces[0];
|
|
||||||
|
|
||||||
if (!latestTrace) {
|
|
||||||
// No trace found for this session - create a standalone feedback trace
|
|
||||||
const traceId = randomUUID();
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
await langfuse.api.ingestion.batch({
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: 'trace-create',
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: traceId,
|
|
||||||
name: 'user-feedback',
|
|
||||||
sessionId,
|
|
||||||
userId,
|
|
||||||
input: { messageId, feedback },
|
|
||||||
metadata: { source: 'feedback-button', note: 'standalone - no chat trace found' },
|
|
||||||
timestamp,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'score-create',
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: randomUUID(),
|
|
||||||
traceId,
|
|
||||||
name: 'user-feedback',
|
|
||||||
value: feedback === 'good' ? 1 : 0,
|
|
||||||
comment: `User gave ${feedback} feedback`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Attach score to the existing chat trace
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
await langfuse.api.ingestion.batch({
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: 'score-create',
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: randomUUID(),
|
|
||||||
traceId: latestTrace.id,
|
|
||||||
name: 'user-feedback',
|
|
||||||
value: feedback === 'good' ? 1 : 0,
|
|
||||||
comment: `User gave ${feedback} feedback`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({ success: true, logged: true });
|
// Validate input
|
||||||
} catch (error) {
|
let data
|
||||||
console.error('Langfuse feedback error:', error);
|
try {
|
||||||
return Response.json({ success: false, error: 'Failed to log feedback' }, { status: 500 });
|
data = feedbackSchema.parse(await req.json())
|
||||||
}
|
} catch {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Invalid input" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageId, feedback, sessionId } = data
|
||||||
|
|
||||||
|
// Get user IP for tracking
|
||||||
|
const forwardedFor = req.headers.get("x-forwarded-for")
|
||||||
|
const userId = forwardedFor?.split(",")[0]?.trim() || "anonymous"
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the most recent chat trace for this session to attach the score to
|
||||||
|
const tracesResponse = await langfuse.api.trace.list({
|
||||||
|
sessionId,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const traces = tracesResponse.data || []
|
||||||
|
const latestTrace = traces[0]
|
||||||
|
|
||||||
|
if (!latestTrace) {
|
||||||
|
// No trace found for this session - create a standalone feedback trace
|
||||||
|
const traceId = randomUUID()
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: "trace-create",
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: traceId,
|
||||||
|
name: "user-feedback",
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
input: { messageId, feedback },
|
||||||
|
metadata: {
|
||||||
|
source: "feedback-button",
|
||||||
|
note: "standalone - no chat trace found",
|
||||||
|
},
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "score-create",
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: randomUUID(),
|
||||||
|
traceId,
|
||||||
|
name: "user-feedback",
|
||||||
|
value: feedback === "good" ? 1 : 0,
|
||||||
|
comment: `User gave ${feedback} feedback`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Attach score to the existing chat trace
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: "score-create",
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: randomUUID(),
|
||||||
|
traceId: latestTrace.id,
|
||||||
|
name: "user-feedback",
|
||||||
|
value: feedback === "good" ? 1 : 0,
|
||||||
|
comment: `User gave ${feedback} feedback`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true, logged: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Langfuse feedback error:", error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Failed to log feedback" },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,71 @@
|
|||||||
import { getLangfuseClient } from '@/lib/langfuse';
|
import { randomUUID } from "crypto"
|
||||||
import { randomUUID } from 'crypto';
|
import { z } from "zod"
|
||||||
import { z } from 'zod';
|
import { getLangfuseClient } from "@/lib/langfuse"
|
||||||
|
|
||||||
const saveSchema = z.object({
|
const saveSchema = z.object({
|
||||||
filename: z.string().min(1).max(255),
|
filename: z.string().min(1).max(255),
|
||||||
format: z.enum(['drawio', 'png', 'svg']),
|
format: z.enum(["drawio", "png", "svg"]),
|
||||||
sessionId: z.string().min(1).max(200).optional(),
|
sessionId: z.string().min(1).max(200).optional(),
|
||||||
});
|
})
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const langfuse = getLangfuseClient();
|
const langfuse = getLangfuseClient()
|
||||||
if (!langfuse) {
|
if (!langfuse) {
|
||||||
return Response.json({ success: true, logged: false });
|
return Response.json({ success: true, logged: false })
|
||||||
}
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = saveSchema.parse(await req.json());
|
|
||||||
} catch {
|
|
||||||
return Response.json({ success: false, error: 'Invalid input' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { filename, format, sessionId } = data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
// Find the most recent chat trace for this session to attach the save flag
|
|
||||||
const tracesResponse = await langfuse.api.trace.list({
|
|
||||||
sessionId,
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const traces = tracesResponse.data || [];
|
|
||||||
const latestTrace = traces[0];
|
|
||||||
|
|
||||||
if (latestTrace) {
|
|
||||||
// Add a score to the existing trace to flag that user saved
|
|
||||||
await langfuse.api.ingestion.batch({
|
|
||||||
batch: [
|
|
||||||
{
|
|
||||||
type: 'score-create',
|
|
||||||
id: randomUUID(),
|
|
||||||
timestamp,
|
|
||||||
body: {
|
|
||||||
id: randomUUID(),
|
|
||||||
traceId: latestTrace.id,
|
|
||||||
name: 'diagram-saved',
|
|
||||||
value: 1,
|
|
||||||
comment: `User saved diagram as ${filename}.${format}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// If no trace found, skip logging (user hasn't chatted yet)
|
|
||||||
|
|
||||||
return Response.json({ success: true, logged: !!latestTrace });
|
// Validate input
|
||||||
} catch (error) {
|
let data
|
||||||
console.error('Langfuse save error:', error);
|
try {
|
||||||
return Response.json({ success: false, error: 'Failed to log save' }, { status: 500 });
|
data = saveSchema.parse(await req.json())
|
||||||
}
|
} catch {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Invalid input" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filename, format, sessionId } = data
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
|
// Find the most recent chat trace for this session to attach the save flag
|
||||||
|
const tracesResponse = await langfuse.api.trace.list({
|
||||||
|
sessionId,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const traces = tracesResponse.data || []
|
||||||
|
const latestTrace = traces[0]
|
||||||
|
|
||||||
|
if (latestTrace) {
|
||||||
|
// Add a score to the existing trace to flag that user saved
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: "score-create",
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: randomUUID(),
|
||||||
|
traceId: latestTrace.id,
|
||||||
|
name: "diagram-saved",
|
||||||
|
value: 1,
|
||||||
|
comment: `User saved diagram as ${filename}.${format}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// If no trace found, skip logging (user hasn't chatted yet)
|
||||||
|
|
||||||
|
return Response.json({ success: true, logged: !!latestTrace })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Langfuse save error:", error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Failed to log save" },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
350
app/globals.css
350
app/globals.css
@@ -6,250 +6,254 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-mono: var(--font-mono);
|
--font-mono: var(--font-mono);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--color-sidebar: var(--sidebar);
|
--color-sidebar: var(--sidebar);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-2: var(--chart-2);
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
/* Clean Light Modern Palette */
|
/* Clean Light Modern Palette */
|
||||||
--background: oklch(0.985 0.002 240);
|
--background: oklch(0.985 0.002 240);
|
||||||
--foreground: oklch(0.23 0.02 260);
|
--foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.23 0.02 260);
|
--card-foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.23 0.02 260);
|
--popover-foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
/* Dark primary - slightly lighter */
|
/* Dark primary - slightly lighter */
|
||||||
--primary: oklch(0.35 0.01 260);
|
--primary: oklch(0.35 0.01 260);
|
||||||
--primary-foreground: oklch(0.99 0 0);
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
|
|
||||||
/* Warm gray secondary */
|
/* Warm gray secondary */
|
||||||
--secondary: oklch(0.96 0.005 260);
|
--secondary: oklch(0.96 0.005 260);
|
||||||
--secondary-foreground: oklch(0.35 0.02 260);
|
--secondary-foreground: oklch(0.35 0.02 260);
|
||||||
|
|
||||||
/* Light muted tones */
|
/* Light muted tones */
|
||||||
--muted: oklch(0.965 0.005 260);
|
--muted: oklch(0.965 0.005 260);
|
||||||
--muted-foreground: oklch(0.50 0.02 260);
|
--muted-foreground: oklch(0.5 0.02 260);
|
||||||
|
|
||||||
/* Soft lavender accent */
|
/* Soft lavender accent */
|
||||||
--accent: oklch(0.94 0.03 280);
|
--accent: oklch(0.94 0.03 280);
|
||||||
--accent-foreground: oklch(0.35 0.08 270);
|
--accent-foreground: oklch(0.35 0.08 270);
|
||||||
|
|
||||||
/* Coral destructive */
|
/* Coral destructive */
|
||||||
--destructive: oklch(0.60 0.20 25);
|
--destructive: oklch(0.6 0.2 25);
|
||||||
|
|
||||||
/* Subtle borders */
|
/* Subtle borders */
|
||||||
--border: oklch(0.92 0.01 260);
|
--border: oklch(0.92 0.01 260);
|
||||||
--input: oklch(0.94 0.01 260);
|
--input: oklch(0.94 0.01 260);
|
||||||
--ring: oklch(0.25 0.01 260);
|
--ring: oklch(0.25 0.01 260);
|
||||||
|
|
||||||
/* Chart colors - harmonious palette */
|
/* Chart colors - harmonious palette */
|
||||||
--chart-1: oklch(0.55 0.18 265);
|
--chart-1: oklch(0.55 0.18 265);
|
||||||
--chart-2: oklch(0.65 0.15 170);
|
--chart-2: oklch(0.65 0.15 170);
|
||||||
--chart-3: oklch(0.70 0.18 45);
|
--chart-3: oklch(0.7 0.18 45);
|
||||||
--chart-4: oklch(0.60 0.20 330);
|
--chart-4: oklch(0.6 0.2 330);
|
||||||
--chart-5: oklch(0.50 0.15 200);
|
--chart-5: oklch(0.5 0.15 200);
|
||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
--sidebar: oklch(0.99 0.002 260);
|
--sidebar: oklch(0.99 0.002 260);
|
||||||
--sidebar-foreground: oklch(0.23 0.02 260);
|
--sidebar-foreground: oklch(0.23 0.02 260);
|
||||||
--sidebar-primary: oklch(0.55 0.18 265);
|
--sidebar-primary: oklch(0.55 0.18 265);
|
||||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||||
--sidebar-accent: oklch(0.96 0.02 270);
|
--sidebar-accent: oklch(0.96 0.02 270);
|
||||||
--sidebar-accent-foreground: oklch(0.35 0.05 265);
|
--sidebar-accent-foreground: oklch(0.35 0.05 265);
|
||||||
--sidebar-border: oklch(0.93 0.01 260);
|
--sidebar-border: oklch(0.93 0.01 260);
|
||||||
--sidebar-ring: oklch(0.55 0.18 265);
|
--sidebar-ring: oklch(0.55 0.18 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.15 0.015 260);
|
--background: oklch(0.15 0.015 260);
|
||||||
--foreground: oklch(0.95 0.01 260);
|
--foreground: oklch(0.95 0.01 260);
|
||||||
|
|
||||||
--card: oklch(0.20 0.015 260);
|
--card: oklch(0.2 0.015 260);
|
||||||
--card-foreground: oklch(0.95 0.01 260);
|
--card-foreground: oklch(0.95 0.01 260);
|
||||||
|
|
||||||
--popover: oklch(0.20 0.015 260);
|
--popover: oklch(0.2 0.015 260);
|
||||||
--popover-foreground: oklch(0.95 0.01 260);
|
--popover-foreground: oklch(0.95 0.01 260);
|
||||||
|
|
||||||
--primary: oklch(0.70 0.16 265);
|
--primary: oklch(0.7 0.16 265);
|
||||||
--primary-foreground: oklch(0.15 0.02 260);
|
--primary-foreground: oklch(0.15 0.02 260);
|
||||||
|
|
||||||
--secondary: oklch(0.25 0.015 260);
|
--secondary: oklch(0.25 0.015 260);
|
||||||
--secondary-foreground: oklch(0.90 0.01 260);
|
--secondary-foreground: oklch(0.9 0.01 260);
|
||||||
|
|
||||||
--muted: oklch(0.25 0.015 260);
|
--muted: oklch(0.25 0.015 260);
|
||||||
--muted-foreground: oklch(0.65 0.02 260);
|
--muted-foreground: oklch(0.65 0.02 260);
|
||||||
|
|
||||||
--accent: oklch(0.30 0.04 280);
|
--accent: oklch(0.3 0.04 280);
|
||||||
--accent-foreground: oklch(0.90 0.03 270);
|
--accent-foreground: oklch(0.9 0.03 270);
|
||||||
|
|
||||||
--destructive: oklch(0.65 0.22 25);
|
--destructive: oklch(0.65 0.22 25);
|
||||||
|
|
||||||
--border: oklch(0.28 0.015 260);
|
--border: oklch(0.28 0.015 260);
|
||||||
--input: oklch(0.25 0.015 260);
|
--input: oklch(0.25 0.015 260);
|
||||||
--ring: oklch(0.70 0.16 265);
|
--ring: oklch(0.7 0.16 265);
|
||||||
|
|
||||||
--chart-1: oklch(0.70 0.16 265);
|
--chart-1: oklch(0.7 0.16 265);
|
||||||
--chart-2: oklch(0.70 0.13 170);
|
--chart-2: oklch(0.7 0.13 170);
|
||||||
--chart-3: oklch(0.75 0.16 45);
|
--chart-3: oklch(0.75 0.16 45);
|
||||||
--chart-4: oklch(0.70 0.18 330);
|
--chart-4: oklch(0.7 0.18 330);
|
||||||
--chart-5: oklch(0.60 0.13 200);
|
--chart-5: oklch(0.6 0.13 200);
|
||||||
|
|
||||||
--sidebar: oklch(0.18 0.015 260);
|
--sidebar: oklch(0.18 0.015 260);
|
||||||
--sidebar-foreground: oklch(0.95 0.01 260);
|
--sidebar-foreground: oklch(0.95 0.01 260);
|
||||||
--sidebar-primary: oklch(0.70 0.16 265);
|
--sidebar-primary: oklch(0.7 0.16 265);
|
||||||
--sidebar-primary-foreground: oklch(0.15 0.02 260);
|
--sidebar-primary-foreground: oklch(0.15 0.02 260);
|
||||||
--sidebar-accent: oklch(0.25 0.03 270);
|
--sidebar-accent: oklch(0.25 0.03 270);
|
||||||
--sidebar-accent-foreground: oklch(0.90 0.02 265);
|
--sidebar-accent-foreground: oklch(0.9 0.02 265);
|
||||||
--sidebar-border: oklch(0.28 0.015 260);
|
--sidebar-border: oklch(0.28 0.015 260);
|
||||||
--sidebar-ring: oklch(0.70 0.16 265);
|
--sidebar-ring: oklch(0.7 0.16 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground font-sans;
|
@apply bg-background text-foreground font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for Radix ScrollArea viewport horizontal overflow */
|
/* Fix for Radix ScrollArea viewport horizontal overflow */
|
||||||
[data-slot="scroll-area-viewport"] > div {
|
[data-slot="scroll-area-viewport"] > div {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.scrollbar-thin {
|
.scrollbar-thin {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: oklch(0.85 0.01 260) transparent;
|
scrollbar-color: oklch(0.85 0.01 260) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar {
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-track {
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
background-color: oklch(0.85 0.01 260);
|
background-color: oklch(0.85 0.01 260);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: oklch(0.75 0.01 260);
|
background-color: oklch(0.75 0.01 260);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth page transitions */
|
/* Smooth page transitions */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(8px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.3s ease-out forwards;
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-in-right {
|
.animate-slide-in-right {
|
||||||
animation: slideInRight 0.3s ease-out forwards;
|
animation: slideInRight 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message bubble animations */
|
/* Message bubble animations */
|
||||||
@keyframes messageIn {
|
@keyframes messageIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(12px) scale(0.98);
|
transform: translateY(12px) scale(0.98);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-message-in {
|
.animate-message-in {
|
||||||
animation: messageIn 0.25s ease-out forwards;
|
animation: messageIn 0.25s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle floating shadow for cards */
|
/* Subtle floating shadow for cards */
|
||||||
.shadow-soft {
|
.shadow-soft {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 2px oklch(0.23 0.02 260 / 0.04),
|
0 1px 2px oklch(0.23 0.02 260 / 0.04),
|
||||||
0 4px 12px oklch(0.23 0.02 260 / 0.06),
|
0 4px 12px oklch(0.23 0.02 260 / 0.06),
|
||||||
0 8px 24px oklch(0.23 0.02 260 / 0.04);
|
0 8px 24px oklch(0.23 0.02 260 / 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-soft-lg {
|
.shadow-soft-lg {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 4px oklch(0.23 0.02 260 / 0.04),
|
0 2px 4px oklch(0.23 0.02 260 / 0.04),
|
||||||
0 8px 20px oklch(0.23 0.02 260 / 0.08),
|
0 8px 20px oklch(0.23 0.02 260 / 0.08),
|
||||||
0 16px 40px oklch(0.23 0.02 260 / 0.06);
|
0 16px 40px oklch(0.23 0.02 260 / 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient text utility */
|
/* Gradient text utility */
|
||||||
.text-gradient-primary {
|
.text-gradient-primary {
|
||||||
background: linear-gradient(135deg, oklch(0.55 0.18 265), oklch(0.60 0.20 290));
|
background: linear-gradient(
|
||||||
-webkit-background-clip: text;
|
135deg,
|
||||||
-webkit-text-fill-color: transparent;
|
oklch(0.55 0.18 265),
|
||||||
background-clip: text;
|
oklch(0.6 0.2 290)
|
||||||
|
);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,53 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import { GoogleAnalytics } from "@next/third-parties/google"
|
||||||
import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
|
import { Analytics } from "@vercel/analytics/react"
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
import type { Metadata, Viewport } from "next"
|
||||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
||||||
import { DiagramProvider } from "@/contexts/diagram-context";
|
import { DiagramProvider } from "@/contexts/diagram-context"
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css"
|
||||||
|
|
||||||
const plusJakarta = Plus_Jakarta_Sans({
|
const plusJakarta = Plus_Jakarta_Sans({
|
||||||
variable: "--font-sans",
|
variable: "--font-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500", "600", "700"],
|
weight: ["400", "500", "600", "700"],
|
||||||
});
|
})
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
variable: "--font-mono",
|
variable: "--font-mono",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500"],
|
weight: ["400", "500"],
|
||||||
});
|
})
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: "device-width",
|
width: "device-width",
|
||||||
initialScale: 1,
|
initialScale: 1,
|
||||||
maximumScale: 1,
|
maximumScale: 1,
|
||||||
userScalable: false,
|
userScalable: false,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Next AI Draw.io - AI-Powered Diagram Generator",
|
title: "Next AI Draw.io - AI-Powered Diagram Generator",
|
||||||
description: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
description:
|
||||||
keywords: ["AI diagram generator", "AWS architecture", "flowchart creator", "draw.io", "AI drawing tool", "technical diagrams", "diagram automation", "free diagram generator", "online diagram maker"],
|
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
||||||
|
keywords: [
|
||||||
|
"AI diagram generator",
|
||||||
|
"AWS architecture",
|
||||||
|
"flowchart creator",
|
||||||
|
"draw.io",
|
||||||
|
"AI drawing tool",
|
||||||
|
"technical diagrams",
|
||||||
|
"diagram automation",
|
||||||
|
"free diagram generator",
|
||||||
|
"online diagram maker",
|
||||||
|
],
|
||||||
authors: [{ name: "Next AI Draw.io" }],
|
authors: [{ name: "Next AI Draw.io" }],
|
||||||
creator: "Next AI Draw.io",
|
creator: "Next AI Draw.io",
|
||||||
publisher: "Next AI Draw.io",
|
publisher: "Next AI Draw.io",
|
||||||
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
title: "Next AI Draw.io - AI Diagram Generator",
|
||||||
description: "Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
|
description:
|
||||||
|
"Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
|
||||||
type: "website",
|
type: "website",
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
siteName: "Next AI Draw.io",
|
siteName: "Next AI Draw.io",
|
||||||
@@ -52,7 +64,8 @@ export const metadata: Metadata = {
|
|||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
title: "Next AI Draw.io - AI Diagram Generator",
|
||||||
description: "Create professional diagrams with AI assistance. Free, no login required.",
|
description:
|
||||||
|
"Create professional diagrams with AI assistance. Free, no login required.",
|
||||||
images: ["/architecture.png"],
|
images: ["/architecture.png"],
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
@@ -69,27 +82,28 @@ export const metadata: Metadata = {
|
|||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon.ico",
|
icon: "/favicon.ico",
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
'@context': 'https://schema.org',
|
"@context": "https://schema.org",
|
||||||
'@type': 'SoftwareApplication',
|
"@type": "SoftwareApplication",
|
||||||
name: 'Next AI Draw.io',
|
name: "Next AI Draw.io",
|
||||||
applicationCategory: 'DesignApplication',
|
applicationCategory: "DesignApplication",
|
||||||
operatingSystem: 'Web Browser',
|
operatingSystem: "Web Browser",
|
||||||
description: 'AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.',
|
description:
|
||||||
url: 'https://next-ai-drawio.jiang.jp',
|
"AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.",
|
||||||
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
offers: {
|
offers: {
|
||||||
'@type': 'Offer',
|
"@type": "Offer",
|
||||||
price: '0',
|
price: "0",
|
||||||
priceCurrency: 'USD',
|
priceCurrency: "USD",
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -109,5 +123,5 @@ export default function RootLayout({
|
|||||||
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
||||||
)}
|
)}
|
||||||
</html>
|
</html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
97
app/page.tsx
97
app/page.tsx
@@ -1,75 +1,75 @@
|
|||||||
"use client";
|
"use client"
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { DrawIoEmbed } from "react-drawio";
|
import { DrawIoEmbed } from "react-drawio"
|
||||||
import ChatPanel from "@/components/chat-panel";
|
import type { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import ChatPanel from "@/components/chat-panel"
|
||||||
import {
|
import {
|
||||||
ResizablePanelGroup,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
} from "@/components/ui/resizable";
|
ResizablePanel,
|
||||||
import type { ImperativePanelHandle } from "react-resizable-panels";
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable"
|
||||||
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport } = useDiagram();
|
const { drawioRef, handleDiagramExport } = useDiagram()
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true);
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">(() => {
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const saved = localStorage.getItem("drawio-theme");
|
const saved = localStorage.getItem("drawio-theme")
|
||||||
if (saved === "min" || saved === "sketch") return saved;
|
if (saved === "min" || saved === "sketch") return saved
|
||||||
}
|
}
|
||||||
return "min";
|
return "min"
|
||||||
});
|
})
|
||||||
const chatPanelRef = useRef<ImperativePanelHandle>(null);
|
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
setIsMobile(window.innerWidth < 768);
|
setIsMobile(window.innerWidth < 768)
|
||||||
};
|
}
|
||||||
|
|
||||||
checkMobile();
|
checkMobile()
|
||||||
window.addEventListener("resize", checkMobile);
|
window.addEventListener("resize", checkMobile)
|
||||||
return () => window.removeEventListener("resize", checkMobile);
|
return () => window.removeEventListener("resize", checkMobile)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const toggleChatPanel = () => {
|
const toggleChatPanel = () => {
|
||||||
const panel = chatPanelRef.current;
|
const panel = chatPanelRef.current
|
||||||
if (panel) {
|
if (panel) {
|
||||||
if (panel.isCollapsed()) {
|
if (panel.isCollapsed()) {
|
||||||
panel.expand();
|
panel.expand()
|
||||||
setIsChatVisible(true);
|
setIsChatVisible(true)
|
||||||
} else {
|
} else {
|
||||||
panel.collapse();
|
panel.collapse()
|
||||||
setIsChatVisible(false);
|
setIsChatVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
toggleChatPanel();
|
toggleChatPanel()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Show confirmation dialog when user tries to leave the page
|
// Show confirmation dialog when user tries to leave the page
|
||||||
// This helps prevent accidental navigation from browser back gestures
|
// This helps prevent accidental navigation from browser back gestures
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
return "";
|
return ""
|
||||||
};
|
}
|
||||||
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
window.addEventListener("beforeunload", handleBeforeUnload)
|
||||||
return () =>
|
return () =>
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-background relative overflow-hidden">
|
<div className="h-screen bg-background relative overflow-hidden">
|
||||||
@@ -80,7 +80,11 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
{/* Draw.io Canvas */}
|
{/* Draw.io Canvas */}
|
||||||
<ResizablePanel defaultSize={isMobile ? 50 : 67} minSize={20}>
|
<ResizablePanel defaultSize={isMobile ? 50 : 67} minSize={20}>
|
||||||
<div className={`h-full relative ${isMobile ? "p-1" : "p-2"}`}>
|
<div
|
||||||
|
className={`h-full relative ${
|
||||||
|
isMobile ? "p-1" : "p-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
||||||
<DrawIoEmbed
|
<DrawIoEmbed
|
||||||
key={drawioUi}
|
key={drawioUi}
|
||||||
@@ -117,9 +121,10 @@ export default function Home() {
|
|||||||
onToggleVisibility={toggleChatPanel}
|
onToggleVisibility={toggleChatPanel}
|
||||||
drawioUi={drawioUi}
|
drawioUi={drawioUi}
|
||||||
onToggleDrawioUi={() => {
|
onToggleDrawioUi={() => {
|
||||||
const newTheme = drawioUi === "min" ? "sketch" : "min";
|
const newTheme =
|
||||||
localStorage.setItem("drawio-theme", newTheme);
|
drawioUi === "min" ? "sketch" : "min"
|
||||||
setDrawioUi(newTheme);
|
localStorage.setItem("drawio-theme", newTheme)
|
||||||
|
setDrawioUi(newTheme)
|
||||||
}}
|
}}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
@@ -127,5 +132,5 @@ export default function Home() {
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { MetadataRoute } from 'next'
|
import type { MetadataRoute } from "next"
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
userAgent: '*',
|
userAgent: "*",
|
||||||
allow: '/',
|
allow: "/",
|
||||||
disallow: '/api/',
|
disallow: "/api/",
|
||||||
},
|
},
|
||||||
sitemap: 'https://next-ai-drawio.jiang.jp/sitemap.xml',
|
sitemap: "https://next-ai-drawio.jiang.jp/sitemap.xml",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { MetadataRoute } from 'next'
|
import type { MetadataRoute } from "next"
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: 'https://next-ai-drawio.jiang.jp',
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'weekly',
|
changeFrequency: "weekly",
|
||||||
priority: 1,
|
priority: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: 'https://next-ai-drawio.jiang.jp/about',
|
url: "https://next-ai-drawio.jiang.jp/about",
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'monthly',
|
changeFrequency: "monthly",
|
||||||
priority: 0.8,
|
priority: 0.8,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
83
biome.json
Normal file
83
biome.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 4
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"complexity": {
|
||||||
|
"noImportantStyles": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "off",
|
||||||
|
"noArrayIndexKey": "off",
|
||||||
|
"noImplicitAnyLet": "off",
|
||||||
|
"noAssignInExpressions": "off"
|
||||||
|
},
|
||||||
|
"a11y": {
|
||||||
|
"useButtonType": "off",
|
||||||
|
"noAutofocus": "off",
|
||||||
|
"noStaticElementInteractions": "off",
|
||||||
|
"useKeyWithClickEvents": "off",
|
||||||
|
"noLabelWithoutControl": "off",
|
||||||
|
"noNoninteractiveTabindex": "off"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"useExhaustiveDependencies": "off"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useNodejsImportProtocol": "off",
|
||||||
|
"useTemplate": "off"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtml": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double",
|
||||||
|
"semicolons": "asNeeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"cssModules": true,
|
||||||
|
"tailwindDirectives": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"includes": ["components/ui/**"],
|
||||||
|
"formatter": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "app/globals.css",
|
"css": "app/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import React from "react";
|
import type { VariantProps } from "class-variance-authority"
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import type React from "react"
|
||||||
|
import { Button, type buttonVariants } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip"
|
||||||
import { type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
interface ButtonWithTooltipProps
|
interface ButtonWithTooltipProps
|
||||||
extends React.ComponentProps<"button">,
|
extends React.ComponentProps<"button">,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
tooltipContent: string;
|
tooltipContent: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
asChild?: boolean;
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonWithTooltip({
|
export function ButtonWithTooltip({
|
||||||
@@ -27,8 +27,10 @@ export function ButtonWithTooltip({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button {...buttonProps}>{children}</Button>
|
<Button {...buttonProps}>{children}</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs text-wrap">{tooltipContent}</TooltipContent>
|
<TooltipContent className="max-w-xs text-wrap">
|
||||||
|
{tooltipContent}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Zap, Cloud, GitBranch, Palette } from "lucide-react";
|
import { Cloud, GitBranch, Palette, Zap } from "lucide-react"
|
||||||
|
|
||||||
interface ExampleCardProps {
|
interface ExampleCardProps {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode
|
||||||
title: string;
|
title: string
|
||||||
description: string;
|
description: string
|
||||||
onClick: () => void;
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) {
|
function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) {
|
||||||
@@ -29,43 +29,43 @@ function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExamplePanel({
|
export default function ExamplePanel({
|
||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
}: {
|
}: {
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void
|
||||||
}) {
|
}) {
|
||||||
const handleReplicateFlowchart = async () => {
|
const handleReplicateFlowchart = async () => {
|
||||||
setInput("Replicate this flowchart.");
|
setInput("Replicate this flowchart.")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/example.png");
|
const response = await fetch("/example.png")
|
||||||
const blob = await response.blob();
|
const blob = await response.blob()
|
||||||
const file = new File([blob], "example.png", { type: "image/png" });
|
const file = new File([blob], "example.png", { type: "image/png" })
|
||||||
setFiles([file]);
|
setFiles([file])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading example image:", error);
|
console.error("Error loading example image:", error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleReplicateArchitecture = async () => {
|
const handleReplicateArchitecture = async () => {
|
||||||
setInput("Replicate this in aws style");
|
setInput("Replicate this in aws style")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/architecture.png");
|
const response = await fetch("/architecture.png")
|
||||||
const blob = await response.blob();
|
const blob = await response.blob()
|
||||||
const file = new File([blob], "architecture.png", {
|
const file = new File([blob], "architecture.png", {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
})
|
||||||
setFiles([file]);
|
setFiles([file])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading architecture image:", error);
|
console.error("Error loading architecture image:", error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 px-2 animate-fade-in">
|
<div className="py-6 px-2 animate-fade-in">
|
||||||
@@ -75,7 +75,8 @@ export default function ExamplePanel({
|
|||||||
Create diagrams with AI
|
Create diagrams with AI
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
||||||
Describe what you want to create or upload an image to replicate
|
Describe what you want to create or upload an image to
|
||||||
|
replicate
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,7 +92,9 @@ export default function ExamplePanel({
|
|||||||
title="Animated Diagram"
|
title="Animated Diagram"
|
||||||
description="Draw a transformer architecture with animated connectors"
|
description="Draw a transformer architecture with animated connectors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInput("Give me a **animated connector** diagram of transformer's architecture")
|
setInput(
|
||||||
|
"Give me a **animated connector** diagram of transformer's architecture",
|
||||||
|
)
|
||||||
setFiles([])
|
setFiles([])
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -126,5 +129,5 @@ export default function ExamplePanel({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useCallback, useRef, useEffect, useState } from "react";
|
import {
|
||||||
import { Button } from "@/components/ui/button";
|
Download,
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
History,
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal";
|
Image as ImageIcon,
|
||||||
import { SaveDialog } from "@/components/save-dialog";
|
LayoutGrid,
|
||||||
|
Loader2,
|
||||||
|
PenTool,
|
||||||
|
Send,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react"
|
||||||
|
import type React from "react"
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
|
import { ErrorToast } from "@/components/error-toast"
|
||||||
|
import { HistoryDialog } from "@/components/history-dialog"
|
||||||
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
|
import { SaveDialog } from "@/components/save-dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,103 +26,105 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog"
|
||||||
import {
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
Loader2,
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
Send,
|
import { FilePreviewList } from "./file-preview-list"
|
||||||
Trash2,
|
|
||||||
Image as ImageIcon,
|
|
||||||
History,
|
|
||||||
Download,
|
|
||||||
PenTool,
|
|
||||||
LayoutGrid,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
|
||||||
import { FilePreviewList } from "./file-preview-list";
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
|
||||||
import { HistoryDialog } from "@/components/history-dialog";
|
|
||||||
import { ErrorToast } from "@/components/error-toast";
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||||
const MAX_FILES = 5;
|
const MAX_FILES = 5
|
||||||
|
|
||||||
function formatFileSize(bytes: number): string {
|
function formatFileSize(bytes: number): string {
|
||||||
const mb = bytes / 1024 / 1024;
|
const mb = bytes / 1024 / 1024
|
||||||
if (mb < 0.01) return `${(bytes / 1024).toFixed(0)}KB`;
|
if (mb < 0.01) return `${(bytes / 1024).toFixed(0)}KB`
|
||||||
return `${mb.toFixed(2)}MB`;
|
return `${mb.toFixed(2)}MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
function showErrorToast(message: React.ReactNode) {
|
function showErrorToast(message: React.ReactNode) {
|
||||||
toast.custom(
|
toast.custom(
|
||||||
(t) => <ErrorToast message={message} onDismiss={() => toast.dismiss(t)} />,
|
(t) => (
|
||||||
{ duration: 5000 }
|
<ErrorToast message={message} onDismiss={() => toast.dismiss(t)} />
|
||||||
);
|
),
|
||||||
|
{ duration: 5000 },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidationResult {
|
interface ValidationResult {
|
||||||
validFiles: File[];
|
validFiles: File[]
|
||||||
errors: string[];
|
errors: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFiles(newFiles: File[], existingCount: number): ValidationResult {
|
function validateFiles(
|
||||||
const errors: string[] = [];
|
newFiles: File[],
|
||||||
const validFiles: File[] = [];
|
existingCount: number,
|
||||||
|
): ValidationResult {
|
||||||
|
const errors: string[] = []
|
||||||
|
const validFiles: File[] = []
|
||||||
|
|
||||||
const availableSlots = MAX_FILES - existingCount;
|
const availableSlots = MAX_FILES - existingCount
|
||||||
|
|
||||||
if (availableSlots <= 0) {
|
if (availableSlots <= 0) {
|
||||||
errors.push(`Maximum ${MAX_FILES} files allowed`);
|
errors.push(`Maximum ${MAX_FILES} files allowed`)
|
||||||
return { validFiles, errors };
|
return { validFiles, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of newFiles) {
|
for (const file of newFiles) {
|
||||||
if (validFiles.length >= availableSlots) {
|
if (validFiles.length >= availableSlots) {
|
||||||
errors.push(`Only ${availableSlots} more file(s) allowed`);
|
errors.push(`Only ${availableSlots} more file(s) allowed`)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
errors.push(`"${file.name}" is ${formatFileSize(file.size)} (exceeds 2MB)`);
|
errors.push(
|
||||||
|
`"${file.name}" is ${formatFileSize(file.size)} (exceeds 2MB)`,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
validFiles.push(file);
|
validFiles.push(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { validFiles, errors };
|
return { validFiles, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
function showValidationErrors(errors: string[]) {
|
function showValidationErrors(errors: string[]) {
|
||||||
if (errors.length === 0) return;
|
if (errors.length === 0) return
|
||||||
|
|
||||||
if (errors.length === 1) {
|
if (errors.length === 1) {
|
||||||
showErrorToast(<span className="text-muted-foreground">{errors[0]}</span>);
|
showErrorToast(
|
||||||
|
<span className="text-muted-foreground">{errors[0]}</span>,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">{errors.length} files rejected:</span>
|
<span className="font-medium">
|
||||||
|
{errors.length} files rejected:
|
||||||
|
</span>
|
||||||
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
||||||
{errors.slice(0, 3).map((err, i) => <li key={i}>{err}</li>)}
|
{errors.slice(0, 3).map((err) => (
|
||||||
{errors.length > 3 && <li>...and {errors.length - 3} more</li>}
|
<li key={err}>{err}</li>
|
||||||
|
))}
|
||||||
|
{errors.length > 3 && (
|
||||||
|
<li>...and {errors.length - 3} more</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
input: string;
|
input: string
|
||||||
status: "submitted" | "streaming" | "ready" | "error";
|
status: "submitted" | "streaming" | "ready" | "error"
|
||||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
||||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||||
onClearChat: () => void;
|
onClearChat: () => void
|
||||||
files?: File[];
|
files?: File[]
|
||||||
onFileChange?: (files: File[]) => void;
|
onFileChange?: (files: File[]) => void
|
||||||
showHistory?: boolean;
|
showHistory?: boolean
|
||||||
onToggleHistory?: (show: boolean) => void;
|
onToggleHistory?: (show: boolean) => void
|
||||||
sessionId?: string;
|
sessionId?: string
|
||||||
error?: Error | null;
|
error?: Error | null
|
||||||
drawioUi?: "min" | "sketch";
|
drawioUi?: "min" | "sketch"
|
||||||
onToggleDrawioUi?: () => void;
|
onToggleDrawioUi?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -126,128 +142,139 @@ export function ChatInput({
|
|||||||
drawioUi = "min",
|
drawioUi = "min",
|
||||||
onToggleDrawioUi = () => {},
|
onToggleDrawioUi = () => {},
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram();
|
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [showClearDialog, setShowClearDialog] = useState(false);
|
const [showClearDialog, setShowClearDialog] = useState(false)
|
||||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
const [showThemeWarning, setShowThemeWarning] = useState(false);
|
const [showThemeWarning, setShowThemeWarning] = useState(false)
|
||||||
|
|
||||||
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
(status === "streaming" || status === "submitted") && !error;
|
(status === "streaming" || status === "submitted") && !error
|
||||||
|
|
||||||
const adjustTextareaHeight = useCallback(() => {
|
const adjustTextareaHeight = useCallback(() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.style.height = "auto";
|
textarea.style.height = "auto"
|
||||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
|
// Handle programmatic input changes (e.g., setInput("") after form submission)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adjustTextareaHeight();
|
adjustTextareaHeight()
|
||||||
}, [input, adjustTextareaHeight]);
|
}, [input, adjustTextareaHeight])
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
onChange(e)
|
||||||
|
adjustTextareaHeight()
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
const form = e.currentTarget.closest("form");
|
const form = e.currentTarget.closest("form")
|
||||||
if (form && input.trim() && !isDisabled) {
|
if (form && input.trim() && !isDisabled) {
|
||||||
form.requestSubmit();
|
form.requestSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
if (isDisabled) return;
|
if (isDisabled) return
|
||||||
|
|
||||||
const items = e.clipboardData.items;
|
const items = e.clipboardData.items
|
||||||
const imageItems = Array.from(items).filter((item) =>
|
const imageItems = Array.from(items).filter((item) =>
|
||||||
item.type.startsWith("image/")
|
item.type.startsWith("image/"),
|
||||||
);
|
)
|
||||||
|
|
||||||
if (imageItems.length > 0) {
|
if (imageItems.length > 0) {
|
||||||
const imageFiles = (await Promise.all(
|
const imageFiles = (
|
||||||
imageItems.map(async (item, index) => {
|
await Promise.all(
|
||||||
const file = item.getAsFile();
|
imageItems.map(async (item, index) => {
|
||||||
if (!file) return null;
|
const file = item.getAsFile()
|
||||||
return new File(
|
if (!file) return null
|
||||||
[file],
|
return new File(
|
||||||
`pasted-image-${Date.now()}-${index}.${file.type.split("/")[1]}`,
|
[file],
|
||||||
{ type: file.type }
|
`pasted-image-${Date.now()}-${index}.${file.type.split("/")[1]}`,
|
||||||
);
|
{ type: file.type },
|
||||||
})
|
)
|
||||||
)).filter((f): f is File => f !== null);
|
}),
|
||||||
|
)
|
||||||
|
).filter((f): f is File => f !== null)
|
||||||
|
|
||||||
const { validFiles, errors } = validateFiles(imageFiles, files.length);
|
const { validFiles, errors } = validateFiles(
|
||||||
showValidationErrors(errors);
|
imageFiles,
|
||||||
|
files.length,
|
||||||
|
)
|
||||||
|
showValidationErrors(errors)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles]);
|
onFileChange([...files, ...validFiles])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newFiles = Array.from(e.target.files || []);
|
const newFiles = Array.from(e.target.files || [])
|
||||||
const { validFiles, errors } = validateFiles(newFiles, files.length);
|
const { validFiles, errors } = validateFiles(newFiles, files.length)
|
||||||
showValidationErrors(errors);
|
showValidationErrors(errors)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles]);
|
onFileChange([...files, ...validFiles])
|
||||||
}
|
}
|
||||||
// Reset input so same file can be selected again
|
// Reset input so same file can be selected again
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = "";
|
fileInputRef.current.value = ""
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleRemoveFile = (fileToRemove: File) => {
|
const handleRemoveFile = (fileToRemove: File) => {
|
||||||
onFileChange(files.filter((file) => file !== fileToRemove));
|
onFileChange(files.filter((file) => file !== fileToRemove))
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = "";
|
fileInputRef.current.value = ""
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click()
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
setIsDragging(true);
|
setIsDragging(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
setIsDragging(false);
|
setIsDragging(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
setIsDragging(false);
|
setIsDragging(false)
|
||||||
|
|
||||||
if (isDisabled) return;
|
if (isDisabled) return
|
||||||
|
|
||||||
const droppedFiles = e.dataTransfer.files;
|
const droppedFiles = e.dataTransfer.files
|
||||||
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
||||||
file.type.startsWith("image/")
|
file.type.startsWith("image/"),
|
||||||
);
|
)
|
||||||
|
|
||||||
const { validFiles, errors } = validateFiles(imageFiles, files.length);
|
const { validFiles, errors } = validateFiles(imageFiles, files.length)
|
||||||
showValidationErrors(errors);
|
showValidationErrors(errors)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles]);
|
onFileChange([...files, ...validFiles])
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onClearChat();
|
onClearChat()
|
||||||
setShowClearDialog(false);
|
setShowClearDialog(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -276,7 +303,7 @@ export function ChatInput({
|
|||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={onChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder="Describe your diagram or paste an image..."
|
placeholder="Describe your diagram or paste an image..."
|
||||||
@@ -316,7 +343,11 @@ export function ChatInput({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowThemeWarning(true)}
|
onClick={() => setShowThemeWarning(true)}
|
||||||
tooltipContent={drawioUi === "min" ? "Switch to Sketch theme" : "Switch to Minimal theme"}
|
tooltipContent={
|
||||||
|
drawioUi === "min"
|
||||||
|
? "Switch to Sketch theme"
|
||||||
|
: "Switch to Minimal theme"
|
||||||
|
}
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
{drawioUi === "min" ? (
|
{drawioUi === "min" ? (
|
||||||
@@ -326,27 +357,33 @@ export function ChatInput({
|
|||||||
)}
|
)}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
<Dialog open={showThemeWarning} onOpenChange={setShowThemeWarning}>
|
<Dialog
|
||||||
|
open={showThemeWarning}
|
||||||
|
onOpenChange={setShowThemeWarning}
|
||||||
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Switch Theme?</DialogTitle>
|
<DialogTitle>Switch Theme?</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Switching themes will reload the diagram editor and clear any unsaved changes.
|
Switching themes will reload the diagram
|
||||||
|
editor and clear any unsaved changes.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowThemeWarning(false)}
|
onClick={() =>
|
||||||
|
setShowThemeWarning(false)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClearChat();
|
onClearChat()
|
||||||
onToggleDrawioUi();
|
onToggleDrawioUi()
|
||||||
setShowThemeWarning(false);
|
setShowThemeWarning(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Switch Theme
|
Switch Theme
|
||||||
@@ -439,5 +476,5 @@ export function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,55 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useRef, useEffect, useState, useCallback } from "react";
|
import type { UIMessage } from "ai"
|
||||||
import Image from "next/image";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
import {
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
Check,
|
||||||
import ExamplePanel from "./chat-example-panel";
|
ChevronDown,
|
||||||
import { UIMessage } from "ai";
|
ChevronUp,
|
||||||
import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils";
|
Copy,
|
||||||
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, ThumbsUp, ThumbsDown, RotateCcw, Pencil } from "lucide-react";
|
Cpu,
|
||||||
import { CodeBlock } from "./code-block";
|
Minus,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
RotateCcw,
|
||||||
|
ThumbsDown,
|
||||||
|
ThumbsUp,
|
||||||
|
X,
|
||||||
|
} from "lucide-react"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import ReactMarkdown from "react-markdown"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import {
|
||||||
|
convertToLegalXml,
|
||||||
|
replaceNodes,
|
||||||
|
validateMxCellStructure,
|
||||||
|
} from "@/lib/utils"
|
||||||
|
import ExamplePanel from "./chat-example-panel"
|
||||||
|
import { CodeBlock } from "./code-block"
|
||||||
|
|
||||||
interface EditPair {
|
interface EditPair {
|
||||||
search: string;
|
search: string
|
||||||
replace: string;
|
replace: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool part interface for type safety
|
||||||
|
interface ToolPartLike {
|
||||||
|
type: string
|
||||||
|
toolCallId: string
|
||||||
|
state?: string
|
||||||
|
input?: { xml?: string; edits?: EditPair[] } & Record<string, unknown>
|
||||||
|
output?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{edits.map((edit, index) => (
|
{edits.map((edit, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/50 overflow-hidden bg-background/50">
|
<div
|
||||||
|
key={`${edit.search.slice(0, 50)}-${edit.replace.slice(0, 50)}-${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">
|
<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">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
Change {index + 1}
|
Change {index + 1}
|
||||||
@@ -30,7 +60,9 @@ function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
|||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Minus className="w-3 h-3 text-red-500" />
|
<Minus className="w-3 h-3 text-red-500" />
|
||||||
<span className="text-[10px] font-medium text-red-600 uppercase tracking-wide">Remove</span>
|
<span className="text-[10px] font-medium text-red-600 uppercase tracking-wide">
|
||||||
|
Remove
|
||||||
|
</span>
|
||||||
</div>
|
</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">
|
<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}
|
{edit.search}
|
||||||
@@ -40,7 +72,9 @@ function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
|||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Plus className="w-3 h-3 text-green-500" />
|
<Plus className="w-3 h-3 text-green-500" />
|
||||||
<span className="text-[10px] font-medium text-green-600 uppercase tracking-wide">Add</span>
|
<span className="text-[10px] font-medium text-green-600 uppercase tracking-wide">
|
||||||
|
Add
|
||||||
|
</span>
|
||||||
</div>
|
</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">
|
<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}
|
{edit.replace}
|
||||||
@@ -50,26 +84,26 @@ function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
|
||||||
const getMessageTextContent = (message: UIMessage): string => {
|
const getMessageTextContent = (message: UIMessage): string => {
|
||||||
if (!message.parts) return "";
|
if (!message.parts) return ""
|
||||||
return message.parts
|
return message.parts
|
||||||
.filter((part: any) => part.type === "text")
|
.filter((part) => part.type === "text")
|
||||||
.map((part: any) => part.text)
|
.map((part) => (part as { text: string }).text)
|
||||||
.join("\n");
|
.join("\n")
|
||||||
};
|
}
|
||||||
|
|
||||||
interface ChatMessageDisplayProps {
|
interface ChatMessageDisplayProps {
|
||||||
messages: UIMessage[];
|
messages: UIMessage[]
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void
|
||||||
sessionId?: string;
|
sessionId?: string
|
||||||
onRegenerate?: (messageIndex: number) => void;
|
onRegenerate?: (messageIndex: number) => void
|
||||||
onEditMessage?: (messageIndex: number, newText: string) => void;
|
onEditMessage?: (messageIndex: number, newText: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessageDisplay({
|
export function ChatMessageDisplay({
|
||||||
@@ -80,43 +114,48 @@ export function ChatMessageDisplay({
|
|||||||
onRegenerate,
|
onRegenerate,
|
||||||
onEditMessage,
|
onEditMessage,
|
||||||
}: ChatMessageDisplayProps) {
|
}: ChatMessageDisplayProps) {
|
||||||
const { chartXML, loadDiagram: onDisplayChart } = useDiagram();
|
const { chartXML, loadDiagram: onDisplayChart } = useDiagram()
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const previousXML = useRef<string>("");
|
const previousXML = useRef<string>("")
|
||||||
const processedToolCalls = useRef<Set<string>>(new Set());
|
const processedToolCalls = useRef<Set<string>>(new Set())
|
||||||
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
|
||||||
{}
|
{},
|
||||||
);
|
)
|
||||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null)
|
||||||
const [copyFailedMessageId, setCopyFailedMessageId] = useState<string | null>(null);
|
const [copyFailedMessageId, setCopyFailedMessageId] = useState<
|
||||||
const [feedback, setFeedback] = useState<Record<string, "good" | "bad">>({});
|
string | null
|
||||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
>(null)
|
||||||
const [editText, setEditText] = useState<string>("");
|
const [feedback, setFeedback] = useState<Record<string, "good" | "bad">>({})
|
||||||
|
const [editingMessageId, setEditingMessageId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
const editTextareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const [editText, setEditText] = useState<string>("")
|
||||||
|
|
||||||
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text)
|
||||||
setCopiedMessageId(messageId);
|
setCopiedMessageId(messageId)
|
||||||
setTimeout(() => setCopiedMessageId(null), 2000);
|
setTimeout(() => setCopiedMessageId(null), 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to copy message:", err);
|
console.error("Failed to copy message:", err)
|
||||||
setCopyFailedMessageId(messageId);
|
setCopyFailedMessageId(messageId)
|
||||||
setTimeout(() => setCopyFailedMessageId(null), 2000);
|
setTimeout(() => setCopyFailedMessageId(null), 2000)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const submitFeedback = async (messageId: string, value: "good" | "bad") => {
|
const submitFeedback = async (messageId: string, value: "good" | "bad") => {
|
||||||
// Toggle off if already selected
|
// Toggle off if already selected
|
||||||
if (feedback[messageId] === value) {
|
if (feedback[messageId] === value) {
|
||||||
setFeedback((prev) => {
|
setFeedback((prev) => {
|
||||||
const next = { ...prev };
|
const next = { ...prev }
|
||||||
delete next[messageId];
|
delete next[messageId]
|
||||||
return next;
|
return next
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeedback((prev) => ({ ...prev, [messageId]: value }));
|
setFeedback((prev) => ({ ...prev, [messageId]: value }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch("/api/log-feedback", {
|
await fetch("/api/log-feedback", {
|
||||||
@@ -127,97 +166,108 @@ export function ChatMessageDisplay({
|
|||||||
feedback: value,
|
feedback: value,
|
||||||
sessionId,
|
sessionId,
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to log feedback:", error);
|
console.warn("Failed to log feedback:", error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDisplayChart = useCallback(
|
const handleDisplayChart = useCallback(
|
||||||
(xml: string) => {
|
(xml: string) => {
|
||||||
const currentXml = xml || "";
|
const currentXml = xml || ""
|
||||||
const convertedXml = convertToLegalXml(currentXml);
|
const convertedXml = convertToLegalXml(currentXml)
|
||||||
if (convertedXml !== previousXML.current) {
|
if (convertedXml !== previousXML.current) {
|
||||||
const replacedXML = replaceNodes(chartXML, convertedXml);
|
const replacedXML = replaceNodes(chartXML, convertedXml)
|
||||||
|
|
||||||
const validationError = validateMxCellStructure(replacedXML);
|
const validationError = validateMxCellStructure(replacedXML)
|
||||||
if (!validationError) {
|
if (!validationError) {
|
||||||
previousXML.current = convertedXml;
|
previousXML.current = convertedXml
|
||||||
onDisplayChart(replacedXML);
|
onDisplayChart(replacedXML)
|
||||||
} else {
|
} else {
|
||||||
console.log("[ChatMessageDisplay] XML validation failed:", validationError);
|
console.log(
|
||||||
|
"[ChatMessageDisplay] XML validation failed:",
|
||||||
|
validationError,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[chartXML, onDisplayChart]
|
[chartXML, onDisplayChart],
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingMessageId && editTextareaRef.current) {
|
||||||
|
editTextareaRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [editingMessageId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
if (message.parts) {
|
if (message.parts) {
|
||||||
message.parts.forEach((part: any) => {
|
message.parts.forEach((part) => {
|
||||||
if (part.type?.startsWith("tool-")) {
|
if (part.type?.startsWith("tool-")) {
|
||||||
const { toolCallId, state } = part;
|
const toolPart = part as ToolPartLike
|
||||||
|
const { toolCallId, state, input } = toolPart
|
||||||
|
|
||||||
if (state === "output-available") {
|
if (state === "output-available") {
|
||||||
setExpandedTools((prev) => ({
|
setExpandedTools((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[toolCallId]: false,
|
[toolCallId]: false,
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
part.type === "tool-display_diagram" &&
|
part.type === "tool-display_diagram" &&
|
||||||
part.input?.xml
|
input?.xml
|
||||||
) {
|
) {
|
||||||
|
const xml = input.xml as string
|
||||||
if (
|
if (
|
||||||
state === "input-streaming" ||
|
state === "input-streaming" ||
|
||||||
state === "input-available"
|
state === "input-available"
|
||||||
) {
|
) {
|
||||||
handleDisplayChart(part.input.xml);
|
handleDisplayChart(xml)
|
||||||
} else if (
|
} else if (
|
||||||
state === "output-available" &&
|
state === "output-available" &&
|
||||||
!processedToolCalls.current.has(toolCallId)
|
!processedToolCalls.current.has(toolCallId)
|
||||||
) {
|
) {
|
||||||
handleDisplayChart(part.input.xml);
|
handleDisplayChart(xml)
|
||||||
processedToolCalls.current.add(toolCallId);
|
processedToolCalls.current.add(toolCallId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}, [messages, handleDisplayChart]);
|
}, [messages, handleDisplayChart])
|
||||||
|
|
||||||
const renderToolPart = (part: any) => {
|
const renderToolPart = (part: ToolPartLike) => {
|
||||||
const callId = part.toolCallId;
|
const callId = part.toolCallId
|
||||||
const { state, input, output } = part;
|
const { state, input, output } = part
|
||||||
const isExpanded = expandedTools[callId] ?? true;
|
const isExpanded = expandedTools[callId] ?? true
|
||||||
const toolName = part.type?.replace("tool-", "");
|
const toolName = part.type?.replace("tool-", "")
|
||||||
|
|
||||||
const toggleExpanded = () => {
|
const toggleExpanded = () => {
|
||||||
setExpandedTools((prev) => ({
|
setExpandedTools((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[callId]: !isExpanded,
|
[callId]: !isExpanded,
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
const getToolDisplayName = (name: string) => {
|
const getToolDisplayName = (name: string) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "display_diagram":
|
case "display_diagram":
|
||||||
return "Generate Diagram";
|
return "Generate Diagram"
|
||||||
case "edit_diagram":
|
case "edit_diagram":
|
||||||
return "Edit Diagram";
|
return "Edit Diagram"
|
||||||
default:
|
default:
|
||||||
return name;
|
return name
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -249,6 +299,7 @@ export function ChatMessageDisplay({
|
|||||||
)}
|
)}
|
||||||
{input && Object.keys(input).length > 0 && (
|
{input && Object.keys(input).length > 0 && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={toggleExpanded}
|
onClick={toggleExpanded}
|
||||||
className="p-1 rounded hover:bg-muted transition-colors"
|
className="p-1 rounded hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
@@ -265,10 +316,16 @@ export function ChatMessageDisplay({
|
|||||||
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
<div className="px-4 py-3 border-t border-border/40 bg-muted/20">
|
||||||
{typeof input === "object" && input.xml ? (
|
{typeof input === "object" && input.xml ? (
|
||||||
<CodeBlock code={input.xml} language="xml" />
|
<CodeBlock code={input.xml} language="xml" />
|
||||||
) : typeof input === "object" && input.edits && Array.isArray(input.edits) ? (
|
) : typeof input === "object" &&
|
||||||
|
input.edits &&
|
||||||
|
Array.isArray(input.edits) ? (
|
||||||
<EditDiffDisplay edits={input.edits} />
|
<EditDiffDisplay edits={input.edits} />
|
||||||
) : typeof input === "object" && Object.keys(input).length > 0 ? (
|
) : typeof input === "object" &&
|
||||||
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
Object.keys(input).length > 0 ? (
|
||||||
|
<CodeBlock
|
||||||
|
code={JSON.stringify(input, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -278,8 +335,8 @@ export function ChatMessageDisplay({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full w-full scrollbar-thin">
|
<ScrollArea className="h-full w-full scrollbar-thin">
|
||||||
@@ -288,92 +345,156 @@ export function ChatMessageDisplay({
|
|||||||
) : (
|
) : (
|
||||||
<div className="py-4 px-4 space-y-4">
|
<div className="py-4 px-4 space-y-4">
|
||||||
{messages.map((message, messageIndex) => {
|
{messages.map((message, messageIndex) => {
|
||||||
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
|
const userMessageText =
|
||||||
const isLastAssistantMessage = message.role === "assistant" && (
|
message.role === "user"
|
||||||
messageIndex === messages.length - 1 ||
|
? getMessageTextContent(message)
|
||||||
messages.slice(messageIndex + 1).every(m => m.role !== "assistant")
|
: ""
|
||||||
);
|
const isLastAssistantMessage =
|
||||||
const isLastUserMessage = message.role === "user" && (
|
message.role === "assistant" &&
|
||||||
messageIndex === messages.length - 1 ||
|
(messageIndex === messages.length - 1 ||
|
||||||
messages.slice(messageIndex + 1).every(m => m.role !== "user")
|
messages
|
||||||
);
|
.slice(messageIndex + 1)
|
||||||
const isEditing = editingMessageId === message.id;
|
.every((m) => m.role !== "assistant"))
|
||||||
|
const isLastUserMessage =
|
||||||
|
message.role === "user" &&
|
||||||
|
(messageIndex === messages.length - 1 ||
|
||||||
|
messages
|
||||||
|
.slice(messageIndex + 1)
|
||||||
|
.every((m) => m.role !== "user"))
|
||||||
|
const isEditing = editingMessageId === message.id
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`flex w-full ${message.role === "user" ? "justify-end" : "justify-start"} animate-message-in`}
|
className={`flex w-full ${message.role === "user" ? "justify-end" : "justify-start"} animate-message-in`}
|
||||||
style={{ animationDelay: `${messageIndex * 50}ms` }}
|
style={{
|
||||||
|
animationDelay: `${messageIndex * 50}ms`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{message.role === "user" && userMessageText && !isEditing && (
|
{message.role === "user" &&
|
||||||
<div className="flex items-center gap-1 self-center mr-2">
|
userMessageText &&
|
||||||
{/* Edit button - only on last user message */}
|
!isEditing && (
|
||||||
{onEditMessage && isLastUserMessage && (
|
<div className="flex items-center gap-1 self-center mr-2">
|
||||||
|
{/* Edit button - only on last user message */}
|
||||||
|
{onEditMessage &&
|
||||||
|
isLastUserMessage && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingMessageId(
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
|
setEditText(
|
||||||
|
userMessageText,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
||||||
|
title="Edit message"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
setEditingMessageId(message.id);
|
onClick={() =>
|
||||||
setEditText(userMessageText);
|
copyMessageToClipboard(
|
||||||
}}
|
message.id,
|
||||||
|
userMessageText,
|
||||||
|
)
|
||||||
|
}
|
||||||
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
||||||
title="Edit message"
|
title={
|
||||||
|
copiedMessageId ===
|
||||||
|
message.id
|
||||||
|
? "Copied!"
|
||||||
|
: copyFailedMessageId ===
|
||||||
|
message.id
|
||||||
|
? "Failed to copy"
|
||||||
|
: "Copy message"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
{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>
|
</button>
|
||||||
)}
|
</div>
|
||||||
<button
|
)}
|
||||||
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
|
||||||
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
|
||||||
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="max-w-[85%] min-w-0">
|
<div className="max-w-[85%] min-w-0">
|
||||||
{/* Edit mode for user messages */}
|
{/* Edit mode for user messages */}
|
||||||
{isEditing && message.role === "user" ? (
|
{isEditing && message.role === "user" ? (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={editTextareaRef}
|
||||||
value={editText}
|
value={editText}
|
||||||
onChange={(e) => setEditText(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setEditText(e.target.value)
|
||||||
|
}
|
||||||
className="w-full min-w-[300px] px-4 py-3 text-sm rounded-2xl border border-primary bg-background text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
className="w-full min-w-[300px] px-4 py-3 text-sm rounded-2xl border border-primary bg-background text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
rows={Math.min(editText.split('\n').length + 1, 6)}
|
rows={Math.min(
|
||||||
autoFocus
|
editText.split("\n")
|
||||||
|
.length + 1,
|
||||||
|
6,
|
||||||
|
)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
setEditingMessageId(null);
|
setEditingMessageId(
|
||||||
setEditText("");
|
null,
|
||||||
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
)
|
||||||
e.preventDefault();
|
setEditText("")
|
||||||
if (editText.trim() && onEditMessage) {
|
} else if (
|
||||||
onEditMessage(messageIndex, editText.trim());
|
e.key === "Enter" &&
|
||||||
setEditingMessageId(null);
|
(e.metaKey || e.ctrlKey)
|
||||||
setEditText("");
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (
|
||||||
|
editText.trim() &&
|
||||||
|
onEditMessage
|
||||||
|
) {
|
||||||
|
onEditMessage(
|
||||||
|
messageIndex,
|
||||||
|
editText.trim(),
|
||||||
|
)
|
||||||
|
setEditingMessageId(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
setEditText("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingMessageId(null);
|
setEditingMessageId(
|
||||||
setEditText("");
|
null,
|
||||||
|
)
|
||||||
|
setEditText("")
|
||||||
}}
|
}}
|
||||||
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/80 transition-colors"
|
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/80 transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (editText.trim() && onEditMessage) {
|
if (
|
||||||
onEditMessage(messageIndex, editText.trim());
|
editText.trim() &&
|
||||||
setEditingMessageId(null);
|
onEditMessage
|
||||||
setEditText("");
|
) {
|
||||||
|
onEditMessage(
|
||||||
|
messageIndex,
|
||||||
|
editText.trim(),
|
||||||
|
)
|
||||||
|
setEditingMessageId(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
setEditText("")
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!editText.trim()}
|
disabled={!editText.trim()}
|
||||||
@@ -385,100 +506,202 @@ export function ChatMessageDisplay({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Text content in bubble */
|
/* Text content in bubble */
|
||||||
message.parts?.some((part: any) => part.type === "text" || part.type === "file") && (
|
message.parts?.some(
|
||||||
|
(part) =>
|
||||||
|
part.type === "text" ||
|
||||||
|
part.type === "file",
|
||||||
|
) && (
|
||||||
<div
|
<div
|
||||||
className={`px-4 py-3 text-sm leading-relaxed ${
|
className={`px-4 py-3 text-sm leading-relaxed ${
|
||||||
message.role === "user"
|
message.role === "user"
|
||||||
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
||||||
: message.role === "system"
|
: message.role ===
|
||||||
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
"system"
|
||||||
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
||||||
|
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||||
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
||||||
|
role={
|
||||||
|
message.role === "user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
? "button"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
tabIndex={
|
||||||
|
message.role === "user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
? 0
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (message.role === "user" && isLastUserMessage && onEditMessage) {
|
if (
|
||||||
setEditingMessageId(message.id);
|
message.role ===
|
||||||
setEditText(userMessageText);
|
"user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
) {
|
||||||
|
setEditingMessageId(
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
|
setEditText(
|
||||||
|
userMessageText,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={message.role === "user" && isLastUserMessage && onEditMessage ? "Click to edit" : undefined}
|
onKeyDown={(e) => {
|
||||||
>
|
if (
|
||||||
{message.parts?.map((part: any, index: number) => {
|
(e.key === "Enter" ||
|
||||||
switch (part.type) {
|
e.key === " ") &&
|
||||||
case "text":
|
message.role ===
|
||||||
return (
|
"user" &&
|
||||||
<div key={index} className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
isLastUserMessage &&
|
||||||
message.role === "user"
|
onEditMessage
|
||||||
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
|
) {
|
||||||
: "dark:prose-invert"
|
e.preventDefault()
|
||||||
}`}>
|
setEditingMessageId(
|
||||||
<ReactMarkdown>{part.text}</ReactMarkdown>
|
message.id,
|
||||||
</div>
|
)
|
||||||
);
|
setEditText(
|
||||||
case "file":
|
userMessageText,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
})}
|
}}
|
||||||
|
title={
|
||||||
|
message.role === "user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
? "Click to edit"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{message.parts?.map(
|
||||||
|
(part, index) => {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${message.id}-text-${index}`}
|
||||||
|
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
||||||
|
message.role ===
|
||||||
|
"user"
|
||||||
|
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
|
||||||
|
: "dark:prose-invert"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ReactMarkdown>
|
||||||
|
{
|
||||||
|
part.text
|
||||||
|
}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case "file":
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${message.id}-file-${part.url}`}
|
||||||
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{/* Tool calls outside bubble */}
|
{/* Tool calls outside bubble */}
|
||||||
{message.parts?.map((part: any) => {
|
{message.parts?.map((part) => {
|
||||||
if (part.type?.startsWith("tool-")) {
|
if (part.type?.startsWith("tool-")) {
|
||||||
return renderToolPart(part);
|
return renderToolPart(
|
||||||
|
part as ToolPartLike,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
})}
|
})}
|
||||||
{/* Action buttons for assistant messages */}
|
{/* Action buttons for assistant messages */}
|
||||||
{message.role === "assistant" && (
|
{message.role === "assistant" && (
|
||||||
<div className="flex items-center gap-1 mt-2">
|
<div className="flex items-center gap-1 mt-2">
|
||||||
{/* Copy button */}
|
{/* Copy button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => copyMessageToClipboard(message.id, getMessageTextContent(message))}
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
copyMessageToClipboard(
|
||||||
|
message.id,
|
||||||
|
getMessageTextContent(
|
||||||
|
message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
className={`p-1.5 rounded-lg transition-colors ${
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
copiedMessageId === message.id
|
copiedMessageId ===
|
||||||
|
message.id
|
||||||
? "text-green-600 bg-green-100"
|
? "text-green-600 bg-green-100"
|
||||||
: "text-muted-foreground/60 hover:text-foreground hover:bg-muted"
|
: "text-muted-foreground/60 hover:text-foreground hover:bg-muted"
|
||||||
}`}
|
}`}
|
||||||
title={copiedMessageId === message.id ? "Copied!" : "Copy response"}
|
title={
|
||||||
|
copiedMessageId ===
|
||||||
|
message.id
|
||||||
|
? "Copied!"
|
||||||
|
: "Copy response"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{copiedMessageId === message.id ? (
|
{copiedMessageId ===
|
||||||
|
message.id ? (
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3.5 w-3.5" />
|
<Copy className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{/* Regenerate button - only on last assistant message */}
|
{/* Regenerate button - only on last assistant message */}
|
||||||
{onRegenerate && isLastAssistantMessage && (
|
{onRegenerate &&
|
||||||
<button
|
isLastAssistantMessage && (
|
||||||
onClick={() => onRegenerate(messageIndex)}
|
<button
|
||||||
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
type="button"
|
||||||
title="Regenerate response"
|
onClick={() =>
|
||||||
>
|
onRegenerate(
|
||||||
<RotateCcw className="h-3.5 w-3.5" />
|
messageIndex,
|
||||||
</button>
|
)
|
||||||
)}
|
}
|
||||||
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
||||||
|
title="Regenerate response"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="w-px h-4 bg-border mx-1" />
|
<div className="w-px h-4 bg-border mx-1" />
|
||||||
{/* Thumbs up */}
|
{/* Thumbs up */}
|
||||||
<button
|
<button
|
||||||
onClick={() => submitFeedback(message.id, "good")}
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
submitFeedback(
|
||||||
|
message.id,
|
||||||
|
"good",
|
||||||
|
)
|
||||||
|
}
|
||||||
className={`p-1.5 rounded-lg transition-colors ${
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
feedback[message.id] === "good"
|
feedback[message.id] ===
|
||||||
|
"good"
|
||||||
? "text-green-600 bg-green-100"
|
? "text-green-600 bg-green-100"
|
||||||
: "text-muted-foreground/60 hover:text-green-600 hover:bg-green-50"
|
: "text-muted-foreground/60 hover:text-green-600 hover:bg-green-50"
|
||||||
}`}
|
}`}
|
||||||
@@ -488,9 +711,16 @@ export function ChatMessageDisplay({
|
|||||||
</button>
|
</button>
|
||||||
{/* Thumbs down */}
|
{/* Thumbs down */}
|
||||||
<button
|
<button
|
||||||
onClick={() => submitFeedback(message.id, "bad")}
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
submitFeedback(
|
||||||
|
message.id,
|
||||||
|
"bad",
|
||||||
|
)
|
||||||
|
}
|
||||||
className={`p-1.5 rounded-lg transition-colors ${
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
feedback[message.id] === "bad"
|
feedback[message.id] ===
|
||||||
|
"bad"
|
||||||
? "text-red-600 bg-red-100"
|
? "text-red-600 bg-red-100"
|
||||||
: "text-muted-foreground/60 hover:text-red-600 hover:bg-red-50"
|
: "text-muted-foreground/60 hover:text-red-600 hover:bg-red-50"
|
||||||
}`}
|
}`}
|
||||||
@@ -502,11 +732,11 @@ export function ChatMessageDisplay({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,36 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import type React from "react";
|
import { useChat } from "@ai-sdk/react"
|
||||||
import { useRef, useEffect, useState } from "react";
|
import { DefaultChatTransport } from "ai"
|
||||||
import { flushSync } from "react-dom";
|
|
||||||
import { FaGithub } from "react-icons/fa";
|
|
||||||
import {
|
import {
|
||||||
|
CheckCircle,
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
Settings,
|
Settings,
|
||||||
CheckCircle,
|
} from "lucide-react"
|
||||||
} from "lucide-react";
|
import Image from "next/image"
|
||||||
import Link from "next/link";
|
import Link from "next/link"
|
||||||
import Image from "next/image";
|
import type React from "react"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { flushSync } from "react-dom"
|
||||||
import { DefaultChatTransport } from "ai";
|
import { FaGithub } from "react-icons/fa"
|
||||||
import { ChatInput } from "@/components/chat-input";
|
import { Toaster } from "sonner"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display";
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { ChatInput } from "@/components/chat-input"
|
||||||
import { replaceNodes, formatXML, validateMxCellStructure } from "@/lib/utils";
|
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import {
|
import {
|
||||||
SettingsDialog,
|
SettingsDialog,
|
||||||
STORAGE_ACCESS_CODE_KEY,
|
STORAGE_ACCESS_CODE_KEY,
|
||||||
} from "@/components/settings-dialog";
|
} from "@/components/settings-dialog"
|
||||||
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { formatXML, validateMxCellStructure } from "@/lib/utils"
|
||||||
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean
|
||||||
onToggleVisibility: () => void;
|
onToggleVisibility: () => void
|
||||||
drawioUi: "min" | "sketch";
|
drawioUi: "min" | "sketch"
|
||||||
onToggleDrawioUi: () => void;
|
onToggleDrawioUi: () => void
|
||||||
isMobile?: boolean;
|
isMobile?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatPanel({
|
export default function ChatPanel({
|
||||||
@@ -48,59 +47,61 @@ export default function ChatPanel({
|
|||||||
resolverRef,
|
resolverRef,
|
||||||
chartXML,
|
chartXML,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
} = useDiagram();
|
} = useDiagram()
|
||||||
|
|
||||||
const onFetchChart = (saveToHistory = true) => {
|
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
|
||||||
}
|
}
|
||||||
if (saveToHistory) {
|
if (saveToHistory) {
|
||||||
onExport();
|
onExport()
|
||||||
} else {
|
} else {
|
||||||
handleExportWithoutHistory();
|
handleExportWithoutHistory()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Promise<string>((_, reject) =>
|
new Promise<string>((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
reject(
|
reject(
|
||||||
new Error("Chart export timed out after 10 seconds")
|
new Error(
|
||||||
|
"Chart export timed out after 10 seconds",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
10000
|
10000,
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([])
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||||
const [accessCodeRequired, setAccessCodeRequired] = useState(false);
|
const [accessCodeRequired, setAccessCodeRequired] = useState(false)
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("")
|
||||||
|
|
||||||
// Check if access code is required on mount
|
// Check if access code is required on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/config")
|
fetch("/api/config")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => setAccessCodeRequired(data.accessCodeRequired))
|
.then((data) => setAccessCodeRequired(data.accessCodeRequired))
|
||||||
.catch(() => setAccessCodeRequired(false));
|
.catch(() => setAccessCodeRequired(false))
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Generate a unique session ID for Langfuse tracing
|
// Generate a unique session ID for Langfuse tracing
|
||||||
const [sessionId, setSessionId] = useState(
|
const [sessionId, setSessionId] = useState(
|
||||||
() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||||
);
|
)
|
||||||
|
|
||||||
// Store XML snapshots for each user message (keyed by message index)
|
// Store XML snapshots for each user message (keyed by message index)
|
||||||
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map());
|
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map())
|
||||||
|
|
||||||
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
|
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
|
||||||
const chartXMLRef = useRef(chartXML);
|
const chartXMLRef = useRef(chartXML)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chartXMLRef.current = chartXML;
|
chartXMLRef.current = chartXML
|
||||||
}, [chartXML]);
|
}, [chartXML])
|
||||||
|
|
||||||
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
||||||
useChat({
|
useChat({
|
||||||
@@ -109,71 +110,71 @@ export default function ChatPanel({
|
|||||||
}),
|
}),
|
||||||
async onToolCall({ toolCall }) {
|
async onToolCall({ toolCall }) {
|
||||||
if (toolCall.toolName === "display_diagram") {
|
if (toolCall.toolName === "display_diagram") {
|
||||||
const { xml } = toolCall.input as { xml: string };
|
const { xml } = toolCall.input as { xml: string }
|
||||||
|
|
||||||
const validationError = validateMxCellStructure(xml);
|
const validationError = validateMxCellStructure(xml)
|
||||||
|
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
addToolResult({
|
addToolResult({
|
||||||
tool: "display_diagram",
|
tool: "display_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: validationError,
|
output: validationError,
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
addToolResult({
|
addToolResult({
|
||||||
tool: "display_diagram",
|
tool: "display_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: "Successfully displayed the diagram.",
|
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 {
|
||||||
console.log("[edit_diagram] Starting...");
|
console.log("[edit_diagram] Starting...")
|
||||||
// Use chartXML from ref directly - more reliable than export
|
// Use chartXML from ref directly - more reliable than export
|
||||||
// especially on Vercel where DrawIO iframe may have latency issues
|
// especially on Vercel where DrawIO iframe may have latency issues
|
||||||
// Using ref to avoid stale closure in callback
|
// Using ref to avoid stale closure in callback
|
||||||
const cachedXML = chartXMLRef.current;
|
const cachedXML = chartXMLRef.current
|
||||||
if (cachedXML) {
|
if (cachedXML) {
|
||||||
currentXml = cachedXML;
|
currentXml = cachedXML
|
||||||
console.log(
|
console.log(
|
||||||
"[edit_diagram] Using cached chartXML, length:",
|
"[edit_diagram] Using cached chartXML, length:",
|
||||||
currentXml.length
|
currentXml.length,
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
// Fallback to export only if no cached XML
|
// Fallback to export only if no cached XML
|
||||||
console.log(
|
console.log(
|
||||||
"[edit_diagram] No cached XML, fetching from DrawIO..."
|
"[edit_diagram] No cached XML, fetching from DrawIO...",
|
||||||
);
|
)
|
||||||
currentXml = await onFetchChart(false);
|
currentXml = await onFetchChart(false)
|
||||||
console.log(
|
console.log(
|
||||||
"[edit_diagram] Got XML from export, length:",
|
"[edit_diagram] Got XML from export, length:",
|
||||||
currentXml.length
|
currentXml.length,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { replaceXMLParts } = await import("@/lib/utils");
|
const { replaceXMLParts } = await import("@/lib/utils")
|
||||||
const editedXml = replaceXMLParts(currentXml, edits);
|
const editedXml = replaceXMLParts(currentXml, edits)
|
||||||
|
|
||||||
onDisplayChart(editedXml);
|
onDisplayChart(editedXml)
|
||||||
|
|
||||||
addToolResult({
|
addToolResult({
|
||||||
tool: "edit_diagram",
|
tool: "edit_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
||||||
});
|
})
|
||||||
console.log("[edit_diagram] Success");
|
console.log("[edit_diagram] Success")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[edit_diagram] Failed:", error);
|
console.error("[edit_diagram] Failed:", error)
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: String(error);
|
: String(error)
|
||||||
|
|
||||||
addToolResult({
|
addToolResult({
|
||||||
tool: "edit_diagram",
|
tool: "edit_diagram",
|
||||||
@@ -186,14 +187,14 @@ ${currentXml || "No XML available"}
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
// Silence access code error in console since it's handled by UI
|
// Silence access code error in console since it's handled by UI
|
||||||
if (!error.message.includes("Invalid or missing access code")) {
|
if (!error.message.includes("Invalid or missing access code")) {
|
||||||
console.error("Chat error:", error);
|
console.error("Chat error:", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add system message for error so it can be cleared
|
// Add system message for error so it can be cleared
|
||||||
@@ -203,63 +204,63 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
role: "system" as const,
|
role: "system" as const,
|
||||||
content: error.message,
|
content: error.message,
|
||||||
parts: [{ type: "text" as const, text: error.message }],
|
parts: [{ type: "text" as const, text: error.message }],
|
||||||
};
|
}
|
||||||
return [...currentMessages, errorMessage];
|
return [...currentMessages, errorMessage]
|
||||||
});
|
})
|
||||||
|
|
||||||
if (error.message.includes("Invalid or missing access code")) {
|
if (error.message.includes("Invalid or missing access code")) {
|
||||||
// Show settings button and open dialog to help user fix it
|
// Show settings button and open dialog to help user fix it
|
||||||
setAccessCodeRequired(true);
|
setAccessCodeRequired(true)
|
||||||
setShowSettingsDialog(true);
|
setShowSettingsDialog(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages])
|
||||||
|
|
||||||
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
const isProcessing = status === "streaming" || status === "submitted";
|
const isProcessing = status === "streaming" || status === "submitted"
|
||||||
if (input.trim() && !isProcessing) {
|
if (input.trim() && !isProcessing) {
|
||||||
try {
|
try {
|
||||||
let chartXml = await onFetchChart();
|
let chartXml = await onFetchChart()
|
||||||
chartXml = formatXML(chartXml);
|
chartXml = formatXML(chartXml)
|
||||||
|
|
||||||
// Update ref directly to avoid race condition with React's async state update
|
// Update ref directly to avoid race condition with React's async state update
|
||||||
// This ensures edit_diagram has the correct XML before AI responds
|
// This ensures edit_diagram has the correct XML before AI responds
|
||||||
chartXMLRef.current = chartXml;
|
chartXMLRef.current = chartXml
|
||||||
|
|
||||||
const parts: any[] = [{ type: "text", text: input }];
|
const parts: any[] = [{ type: "text", text: input }]
|
||||||
|
|
||||||
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()
|
||||||
const dataUrl = await new Promise<string>((resolve) => {
|
const dataUrl = await new Promise<string>((resolve) => {
|
||||||
reader.onload = () =>
|
reader.onload = () =>
|
||||||
resolve(reader.result as string);
|
resolve(reader.result as string)
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file)
|
||||||
});
|
})
|
||||||
|
|
||||||
parts.push({
|
parts.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
url: dataUrl,
|
url: dataUrl,
|
||||||
mediaType: file.type,
|
mediaType: file.type,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save XML snapshot for this message (will be at index = current messages.length)
|
// Save XML snapshot for this message (will be at index = current messages.length)
|
||||||
const messageIndex = messages.length;
|
const messageIndex = messages.length
|
||||||
xmlSnapshotsRef.current.set(messageIndex, chartXml);
|
xmlSnapshotsRef.current.set(messageIndex, chartXml)
|
||||||
|
|
||||||
const accessCode =
|
const accessCode =
|
||||||
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || "";
|
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts },
|
{ parts },
|
||||||
{
|
{
|
||||||
@@ -270,78 +271,78 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
headers: {
|
headers: {
|
||||||
"x-access-code": accessCode,
|
"x-access-code": accessCode,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
setInput("");
|
setInput("")
|
||||||
setFiles([]);
|
setFiles([])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching chart data:", error);
|
console.error("Error fetching chart data:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleInputChange = (
|
const handleInputChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
setInput(e.target.value);
|
setInput(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFileChange = (newFiles: File[]) => {
|
const handleFileChange = (newFiles: File[]) => {
|
||||||
setFiles(newFiles);
|
setFiles(newFiles)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleRegenerate = async (messageIndex: number) => {
|
const handleRegenerate = async (messageIndex: number) => {
|
||||||
const isProcessing = status === "streaming" || status === "submitted";
|
const isProcessing = status === "streaming" || status === "submitted"
|
||||||
if (isProcessing) return;
|
if (isProcessing) return
|
||||||
|
|
||||||
// Find the user message before this assistant message
|
// Find the user message before this assistant message
|
||||||
let userMessageIndex = messageIndex - 1;
|
let userMessageIndex = messageIndex - 1
|
||||||
while (
|
while (
|
||||||
userMessageIndex >= 0 &&
|
userMessageIndex >= 0 &&
|
||||||
messages[userMessageIndex].role !== "user"
|
messages[userMessageIndex].role !== "user"
|
||||||
) {
|
) {
|
||||||
userMessageIndex--;
|
userMessageIndex--
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userMessageIndex < 0) return;
|
if (userMessageIndex < 0) return
|
||||||
|
|
||||||
const userMessage = messages[userMessageIndex];
|
const userMessage = messages[userMessageIndex]
|
||||||
const userParts = userMessage.parts;
|
const userParts = userMessage.parts
|
||||||
|
|
||||||
// Get the text from the user message
|
// Get the text from the user message
|
||||||
const textPart = userParts?.find((p: any) => p.type === "text");
|
const textPart = userParts?.find((p: any) => p.type === "text")
|
||||||
if (!textPart) return;
|
if (!textPart) return
|
||||||
|
|
||||||
// Get the saved XML snapshot for this user message
|
// Get the saved XML snapshot for this user message
|
||||||
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex);
|
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex)
|
||||||
if (!savedXml) {
|
if (!savedXml) {
|
||||||
console.error(
|
console.error(
|
||||||
"No saved XML snapshot for message index:",
|
"No saved XML snapshot for message index:",
|
||||||
userMessageIndex
|
userMessageIndex,
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the diagram to the saved state
|
// Restore the diagram to the saved state
|
||||||
onDisplayChart(savedXml);
|
onDisplayChart(savedXml)
|
||||||
|
|
||||||
// Update ref directly to ensure edit_diagram has the correct XML
|
// Update ref directly to ensure edit_diagram has the correct XML
|
||||||
chartXMLRef.current = savedXml;
|
chartXMLRef.current = savedXml
|
||||||
|
|
||||||
// Clean up snapshots for messages after the user message (they will be removed)
|
// Clean up snapshots for messages after the user message (they will be removed)
|
||||||
for (const key of xmlSnapshotsRef.current.keys()) {
|
for (const key of xmlSnapshotsRef.current.keys()) {
|
||||||
if (key > userMessageIndex) {
|
if (key > userMessageIndex) {
|
||||||
xmlSnapshotsRef.current.delete(key);
|
xmlSnapshotsRef.current.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
||||||
// Use flushSync to ensure state update is processed synchronously before sending
|
// Use flushSync to ensure state update is processed synchronously before sending
|
||||||
const newMessages = messages.slice(0, userMessageIndex);
|
const newMessages = messages.slice(0, userMessageIndex)
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
setMessages(newMessages);
|
setMessages(newMessages)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Now send the message after state is guaranteed to be updated
|
// Now send the message after state is guaranteed to be updated
|
||||||
sendMessage(
|
sendMessage(
|
||||||
@@ -351,54 +352,54 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xml: savedXml,
|
xml: savedXml,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEditMessage = async (messageIndex: number, newText: string) => {
|
const handleEditMessage = async (messageIndex: number, newText: string) => {
|
||||||
const isProcessing = status === "streaming" || status === "submitted";
|
const isProcessing = status === "streaming" || status === "submitted"
|
||||||
if (isProcessing) return;
|
if (isProcessing) return
|
||||||
|
|
||||||
const message = messages[messageIndex];
|
const message = messages[messageIndex]
|
||||||
if (!message || message.role !== "user") return;
|
if (!message || message.role !== "user") return
|
||||||
|
|
||||||
// Get the saved XML snapshot for this user message
|
// Get the saved XML snapshot for this user message
|
||||||
const savedXml = xmlSnapshotsRef.current.get(messageIndex);
|
const savedXml = xmlSnapshotsRef.current.get(messageIndex)
|
||||||
if (!savedXml) {
|
if (!savedXml) {
|
||||||
console.error(
|
console.error(
|
||||||
"No saved XML snapshot for message index:",
|
"No saved XML snapshot for message index:",
|
||||||
messageIndex
|
messageIndex,
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the diagram to the saved state
|
// Restore the diagram to the saved state
|
||||||
onDisplayChart(savedXml);
|
onDisplayChart(savedXml)
|
||||||
|
|
||||||
// Update ref directly to ensure edit_diagram has the correct XML
|
// Update ref directly to ensure edit_diagram has the correct XML
|
||||||
chartXMLRef.current = savedXml;
|
chartXMLRef.current = savedXml
|
||||||
|
|
||||||
// Clean up snapshots for messages after the user message (they will be removed)
|
// Clean up snapshots for messages after the user message (they will be removed)
|
||||||
for (const key of xmlSnapshotsRef.current.keys()) {
|
for (const key of xmlSnapshotsRef.current.keys()) {
|
||||||
if (key > messageIndex) {
|
if (key > messageIndex) {
|
||||||
xmlSnapshotsRef.current.delete(key);
|
xmlSnapshotsRef.current.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new parts with updated text
|
// Create new parts with updated text
|
||||||
const newParts = message.parts?.map((part: any) => {
|
const newParts = message.parts?.map((part: any) => {
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
return { ...part, text: newText };
|
return { ...part, text: newText }
|
||||||
}
|
}
|
||||||
return part;
|
return part
|
||||||
}) || [{ type: "text", text: newText }];
|
}) || [{ type: "text", text: newText }]
|
||||||
|
|
||||||
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
||||||
// Use flushSync to ensure state update is processed synchronously before sending
|
// Use flushSync to ensure state update is processed synchronously before sending
|
||||||
const newMessages = messages.slice(0, messageIndex);
|
const newMessages = messages.slice(0, messageIndex)
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
setMessages(newMessages);
|
setMessages(newMessages)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Now send the edited message after state is guaranteed to be updated
|
// Now send the edited message after state is guaranteed to be updated
|
||||||
sendMessage(
|
sendMessage(
|
||||||
@@ -408,9 +409,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xml: savedXml,
|
xml: savedXml,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Collapsed view (desktop only)
|
// Collapsed view (desktop only)
|
||||||
if (!isVisible && !isMobile) {
|
if (!isVisible && !isMobile) {
|
||||||
@@ -435,7 +436,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
AI Chat
|
AI Chat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full view
|
// Full view
|
||||||
@@ -447,7 +448,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
style={{ position: "absolute" }}
|
style={{ position: "absolute" }}
|
||||||
/>
|
/>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className={`${isMobile ? "px-3 py-2" : "px-5 py-4"} border-b border-border/50`}>
|
<header
|
||||||
|
className={`${isMobile ? "px-3 py-2" : "px-5 py-4"} border-b border-border/50`}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -458,7 +461,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
height={isMobile ? 24 : 28}
|
height={isMobile ? 24 : 28}
|
||||||
className="rounded"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
<h1 className={`${isMobile ? "text-sm" : "text-base"} font-semibold tracking-tight whitespace-nowrap`}>
|
<h1
|
||||||
|
className={`${isMobile ? "text-sm" : "text-base"} font-semibold tracking-tight whitespace-nowrap`}
|
||||||
|
>
|
||||||
Next AI Drawio
|
Next AI Drawio
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,7 +493,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
<FaGithub className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`} />
|
<FaGithub
|
||||||
|
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
{accessCodeRequired && (
|
{accessCodeRequired && (
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
@@ -498,7 +505,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
onClick={() => setShowSettingsDialog(true)}
|
onClick={() => setShowSettingsDialog(true)}
|
||||||
className="hover:bg-accent"
|
className="hover:bg-accent"
|
||||||
>
|
>
|
||||||
<Settings className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`} />
|
<Settings
|
||||||
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||||
|
/>
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
)}
|
)}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
@@ -529,21 +538,23 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<footer className={`${isMobile ? "p-2" : "p-4"} border-t border-border/50 bg-card/50`}>
|
<footer
|
||||||
|
className={`${isMobile ? "p-2" : "p-4"} border-t border-border/50 bg-card/50`}
|
||||||
|
>
|
||||||
<ChatInput
|
<ChatInput
|
||||||
input={input}
|
input={input}
|
||||||
status={status}
|
status={status}
|
||||||
onSubmit={onFormSubmit}
|
onSubmit={onFormSubmit}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onClearChat={() => {
|
onClearChat={() => {
|
||||||
setMessages([]);
|
setMessages([])
|
||||||
clearDiagram();
|
clearDiagram()
|
||||||
setSessionId(
|
setSessionId(
|
||||||
`session-${Date.now()}-${Math.random()
|
`session-${Date.now()}-${Math.random()
|
||||||
.toString(36)
|
.toString(36)
|
||||||
.slice(2, 9)}`
|
.slice(2, 9)}`,
|
||||||
);
|
)
|
||||||
xmlSnapshotsRef.current.clear();
|
xmlSnapshotsRef.current.clear()
|
||||||
}}
|
}}
|
||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
@@ -561,5 +572,5 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
onOpenChange={setShowSettingsDialog}
|
onOpenChange={setShowSettingsDialog}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Highlight, themes } from "prism-react-renderer";
|
import { Highlight, themes } from "prism-react-renderer"
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
code: string;
|
code: string
|
||||||
language?: "xml" | "json";
|
language?: "xml" | "json"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden w-full">
|
<div className="overflow-hidden w-full">
|
||||||
<Highlight theme={themes.github} code={code} language={language}>
|
<Highlight theme={themes.github} code={code} language={language}>
|
||||||
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
{({
|
||||||
|
className: _className,
|
||||||
|
style,
|
||||||
|
tokens,
|
||||||
|
getLineProps,
|
||||||
|
getTokenProps,
|
||||||
|
}) => (
|
||||||
<pre
|
<pre
|
||||||
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
|
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
fontFamily: "var(--font-mono), ui-monospace, monospace",
|
fontFamily:
|
||||||
|
"var(--font-mono), ui-monospace, monospace",
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@@ -25,9 +32,16 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tokens.map((line, i) => (
|
{tokens.map((line, i) => (
|
||||||
<div key={i} {...getLineProps({ line })} style={{ wordBreak: "break-all" }}>
|
<div
|
||||||
|
key={i}
|
||||||
|
{...getLineProps({ line })}
|
||||||
|
style={{ wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
{line.map((token, key) => (
|
{line.map((token, key) => (
|
||||||
<span key={key} {...getTokenProps({ token })} />
|
<span
|
||||||
|
key={key}
|
||||||
|
{...getTokenProps({ token })}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -35,5 +49,5 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
|||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React from "react";
|
import type React from "react"
|
||||||
|
|
||||||
interface ErrorToastProps {
|
interface ErrorToastProps {
|
||||||
message: React.ReactNode;
|
message: React.ReactNode
|
||||||
onDismiss: () => void;
|
onDismiss: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
|
export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter" || e.key === " " || e.key === "Escape") {
|
if (e.key === "Enter" || e.key === " " || e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
onDismiss();
|
onDismiss()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -25,7 +25,12 @@ export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
|
|||||||
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm cursor-pointer hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
|
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm cursor-pointer hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-destructive/10 flex-shrink-0">
|
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-destructive/10 flex-shrink-0">
|
||||||
<svg className="w-4 h-4 text-destructive" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg
|
||||||
|
className="w-4 h-4 text-destructive"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
@@ -35,5 +40,5 @@ export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-foreground">{message}</span>
|
<span className="text-sm text-foreground">{message}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,84 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { X } from "lucide-react"
|
||||||
import Image from "next/image";
|
import Image from "next/image"
|
||||||
import { X } from "lucide-react";
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
interface FilePreviewListProps {
|
interface FilePreviewListProps {
|
||||||
files: File[];
|
files: File[]
|
||||||
onRemoveFile: (fileToRemove: File) => void;
|
onRemoveFile: (fileToRemove: File) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
||||||
|
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
||||||
|
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
||||||
|
|
||||||
// Cleanup object URLs on unmount
|
// Create and cleanup object URLs when files change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const objectUrls = files
|
const currentUrls = imageUrlsRef.current
|
||||||
.filter((file) => file.type.startsWith("image/"))
|
const newUrls = new Map<File, string>()
|
||||||
.map((file) => URL.createObjectURL(file));
|
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
// Reuse existing URL if file is already tracked
|
||||||
|
const existingUrl = currentUrls.get(file)
|
||||||
|
if (existingUrl) {
|
||||||
|
newUrls.set(file, existingUrl)
|
||||||
|
} else {
|
||||||
|
newUrls.set(file, URL.createObjectURL(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Revoke URLs for files that are no longer in the list
|
||||||
|
currentUrls.forEach((url, file) => {
|
||||||
|
if (!newUrls.has(file)) {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
imageUrlsRef.current = newUrls
|
||||||
|
setImageUrls(newUrls)
|
||||||
|
}, [files])
|
||||||
|
|
||||||
|
// Cleanup all URLs on unmount only
|
||||||
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
objectUrls.forEach(URL.revokeObjectURL);
|
imageUrlsRef.current.forEach((url) => {
|
||||||
};
|
URL.revokeObjectURL(url)
|
||||||
}, [files]);
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (files.length === 0) return null;
|
// Clear selected image if its URL was revoked
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedImage &&
|
||||||
|
!Array.from(imageUrls.values()).includes(selectedImage)
|
||||||
|
) {
|
||||||
|
setSelectedImage(null)
|
||||||
|
}
|
||||||
|
}, [imageUrls, selectedImage])
|
||||||
|
|
||||||
|
if (files.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
||||||
{files.map((file, index) => {
|
{files.map((file, index) => {
|
||||||
const imageUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null;
|
const imageUrl = imageUrls.get(file) || null
|
||||||
return (
|
return (
|
||||||
<div key={file.name + index} className="relative group">
|
<div key={file.name + index} className="relative group">
|
||||||
<div
|
<div
|
||||||
className="w-20 h-20 border rounded-md overflow-hidden bg-muted cursor-pointer"
|
className="w-20 h-20 border rounded-md overflow-hidden bg-muted cursor-pointer"
|
||||||
onClick={() => imageUrl && setSelectedImage(imageUrl)}
|
onClick={() =>
|
||||||
|
imageUrl && setSelectedImage(imageUrl)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{file.type.startsWith("image/") ? (
|
{file.type.startsWith("image/") && imageUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl!}
|
src={imageUrl}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
width={80}
|
width={80}
|
||||||
height={80}
|
height={80}
|
||||||
@@ -59,7 +99,7 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
|||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,5 +129,5 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react";
|
import Image from "next/image"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,34 +10,32 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog"
|
||||||
import { Button } from "@/components/ui/button";
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import Image from "next/image";
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
|
||||||
|
|
||||||
interface HistoryDialogProps {
|
interface HistoryDialogProps {
|
||||||
showHistory: boolean;
|
showHistory: boolean
|
||||||
onToggleHistory: (show: boolean) => void;
|
onToggleHistory: (show: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryDialog({
|
export function HistoryDialog({
|
||||||
showHistory,
|
showHistory,
|
||||||
onToggleHistory,
|
onToggleHistory,
|
||||||
}: HistoryDialogProps) {
|
}: HistoryDialogProps) {
|
||||||
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram();
|
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSelectedIndex(null);
|
setSelectedIndex(null)
|
||||||
onToggleHistory(false);
|
onToggleHistory(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleConfirmRestore = () => {
|
const handleConfirmRestore = () => {
|
||||||
if (selectedIndex !== null) {
|
if (selectedIndex !== null) {
|
||||||
onDisplayChart(diagramHistory[selectedIndex].xml);
|
onDisplayChart(diagramHistory[selectedIndex].xml)
|
||||||
handleClose();
|
handleClose()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
||||||
@@ -100,15 +100,12 @@ export function HistoryDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button variant="outline" onClick={handleClose}>
|
||||||
variant="outline"
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
interface ResetWarningModalProps {
|
interface ResetWarningModalProps {
|
||||||
open: boolean;
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void
|
||||||
onClear: () => void;
|
onClear: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResetWarningModal({
|
export function ResetWarningModal({
|
||||||
@@ -44,5 +44,5 @@ export function ResetWarningModal({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
} from "@/components/ui/dialog"
|
||||||
} from "@/components/ui/dialog";
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select"
|
||||||
|
|
||||||
export type ExportFormat = "drawio" | "png" | "svg";
|
export type ExportFormat = "drawio" | "png" | "svg"
|
||||||
|
|
||||||
const FORMAT_OPTIONS: { value: ExportFormat; label: string; extension: string }[] = [
|
const FORMAT_OPTIONS: {
|
||||||
|
value: ExportFormat
|
||||||
|
label: string
|
||||||
|
extension: string
|
||||||
|
}[] = [
|
||||||
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
|
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
|
||||||
{ value: "png", label: "PNG Image", extension: ".png" },
|
{ value: "png", label: "PNG Image", extension: ".png" },
|
||||||
{ value: "svg", label: "SVG Image", extension: ".svg" },
|
{ value: "svg", label: "SVG Image", extension: ".svg" },
|
||||||
];
|
]
|
||||||
|
|
||||||
interface SaveDialogProps {
|
interface SaveDialogProps {
|
||||||
open: boolean;
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void
|
||||||
onSave: (filename: string, format: ExportFormat) => void;
|
onSave: (filename: string, format: ExportFormat) => void
|
||||||
defaultFilename: string;
|
defaultFilename: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SaveDialog({
|
export function SaveDialog({
|
||||||
@@ -39,29 +43,29 @@ export function SaveDialog({
|
|||||||
onSave,
|
onSave,
|
||||||
defaultFilename,
|
defaultFilename,
|
||||||
}: SaveDialogProps) {
|
}: SaveDialogProps) {
|
||||||
const [filename, setFilename] = useState(defaultFilename);
|
const [filename, setFilename] = useState(defaultFilename)
|
||||||
const [format, setFormat] = useState<ExportFormat>("drawio");
|
const [format, setFormat] = useState<ExportFormat>("drawio")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setFilename(defaultFilename);
|
setFilename(defaultFilename)
|
||||||
}
|
}
|
||||||
}, [open, defaultFilename]);
|
}, [open, defaultFilename])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const finalFilename = filename.trim() || defaultFilename;
|
const finalFilename = filename.trim() || defaultFilename
|
||||||
onSave(finalFilename, format);
|
onSave(finalFilename, format)
|
||||||
onOpenChange(false);
|
onOpenChange(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
handleSave();
|
handleSave()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format);
|
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -72,13 +76,19 @@ export function SaveDialog({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Format</label>
|
<label className="text-sm font-medium">Format</label>
|
||||||
<Select value={format} onValueChange={(v) => setFormat(v as ExportFormat)}>
|
<Select
|
||||||
|
value={format}
|
||||||
|
onValueChange={(v) => setFormat(v as ExportFormat)}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FORMAT_OPTIONS.map((opt) => (
|
{FORMAT_OPTIONS.map((opt) => (
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
<SelectItem
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -104,12 +114,15 @@ export function SaveDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave}>Save</Button>
|
<Button onClick={handleSave}>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,46 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
} from "@/components/ui/dialog"
|
||||||
DialogDescription,
|
import { Input } from "@/components/ui/input"
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean;
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code";
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
|
|
||||||
export function SettingsDialog({
|
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||||
open,
|
const [accessCode, setAccessCode] = useState("")
|
||||||
onOpenChange,
|
|
||||||
}: SettingsDialogProps) {
|
|
||||||
const [accessCode, setAccessCode] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const storedCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || "";
|
const storedCode =
|
||||||
setAccessCode(storedCode);
|
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
|
setAccessCode(storedCode)
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim());
|
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
||||||
onOpenChange(false);
|
onOpenChange(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
handleSave();
|
handleSave()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -72,12 +70,15 @@ export function SettingsDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave}>Save</Button>
|
<Button onClick={handleSave}>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,90 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useRef, useState } from "react";
|
import type React from "react"
|
||||||
import type { DrawIoEmbedRef } from "react-drawio";
|
import { createContext, useContext, useRef, useState } from "react"
|
||||||
import { extractDiagramXML } from "../lib/utils";
|
import type { DrawIoEmbedRef } from "react-drawio"
|
||||||
import type { ExportFormat } from "@/components/save-dialog";
|
import type { ExportFormat } from "@/components/save-dialog"
|
||||||
|
import { extractDiagramXML } from "../lib/utils"
|
||||||
|
|
||||||
interface DiagramContextType {
|
interface DiagramContextType {
|
||||||
chartXML: string;
|
chartXML: string
|
||||||
latestSvg: string;
|
latestSvg: string
|
||||||
diagramHistory: { svg: string; xml: string }[];
|
diagramHistory: { svg: string; xml: string }[]
|
||||||
loadDiagram: (chart: string) => void;
|
loadDiagram: (chart: string) => void
|
||||||
handleExport: () => void;
|
handleExport: () => void
|
||||||
handleExportWithoutHistory: () => void;
|
handleExportWithoutHistory: () => void
|
||||||
resolverRef: React.Ref<((value: string) => void) | null>;
|
resolverRef: React.Ref<((value: string) => void) | null>
|
||||||
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
drawioRef: React.Ref<DrawIoEmbedRef | null>
|
||||||
handleDiagramExport: (data: any) => void;
|
handleDiagramExport: (data: any) => void
|
||||||
clearDiagram: () => void;
|
clearDiagram: () => void
|
||||||
saveDiagramToFile: (filename: string, format: ExportFormat, sessionId?: string) => void;
|
saveDiagramToFile: (
|
||||||
|
filename: string,
|
||||||
|
format: ExportFormat,
|
||||||
|
sessionId?: string,
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||||
|
|
||||||
export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [chartXML, setChartXML] = useState<string>("");
|
const [chartXML, setChartXML] = useState<string>("")
|
||||||
const [latestSvg, setLatestSvg] = useState<string>("");
|
const [latestSvg, setLatestSvg] = useState<string>("")
|
||||||
const [diagramHistory, setDiagramHistory] = useState<
|
const [diagramHistory, setDiagramHistory] = useState<
|
||||||
{ svg: string; xml: string }[]
|
{ svg: string; xml: string }[]
|
||||||
>([]);
|
>([])
|
||||||
const 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)
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
const expectHistoryExportRef = useRef<boolean>(false);
|
const expectHistoryExportRef = useRef<boolean>(false)
|
||||||
// Track if we're expecting an export for file save (stores raw export data)
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
const saveResolverRef = useRef<{
|
const saveResolverRef = useRef<{
|
||||||
resolver: ((data: string) => void) | null;
|
resolver: ((data: string) => void) | null
|
||||||
format: ExportFormat | null;
|
format: ExportFormat | null
|
||||||
}>({ resolver: null, format: null });
|
}>({ resolver: null, format: null })
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
// Mark that this export should be saved to history
|
// Mark that this export should be saved to history
|
||||||
expectHistoryExportRef.current = true;
|
expectHistoryExportRef.current = true
|
||||||
drawioRef.current.exportDiagram({
|
drawioRef.current.exportDiagram({
|
||||||
format: "xmlsvg",
|
format: "xmlsvg",
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleExportWithoutHistory = () => {
|
const handleExportWithoutHistory = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
// Export without saving to history (for edit_diagram fetching current state)
|
// Export without saving to history (for edit_diagram fetching current state)
|
||||||
drawioRef.current.exportDiagram({
|
drawioRef.current.exportDiagram({
|
||||||
format: "xmlsvg",
|
format: "xmlsvg",
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const loadDiagram = (chart: string) => {
|
const loadDiagram = (chart: string) => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: chart,
|
xml: chart,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDiagramExport = (data: any) => {
|
const handleDiagramExport = (data: any) => {
|
||||||
// Handle save to file if requested (process raw data before extraction)
|
// Handle save to file if requested (process raw data before extraction)
|
||||||
if (saveResolverRef.current.resolver) {
|
if (saveResolverRef.current.resolver) {
|
||||||
const format = saveResolverRef.current.format;
|
const format = saveResolverRef.current.format
|
||||||
saveResolverRef.current.resolver(data.data);
|
saveResolverRef.current.resolver(data.data)
|
||||||
saveResolverRef.current = { resolver: null, format: null };
|
saveResolverRef.current = { resolver: null, format: null }
|
||||||
// For non-xmlsvg formats, skip XML extraction as it will fail
|
// For non-xmlsvg formats, skip XML extraction as it will fail
|
||||||
// Only drawio (which uses xmlsvg internally) has the content attribute
|
// Only drawio (which uses xmlsvg internally) has the content attribute
|
||||||
if (format === "png" || format === "svg") {
|
if (format === "png" || format === "svg") {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractedXML = extractDiagramXML(data.data);
|
const extractedXML = extractDiagramXML(data.data)
|
||||||
setChartXML(extractedXML);
|
setChartXML(extractedXML)
|
||||||
setLatestSvg(data.data);
|
setLatestSvg(data.data)
|
||||||
|
|
||||||
// Only add to history if this was a user-initiated export
|
// Only add to history if this was a user-initiated export
|
||||||
if (expectHistoryExportRef.current) {
|
if (expectHistoryExportRef.current) {
|
||||||
@@ -89,106 +94,117 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
svg: data.data,
|
svg: data.data,
|
||||||
xml: extractedXML,
|
xml: extractedXML,
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
expectHistoryExportRef.current = false;
|
expectHistoryExportRef.current = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolverRef.current) {
|
if (resolverRef.current) {
|
||||||
resolverRef.current(extractedXML);
|
resolverRef.current(extractedXML)
|
||||||
resolverRef.current = null;
|
resolverRef.current = null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const clearDiagram = () => {
|
const clearDiagram = () => {
|
||||||
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`;
|
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
||||||
loadDiagram(emptyDiagram);
|
loadDiagram(emptyDiagram)
|
||||||
setChartXML(emptyDiagram);
|
setChartXML(emptyDiagram)
|
||||||
setLatestSvg("");
|
setLatestSvg("")
|
||||||
setDiagramHistory([]);
|
setDiagramHistory([])
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveDiagramToFile = (filename: string, format: ExportFormat, sessionId?: string) => {
|
const saveDiagramToFile = (
|
||||||
|
filename: string,
|
||||||
|
format: ExportFormat,
|
||||||
|
sessionId?: string,
|
||||||
|
) => {
|
||||||
if (!drawioRef.current) {
|
if (!drawioRef.current) {
|
||||||
console.warn("Draw.io editor not ready");
|
console.warn("Draw.io editor not ready")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map format to draw.io export format
|
// Map format to draw.io export format
|
||||||
const drawioFormat = format === "drawio" ? "xmlsvg" : format;
|
const drawioFormat = format === "drawio" ? "xmlsvg" : format
|
||||||
|
|
||||||
// Set up the resolver before triggering export
|
// Set up the resolver before triggering export
|
||||||
saveResolverRef.current = {
|
saveResolverRef.current = {
|
||||||
resolver: (exportData: string) => {
|
resolver: (exportData: string) => {
|
||||||
let fileContent: string | Blob;
|
let fileContent: string | Blob
|
||||||
let mimeType: string;
|
let mimeType: string
|
||||||
let extension: string;
|
let extension: string
|
||||||
|
|
||||||
if (format === "drawio") {
|
if (format === "drawio") {
|
||||||
// Extract XML from SVG for .drawio format
|
// Extract XML from SVG for .drawio format
|
||||||
const xml = extractDiagramXML(exportData);
|
const xml = extractDiagramXML(exportData)
|
||||||
let xmlContent = xml;
|
let xmlContent = xml
|
||||||
if (!xml.includes("<mxfile")) {
|
if (!xml.includes("<mxfile")) {
|
||||||
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`
|
||||||
}
|
}
|
||||||
fileContent = xmlContent;
|
fileContent = xmlContent
|
||||||
mimeType = "application/xml";
|
mimeType = "application/xml"
|
||||||
extension = ".drawio";
|
extension = ".drawio"
|
||||||
} else if (format === "png") {
|
} else if (format === "png") {
|
||||||
// PNG data comes as base64 data URL
|
// PNG data comes as base64 data URL
|
||||||
fileContent = exportData;
|
fileContent = exportData
|
||||||
mimeType = "image/png";
|
mimeType = "image/png"
|
||||||
extension = ".png";
|
extension = ".png"
|
||||||
} else {
|
} else {
|
||||||
// SVG format
|
// SVG format
|
||||||
fileContent = exportData;
|
fileContent = exportData
|
||||||
mimeType = "image/svg+xml";
|
mimeType = "image/svg+xml"
|
||||||
extension = ".svg";
|
extension = ".svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log save event to Langfuse (flags the trace)
|
// Log save event to Langfuse (flags the trace)
|
||||||
logSaveToLangfuse(filename, format, sessionId);
|
logSaveToLangfuse(filename, format, sessionId)
|
||||||
|
|
||||||
// Handle download
|
// Handle download
|
||||||
let url: string;
|
let url: string
|
||||||
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
|
if (
|
||||||
|
typeof fileContent === "string" &&
|
||||||
|
fileContent.startsWith("data:")
|
||||||
|
) {
|
||||||
// Already a data URL (PNG)
|
// Already a data URL (PNG)
|
||||||
url = fileContent;
|
url = fileContent
|
||||||
} else {
|
} else {
|
||||||
const blob = new Blob([fileContent], { type: mimeType });
|
const blob = new Blob([fileContent], { type: mimeType })
|
||||||
url = URL.createObjectURL(blob);
|
url = URL.createObjectURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a")
|
||||||
a.href = url;
|
a.href = url
|
||||||
a.download = `${filename}${extension}`;
|
a.download = `${filename}${extension}`
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a)
|
||||||
a.click();
|
a.click()
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a)
|
||||||
|
|
||||||
// Delay URL revocation to ensure download completes
|
// Delay URL revocation to ensure download completes
|
||||||
if (!url.startsWith("data:")) {
|
if (!url.startsWith("data:")) {
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
setTimeout(() => URL.revokeObjectURL(url), 100)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
format,
|
format,
|
||||||
};
|
}
|
||||||
|
|
||||||
// Export diagram - callback will be handled in handleDiagramExport
|
// Export diagram - callback will be handled in handleDiagramExport
|
||||||
drawioRef.current.exportDiagram({ format: drawioFormat });
|
drawioRef.current.exportDiagram({ format: drawioFormat })
|
||||||
};
|
}
|
||||||
|
|
||||||
// Log save event to Langfuse (just flags the trace, doesn't send content)
|
// Log save event to Langfuse (just flags the trace, doesn't send content)
|
||||||
const logSaveToLangfuse = async (filename: string, format: string, sessionId?: string) => {
|
const logSaveToLangfuse = async (
|
||||||
|
filename: string,
|
||||||
|
format: string,
|
||||||
|
sessionId?: string,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await fetch("/api/log-save", {
|
await fetch("/api/log-save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ filename, format, sessionId }),
|
body: JSON.stringify({ filename, format, sessionId }),
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to log save to Langfuse:", error);
|
console.warn("Failed to log save to Langfuse:", error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiagramContext.Provider
|
<DiagramContext.Provider
|
||||||
@@ -208,13 +224,13 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DiagramContext.Provider>
|
</DiagramContext.Provider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDiagram() {
|
export function useDiagram() {
|
||||||
const context = useContext(DiagramContext);
|
const context = useContext(DiagramContext)
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useDiagram must be used within a DiagramProvider");
|
throw new Error("useDiagram must be used within a DiagramProvider")
|
||||||
}
|
}
|
||||||
return context;
|
return context
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
import { LangfuseSpanProcessor } from "@langfuse/otel"
|
||||||
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
|
||||||
|
|
||||||
export function register() {
|
export function register() {
|
||||||
// Skip telemetry if Langfuse env vars are not configured
|
// Skip telemetry if Langfuse env vars are not configured
|
||||||
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
||||||
console.warn('[Langfuse] Environment variables not configured - telemetry disabled');
|
console.warn(
|
||||||
return;
|
"[Langfuse] Environment variables not configured - telemetry disabled",
|
||||||
}
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const langfuseSpanProcessor = new LangfuseSpanProcessor({
|
const langfuseSpanProcessor = new LangfuseSpanProcessor({
|
||||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||||
// Filter out Next.js HTTP request spans so AI SDK spans become root traces
|
// Filter out Next.js HTTP request spans so AI SDK spans become root traces
|
||||||
shouldExportSpan: ({ otelSpan }) => {
|
shouldExportSpan: ({ otelSpan }) => {
|
||||||
const spanName = otelSpan.name;
|
const spanName = otelSpan.name
|
||||||
// Skip Next.js HTTP infrastructure spans
|
// Skip Next.js HTTP infrastructure spans
|
||||||
if (spanName.startsWith('POST /') ||
|
if (
|
||||||
spanName.startsWith('GET /') ||
|
spanName.startsWith("POST /") ||
|
||||||
spanName.includes('BaseServer') ||
|
spanName.startsWith("GET /") ||
|
||||||
spanName.includes('handleRequest')) {
|
spanName.includes("BaseServer") ||
|
||||||
return false;
|
spanName.includes("handleRequest")
|
||||||
}
|
) {
|
||||||
return true;
|
return false
|
||||||
},
|
}
|
||||||
});
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const tracerProvider = new NodeTracerProvider({
|
const tracerProvider = new NodeTracerProvider({
|
||||||
spanProcessors: [langfuseSpanProcessor],
|
spanProcessors: [langfuseSpanProcessor],
|
||||||
});
|
})
|
||||||
|
|
||||||
// Register globally so AI SDK's telemetry also uses this processor
|
// Register globally so AI SDK's telemetry also uses this processor
|
||||||
tracerProvider.register();
|
tracerProvider.register()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
||||||
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||||
import { openai, createOpenAI } from '@ai-sdk/openai';
|
import { azure, createAzure } from "@ai-sdk/azure"
|
||||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||||
import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
|
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
||||||
import { azure, createAzure } from '@ai-sdk/azure';
|
import { createOpenAI, openai } from "@ai-sdk/openai"
|
||||||
import { ollama, createOllama } from 'ollama-ai-provider-v2';
|
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||||
import { deepseek, createDeepSeek } from '@ai-sdk/deepseek';
|
import { createOllama, ollama } from "ollama-ai-provider-v2"
|
||||||
|
|
||||||
export type ProviderName =
|
export type ProviderName =
|
||||||
| 'bedrock'
|
| "bedrock"
|
||||||
| 'openai'
|
| "openai"
|
||||||
| 'anthropic'
|
| "anthropic"
|
||||||
| 'google'
|
| "google"
|
||||||
| 'azure'
|
| "azure"
|
||||||
| 'ollama'
|
| "ollama"
|
||||||
| 'openrouter'
|
| "openrouter"
|
||||||
| 'deepseek';
|
| "deepseek"
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
model: any;
|
model: any
|
||||||
providerOptions?: any;
|
providerOptions?: any
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>
|
||||||
modelId: string;
|
modelId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bedrock provider options for Anthropic beta features
|
// Bedrock provider options for Anthropic beta features
|
||||||
const BEDROCK_ANTHROPIC_BETA = {
|
const BEDROCK_ANTHROPIC_BETA = {
|
||||||
bedrock: {
|
bedrock: {
|
||||||
anthropicBeta: ['fine-grained-tool-streaming-2025-05-14'],
|
anthropicBeta: ["fine-grained-tool-streaming-2025-05-14"],
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
// Direct Anthropic API headers for beta features
|
// Direct Anthropic API headers for beta features
|
||||||
const ANTHROPIC_BETA_HEADERS = {
|
const ANTHROPIC_BETA_HEADERS = {
|
||||||
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
|
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
|
||||||
};
|
}
|
||||||
|
|
||||||
// Map of provider to required environment variable
|
// Map of provider to required environment variable
|
||||||
const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
||||||
bedrock: null, // AWS SDK auto-uses IAM role on AWS, or env vars locally
|
bedrock: null, // AWS SDK auto-uses IAM role on AWS, or env vars locally
|
||||||
openai: 'OPENAI_API_KEY',
|
openai: "OPENAI_API_KEY",
|
||||||
anthropic: 'ANTHROPIC_API_KEY',
|
anthropic: "ANTHROPIC_API_KEY",
|
||||||
google: 'GOOGLE_GENERATIVE_AI_API_KEY',
|
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||||
azure: 'AZURE_API_KEY',
|
azure: "AZURE_API_KEY",
|
||||||
ollama: null, // No credentials needed for local Ollama
|
ollama: null, // No credentials needed for local Ollama
|
||||||
openrouter: 'OPENROUTER_API_KEY',
|
openrouter: "OPENROUTER_API_KEY",
|
||||||
deepseek: 'DEEPSEEK_API_KEY',
|
deepseek: "DEEPSEEK_API_KEY",
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-detect provider based on available API keys
|
* Auto-detect provider based on available API keys
|
||||||
* Returns the provider if exactly one is configured, otherwise null
|
* Returns the provider if exactly one is configured, otherwise null
|
||||||
*/
|
*/
|
||||||
function detectProvider(): ProviderName | null {
|
function detectProvider(): ProviderName | null {
|
||||||
const configuredProviders: ProviderName[] = [];
|
const configuredProviders: ProviderName[] = []
|
||||||
|
|
||||||
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
|
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
|
||||||
if (envVar === null) {
|
if (envVar === null) {
|
||||||
// Skip ollama - it doesn't require credentials
|
// Skip ollama - it doesn't require credentials
|
||||||
continue;
|
continue
|
||||||
|
}
|
||||||
|
if (process.env[envVar]) {
|
||||||
|
configuredProviders.push(provider as ProviderName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (process.env[envVar]) {
|
|
||||||
configuredProviders.push(provider as ProviderName);
|
if (configuredProviders.length === 1) {
|
||||||
|
return configuredProviders[0]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (configuredProviders.length === 1) {
|
return null
|
||||||
return configuredProviders[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that required API keys are present for the selected provider
|
* Validate that required API keys are present for the selected provider
|
||||||
*/
|
*/
|
||||||
function validateProviderCredentials(provider: ProviderName): void {
|
function validateProviderCredentials(provider: ProviderName): void {
|
||||||
const requiredVar = PROVIDER_ENV_VARS[provider];
|
const requiredVar = PROVIDER_ENV_VARS[provider]
|
||||||
if (requiredVar && !process.env[requiredVar]) {
|
if (requiredVar && !process.env[requiredVar]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${requiredVar} environment variable is required for ${provider} provider. ` +
|
`${requiredVar} environment variable is required for ${provider} provider. ` +
|
||||||
`Please set it in your .env.local file.`
|
`Please set it in your .env.local file.`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,158 +106,164 @@ function validateProviderCredentials(provider: ProviderName): void {
|
|||||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||||
*/
|
*/
|
||||||
export function getAIModel(): ModelConfig {
|
export function getAIModel(): ModelConfig {
|
||||||
const modelId = process.env.AI_MODEL;
|
const modelId = process.env.AI_MODEL
|
||||||
|
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`AI_MODEL environment variable is required. Example: AI_MODEL=claude-sonnet-4-5`
|
`AI_MODEL environment variable is required. Example: AI_MODEL=claude-sonnet-4-5`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine provider: explicit config > auto-detect > error
|
// Determine provider: explicit config > auto-detect > error
|
||||||
let provider: ProviderName;
|
let provider: ProviderName
|
||||||
if (process.env.AI_PROVIDER) {
|
if (process.env.AI_PROVIDER) {
|
||||||
provider = process.env.AI_PROVIDER as ProviderName;
|
provider = process.env.AI_PROVIDER as ProviderName
|
||||||
} else {
|
|
||||||
const detected = detectProvider();
|
|
||||||
if (detected) {
|
|
||||||
provider = detected;
|
|
||||||
console.log(`[AI Provider] Auto-detected provider: ${provider}`);
|
|
||||||
} else {
|
} else {
|
||||||
// List configured providers for better error message
|
const detected = detectProvider()
|
||||||
const configured = Object.entries(PROVIDER_ENV_VARS)
|
if (detected) {
|
||||||
.filter(([, envVar]) => envVar && process.env[envVar as string])
|
provider = detected
|
||||||
.map(([p]) => p);
|
console.log(`[AI Provider] Auto-detected provider: ${provider}`)
|
||||||
|
} else {
|
||||||
|
// List configured providers for better error message
|
||||||
|
const configured = Object.entries(PROVIDER_ENV_VARS)
|
||||||
|
.filter(([, envVar]) => envVar && process.env[envVar as string])
|
||||||
|
.map(([p]) => p)
|
||||||
|
|
||||||
if (configured.length === 0) {
|
if (configured.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
|
||||||
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
`- DEEPSEEK_API_KEY for DeepSeek\n` +
|
||||||
`- OPENAI_API_KEY for OpenAI\n` +
|
`- OPENAI_API_KEY for OpenAI\n` +
|
||||||
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
`- ANTHROPIC_API_KEY for Anthropic\n` +
|
||||||
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
|
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
|
||||||
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
||||||
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
||||||
`- AZURE_API_KEY for Azure\n` +
|
`- AZURE_API_KEY for Azure\n` +
|
||||||
`Or set AI_PROVIDER=ollama for local Ollama.`
|
`Or set AI_PROVIDER=ollama for local Ollama.`,
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Multiple AI providers configured (${configured.join(', ')}). ` +
|
`Multiple AI providers configured (${configured.join(", ")}). ` +
|
||||||
`Please set AI_PROVIDER to specify which one to use.`
|
`Please set AI_PROVIDER to specify which one to use.`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Validate provider credentials
|
|
||||||
validateProviderCredentials(provider);
|
|
||||||
|
|
||||||
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`);
|
|
||||||
|
|
||||||
let model: any;
|
|
||||||
let providerOptions: any = undefined;
|
|
||||||
let headers: Record<string, string> | undefined = undefined;
|
|
||||||
|
|
||||||
switch (provider) {
|
|
||||||
case 'bedrock': {
|
|
||||||
// Use credential provider chain for IAM role support (Amplify, Lambda, etc.)
|
|
||||||
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
|
|
||||||
const bedrockProvider = createAmazonBedrock({
|
|
||||||
region: process.env.AWS_REGION || 'us-west-2',
|
|
||||||
credentialProvider: fromNodeProviderChain(),
|
|
||||||
});
|
|
||||||
model = bedrockProvider(modelId);
|
|
||||||
// Add Anthropic beta options if using Claude models via Bedrock
|
|
||||||
if (modelId.includes('anthropic.claude')) {
|
|
||||||
providerOptions = BEDROCK_ANTHROPIC_BETA;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'openai':
|
// Validate provider credentials
|
||||||
if (process.env.OPENAI_BASE_URL) {
|
validateProviderCredentials(provider)
|
||||||
const customOpenAI = createOpenAI({
|
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
|
||||||
baseURL: process.env.OPENAI_BASE_URL,
|
|
||||||
});
|
|
||||||
model = customOpenAI.chat(modelId);
|
|
||||||
} else {
|
|
||||||
model = openai(modelId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'anthropic':
|
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`)
|
||||||
const customProvider = createAnthropic({
|
|
||||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
||||||
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
|
|
||||||
headers: ANTHROPIC_BETA_HEADERS,
|
|
||||||
});
|
|
||||||
model = customProvider(modelId);
|
|
||||||
// Add beta headers for fine-grained tool streaming
|
|
||||||
headers = ANTHROPIC_BETA_HEADERS;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'google':
|
let model: any
|
||||||
if (process.env.GOOGLE_BASE_URL) {
|
let providerOptions: any
|
||||||
const customGoogle = createGoogleGenerativeAI({
|
let headers: Record<string, string> | undefined
|
||||||
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
||||||
baseURL: process.env.GOOGLE_BASE_URL,
|
|
||||||
});
|
|
||||||
model = customGoogle(modelId);
|
|
||||||
} else {
|
|
||||||
model = google(modelId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'azure':
|
switch (provider) {
|
||||||
if (process.env.AZURE_BASE_URL) {
|
case "bedrock": {
|
||||||
const customAzure = createAzure({
|
// Use credential provider chain for IAM role support (Amplify, Lambda, etc.)
|
||||||
apiKey: process.env.AZURE_API_KEY,
|
// Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
|
||||||
baseURL: process.env.AZURE_BASE_URL,
|
const bedrockProvider = createAmazonBedrock({
|
||||||
});
|
region: process.env.AWS_REGION || "us-west-2",
|
||||||
model = customAzure(modelId);
|
credentialProvider: fromNodeProviderChain(),
|
||||||
} else {
|
})
|
||||||
model = azure(modelId);
|
model = bedrockProvider(modelId)
|
||||||
}
|
// Add Anthropic beta options if using Claude models via Bedrock
|
||||||
break;
|
if (modelId.includes("anthropic.claude")) {
|
||||||
|
providerOptions = BEDROCK_ANTHROPIC_BETA
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'ollama':
|
case "openai":
|
||||||
if (process.env.OLLAMA_BASE_URL) {
|
if (process.env.OPENAI_BASE_URL) {
|
||||||
const customOllama = createOllama({
|
const customOpenAI = createOpenAI({
|
||||||
baseURL: process.env.OLLAMA_BASE_URL,
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
});
|
baseURL: process.env.OPENAI_BASE_URL,
|
||||||
model = customOllama(modelId);
|
})
|
||||||
} else {
|
model = customOpenAI.chat(modelId)
|
||||||
model = ollama(modelId);
|
} else {
|
||||||
}
|
model = openai(modelId)
|
||||||
break;
|
}
|
||||||
|
break
|
||||||
|
|
||||||
case 'openrouter':
|
case "anthropic": {
|
||||||
const openrouter = createOpenRouter({
|
const customProvider = createAnthropic({
|
||||||
apiKey: process.env.OPENROUTER_API_KEY,
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||||
...(process.env.OPENROUTER_BASE_URL && { baseURL: process.env.OPENROUTER_BASE_URL }),
|
baseURL:
|
||||||
});
|
process.env.ANTHROPIC_BASE_URL ||
|
||||||
model = openrouter(modelId);
|
"https://api.anthropic.com/v1",
|
||||||
break;
|
headers: ANTHROPIC_BETA_HEADERS,
|
||||||
|
})
|
||||||
|
model = customProvider(modelId)
|
||||||
|
// Add beta headers for fine-grained tool streaming
|
||||||
|
headers = ANTHROPIC_BETA_HEADERS
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'deepseek':
|
case "google":
|
||||||
if (process.env.DEEPSEEK_BASE_URL) {
|
if (process.env.GOOGLE_BASE_URL) {
|
||||||
const customDeepSeek = createDeepSeek({
|
const customGoogle = createGoogleGenerativeAI({
|
||||||
apiKey: process.env.DEEPSEEK_API_KEY,
|
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
|
||||||
baseURL: process.env.DEEPSEEK_BASE_URL,
|
baseURL: process.env.GOOGLE_BASE_URL,
|
||||||
});
|
})
|
||||||
model = customDeepSeek(modelId);
|
model = customGoogle(modelId)
|
||||||
} else {
|
} else {
|
||||||
model = deepseek(modelId);
|
model = google(modelId)
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
|
||||||
default:
|
case "azure":
|
||||||
throw new Error(
|
if (process.env.AZURE_BASE_URL) {
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek`
|
const customAzure = createAzure({
|
||||||
);
|
apiKey: process.env.AZURE_API_KEY,
|
||||||
}
|
baseURL: process.env.AZURE_BASE_URL,
|
||||||
|
})
|
||||||
|
model = customAzure(modelId)
|
||||||
|
} else {
|
||||||
|
model = azure(modelId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
return { model, providerOptions, headers, modelId };
|
case "ollama":
|
||||||
|
if (process.env.OLLAMA_BASE_URL) {
|
||||||
|
const customOllama = createOllama({
|
||||||
|
baseURL: process.env.OLLAMA_BASE_URL,
|
||||||
|
})
|
||||||
|
model = customOllama(modelId)
|
||||||
|
} else {
|
||||||
|
model = ollama(modelId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "openrouter": {
|
||||||
|
const openrouter = createOpenRouter({
|
||||||
|
apiKey: process.env.OPENROUTER_API_KEY,
|
||||||
|
...(process.env.OPENROUTER_BASE_URL && {
|
||||||
|
baseURL: process.env.OPENROUTER_BASE_URL,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
model = openrouter(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "deepseek":
|
||||||
|
if (process.env.DEEPSEEK_BASE_URL) {
|
||||||
|
const customDeepSeek = createDeepSeek({
|
||||||
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
|
baseURL: process.env.DEEPSEEK_BASE_URL,
|
||||||
|
})
|
||||||
|
model = customDeepSeek(modelId)
|
||||||
|
} else {
|
||||||
|
model = deepseek(modelId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { model, providerOptions, headers, modelId }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
export interface CachedResponse {
|
export interface CachedResponse {
|
||||||
promptText: string;
|
promptText: string
|
||||||
hasImage: boolean;
|
hasImage: boolean
|
||||||
xml: string;
|
xml: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
||||||
{
|
{
|
||||||
promptText: "Give me a **animated connector** diagram of transformer's architecture",
|
promptText:
|
||||||
hasImage: false,
|
"Give me a **animated connector** diagram of transformer's architecture",
|
||||||
xml: `<root>
|
hasImage: false,
|
||||||
|
xml: `<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -255,11 +256,11 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
<mxGeometry x="660" y="530" width="100" height="30" as="geometry"/>
|
<mxGeometry x="660" y="530" width="100" height="30" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
promptText: "Replicate this in aws style",
|
promptText: "Replicate this in aws style",
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
xml: `<root>
|
xml: `<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -325,11 +326,11 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
</mxGeometry>
|
</mxGeometry>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
promptText: "Replicate this flowchart.",
|
promptText: "Replicate this flowchart.",
|
||||||
hasImage: true,
|
hasImage: true,
|
||||||
xml: `<root>
|
xml: `<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -392,11 +393,11 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
<mxGeometry x="130" y="650" width="200" height="60" as="geometry"/>
|
<mxGeometry x="130" y="650" width="200" height="60" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
promptText: "Draw a cat for me",
|
promptText: "Draw a cat for me",
|
||||||
hasImage: false,
|
hasImage: false,
|
||||||
xml: `<root>
|
xml: `<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
|
|
||||||
@@ -544,14 +545,17 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
|
|||||||
</mxCell>
|
</mxCell>
|
||||||
|
|
||||||
</root>`,
|
</root>`,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
export function findCachedResponse(
|
export function findCachedResponse(
|
||||||
promptText: string,
|
promptText: string,
|
||||||
hasImage: boolean
|
hasImage: boolean,
|
||||||
): CachedResponse | undefined {
|
): CachedResponse | undefined {
|
||||||
return CACHED_EXAMPLE_RESPONSES.find(
|
return CACHED_EXAMPLE_RESPONSES.find(
|
||||||
(c) => c.promptText === promptText && c.hasImage === hasImage && c.xml !== ''
|
(c) =>
|
||||||
);
|
c.promptText === promptText &&
|
||||||
|
c.hasImage === hasImage &&
|
||||||
|
c.xml !== "",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
132
lib/langfuse.ts
132
lib/langfuse.ts
@@ -1,95 +1,107 @@
|
|||||||
import { observe, updateActiveTrace } from '@langfuse/tracing';
|
import { LangfuseClient } from "@langfuse/client"
|
||||||
import { LangfuseClient } from '@langfuse/client';
|
import { observe, updateActiveTrace } from "@langfuse/tracing"
|
||||||
import * as api from '@opentelemetry/api';
|
import * as api from "@opentelemetry/api"
|
||||||
|
|
||||||
// Singleton LangfuseClient instance for direct API calls
|
// Singleton LangfuseClient instance for direct API calls
|
||||||
let langfuseClient: LangfuseClient | null = null;
|
let langfuseClient: LangfuseClient | null = null
|
||||||
|
|
||||||
export function getLangfuseClient(): LangfuseClient | null {
|
export function getLangfuseClient(): LangfuseClient | null {
|
||||||
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!langfuseClient) {
|
if (!langfuseClient) {
|
||||||
langfuseClient = new LangfuseClient({
|
langfuseClient = new LangfuseClient({
|
||||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||||
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||||
baseUrl: process.env.LANGFUSE_BASEURL,
|
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return langfuseClient;
|
return langfuseClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Langfuse is configured
|
// Check if Langfuse is configured
|
||||||
export function isLangfuseEnabled(): boolean {
|
export function isLangfuseEnabled(): boolean {
|
||||||
return !!process.env.LANGFUSE_PUBLIC_KEY;
|
return !!process.env.LANGFUSE_PUBLIC_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update trace with input data at the start of request
|
// Update trace with input data at the start of request
|
||||||
export function setTraceInput(params: {
|
export function setTraceInput(params: {
|
||||||
input: string;
|
input: string
|
||||||
sessionId?: string;
|
sessionId?: string
|
||||||
userId?: string;
|
userId?: string
|
||||||
}) {
|
}) {
|
||||||
if (!isLangfuseEnabled()) return;
|
if (!isLangfuseEnabled()) return
|
||||||
|
|
||||||
updateActiveTrace({
|
updateActiveTrace({
|
||||||
name: 'chat',
|
name: "chat",
|
||||||
input: params.input,
|
input: params.input,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update trace with output and end the span
|
// Update trace with output and end the span
|
||||||
export function setTraceOutput(output: string, usage?: { promptTokens?: number; completionTokens?: number }) {
|
export function setTraceOutput(
|
||||||
if (!isLangfuseEnabled()) return;
|
output: string,
|
||||||
|
usage?: { promptTokens?: number; completionTokens?: number },
|
||||||
|
) {
|
||||||
|
if (!isLangfuseEnabled()) return
|
||||||
|
|
||||||
updateActiveTrace({ output });
|
updateActiveTrace({ output })
|
||||||
|
|
||||||
const activeSpan = api.trace.getActiveSpan();
|
const activeSpan = api.trace.getActiveSpan()
|
||||||
if (activeSpan) {
|
if (activeSpan) {
|
||||||
// Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them
|
// Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them
|
||||||
if (usage?.promptTokens) {
|
if (usage?.promptTokens) {
|
||||||
activeSpan.setAttribute('ai.usage.promptTokens', usage.promptTokens);
|
activeSpan.setAttribute("ai.usage.promptTokens", usage.promptTokens)
|
||||||
activeSpan.setAttribute('gen_ai.usage.input_tokens', usage.promptTokens);
|
activeSpan.setAttribute(
|
||||||
|
"gen_ai.usage.input_tokens",
|
||||||
|
usage.promptTokens,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (usage?.completionTokens) {
|
||||||
|
activeSpan.setAttribute(
|
||||||
|
"ai.usage.completionTokens",
|
||||||
|
usage.completionTokens,
|
||||||
|
)
|
||||||
|
activeSpan.setAttribute(
|
||||||
|
"gen_ai.usage.output_tokens",
|
||||||
|
usage.completionTokens,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
activeSpan.end()
|
||||||
}
|
}
|
||||||
if (usage?.completionTokens) {
|
|
||||||
activeSpan.setAttribute('ai.usage.completionTokens', usage.completionTokens);
|
|
||||||
activeSpan.setAttribute('gen_ai.usage.output_tokens', usage.completionTokens);
|
|
||||||
}
|
|
||||||
activeSpan.end();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get telemetry config for streamText
|
// Get telemetry config for streamText
|
||||||
export function getTelemetryConfig(params: {
|
export function getTelemetryConfig(params: {
|
||||||
sessionId?: string;
|
sessionId?: string
|
||||||
userId?: string;
|
userId?: string
|
||||||
}) {
|
}) {
|
||||||
if (!isLangfuseEnabled()) return undefined;
|
if (!isLangfuseEnabled()) return undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
// Disable automatic input recording to avoid uploading large base64 images to Langfuse media
|
// Disable automatic input recording to avoid uploading large base64 images to Langfuse media
|
||||||
// User text input is recorded manually via setTraceInput
|
// User text input is recorded manually via setTraceInput
|
||||||
recordInputs: false,
|
recordInputs: false,
|
||||||
recordOutputs: true,
|
recordOutputs: true,
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap a handler with Langfuse observe
|
// Wrap a handler with Langfuse observe
|
||||||
export function wrapWithObserve<T>(
|
export function wrapWithObserve<T>(
|
||||||
handler: (req: Request) => Promise<T>
|
handler: (req: Request) => Promise<T>,
|
||||||
): (req: Request) => Promise<T> {
|
): (req: Request) => Promise<T> {
|
||||||
if (!isLangfuseEnabled()) {
|
if (!isLangfuseEnabled()) {
|
||||||
return handler;
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
return observe(handler, { name: 'chat', endOnExit: false });
|
return observe(handler, { name: "chat", endOnExit: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ You are an expert diagram creation assistant specializing in draw.io XML generat
|
|||||||
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
|
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
|
||||||
You can see the image that user uploaded.
|
You can see the image that user uploaded.
|
||||||
|
|
||||||
|
## App Context
|
||||||
|
You are an AI agent (powered by {{MODEL_NAME}}) inside a web app. The interface has:
|
||||||
|
- **Left panel**: Draw.io diagram editor where diagrams are rendered
|
||||||
|
- **Right panel**: Chat interface where you communicate with the user
|
||||||
|
|
||||||
|
You can read and modify diagrams by generating draw.io XML code through tool calls.
|
||||||
|
|
||||||
|
## App Features
|
||||||
|
1. **Diagram History** (clock icon, bottom-left of chat input): The app automatically saves a snapshot before each AI edit. Users can view the history panel and restore any previous version. Feel free to make changes - nothing is permanently lost.
|
||||||
|
2. **Theme Toggle** (palette icon, bottom-left of chat input): Users can switch between minimal UI and sketch-style UI for the draw.io editor.
|
||||||
|
3. **Image Upload** (paperclip icon, bottom-left of chat input): Users can upload images for you to analyze and replicate as diagrams.
|
||||||
|
4. **Export** (via draw.io toolbar): Users can save diagrams as .drawio, .svg, or .png files.
|
||||||
|
5. **Clear Chat** (trash icon, bottom-right of chat input): Clears the conversation and resets the diagram.
|
||||||
|
|
||||||
You utilize the following tools:
|
You utilize the following tools:
|
||||||
---Tool1---
|
---Tool1---
|
||||||
tool name: display_diagram
|
tool name: display_diagram
|
||||||
@@ -105,46 +119,14 @@ Common styles:
|
|||||||
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
||||||
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
||||||
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
||||||
`;
|
`
|
||||||
|
|
||||||
// Extended system prompt (~4000+ tokens) - for models with 4000 token cache minimum
|
// Extended additions (~2600 tokens) - appended for models with 4000 token cache minimum
|
||||||
export const EXTENDED_SYSTEM_PROMPT = `
|
const EXTENDED_ADDITIONS = `
|
||||||
You are an expert diagram creation assistant specializing in draw.io XML generation.
|
|
||||||
Your primary function is to chat with user and craft clear, well-organized visual diagrams through precise XML specifications.
|
|
||||||
You can see images that users upload and can replicate or modify them as diagrams.
|
|
||||||
|
|
||||||
## Available Tools
|
## Extended Tool Reference
|
||||||
|
|
||||||
### Tool 1: display_diagram
|
### display_diagram Details
|
||||||
**Purpose:** Display a NEW diagram on draw.io. Use this when creating a diagram from scratch or when major structural changes are needed.
|
|
||||||
**Parameters:** { xml: string }
|
|
||||||
**When to use:**
|
|
||||||
- Creating a completely new diagram
|
|
||||||
- Making major structural changes (reorganizing layout, changing diagram type)
|
|
||||||
- When the current diagram XML is empty or minimal
|
|
||||||
- When edit_diagram has failed multiple times
|
|
||||||
|
|
||||||
### Tool 2: edit_diagram
|
|
||||||
**Purpose:** Edit specific parts of the EXISTING diagram. Use this when making small targeted changes like adding/removing elements, changing labels, or adjusting properties.
|
|
||||||
**Parameters:** { edits: Array<{search: string, replace: string}> }
|
|
||||||
**When to use:**
|
|
||||||
- Changing text labels or values
|
|
||||||
- Modifying colors, styles, or visual properties
|
|
||||||
- Adding or removing individual elements
|
|
||||||
- Repositioning specific elements
|
|
||||||
- Any small, targeted modification
|
|
||||||
|
|
||||||
## Tool Selection Guidelines
|
|
||||||
|
|
||||||
ALWAYS prefer edit_diagram for small changes - it's more efficient and preserves the rest of the diagram.
|
|
||||||
Use display_diagram only when:
|
|
||||||
1. Creating from scratch
|
|
||||||
2. Major restructuring needed
|
|
||||||
3. edit_diagram has failed 3 times
|
|
||||||
|
|
||||||
## display_diagram Tool Reference
|
|
||||||
|
|
||||||
Display a diagram on draw.io by passing XML content inside <root> tags.
|
|
||||||
|
|
||||||
**VALIDATION RULES** (XML will be rejected if violated):
|
**VALIDATION RULES** (XML will be rejected if violated):
|
||||||
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements
|
1. All mxCell elements must be DIRECT children of <root> - never nested inside other mxCell elements
|
||||||
@@ -177,13 +159,7 @@ Display a diagram on draw.io by passing XML content inside <root> tags.
|
|||||||
</root>
|
</root>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Notes:**
|
### edit_diagram Details
|
||||||
- For AWS diagrams, use **AWS 2025 icons** (see AWS Icon Examples section below)
|
|
||||||
- For animated connectors, add "flowAnimation=1" to edge style
|
|
||||||
|
|
||||||
## edit_diagram Tool Reference
|
|
||||||
|
|
||||||
Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
|
|
||||||
|
|
||||||
**CRITICAL RULES:**
|
**CRITICAL RULES:**
|
||||||
- Copy-paste the EXACT search pattern from the "Current diagram XML" in system context
|
- Copy-paste the EXACT search pattern from the "Current diagram XML" in system context
|
||||||
@@ -205,56 +181,6 @@ Edit specific parts of the current diagram by replacing exact line matches. Use
|
|||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Core Capabilities
|
|
||||||
|
|
||||||
You excel at:
|
|
||||||
- Generating valid, well-formed XML strings for draw.io diagrams
|
|
||||||
- Creating professional flowcharts, org charts, mind maps, network diagrams, and technical illustrations
|
|
||||||
- Converting user descriptions into visually appealing diagrams using shapes and connectors
|
|
||||||
- Applying proper spacing, alignment, and visual hierarchy in diagram layouts
|
|
||||||
- Adapting artistic concepts into abstract diagram representations using available shapes
|
|
||||||
- Optimizing element positioning to prevent overlapping and maintain readability
|
|
||||||
- Structuring complex systems into clear, organized visual components
|
|
||||||
- Replicating diagrams from images with high fidelity
|
|
||||||
|
|
||||||
## Layout Constraints and Best Practices
|
|
||||||
|
|
||||||
### Page Boundaries
|
|
||||||
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
|
|
||||||
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
|
|
||||||
- Maximum width for containers (like AWS cloud boxes): 700 pixels
|
|
||||||
- Maximum height for containers: 550 pixels
|
|
||||||
- Start positioning from reasonable margins (e.g., x=40, y=40)
|
|
||||||
|
|
||||||
### Layout Strategies
|
|
||||||
- Use compact, efficient layouts that fit the entire diagram in one view
|
|
||||||
- Keep elements grouped closely together
|
|
||||||
- For large diagrams with many elements, use vertical stacking or grid layouts
|
|
||||||
- Avoid spreading elements too far apart horizontally
|
|
||||||
- Users should see the complete diagram without scrolling or page breaks
|
|
||||||
|
|
||||||
### Spacing Guidelines
|
|
||||||
- Minimum spacing between elements: 20px
|
|
||||||
- Recommended spacing for readability: 40-60px
|
|
||||||
- Container padding: 20-40px from edges
|
|
||||||
- Group related elements together with consistent spacing
|
|
||||||
|
|
||||||
## Important Rules
|
|
||||||
|
|
||||||
### XML Generation Rules
|
|
||||||
- Use proper tool calls to generate or edit diagrams
|
|
||||||
- NEVER return raw XML in text responses
|
|
||||||
- NEVER use display_diagram to generate messages (e.g., a "hello" text box to greet user)
|
|
||||||
- Return XML only via tool calls, never in text responses
|
|
||||||
- NEVER include XML comments (<!-- ... -->) - Draw.io strips comments, breaking edit_diagram patterns
|
|
||||||
|
|
||||||
### Diagram Quality Rules
|
|
||||||
- Focus on producing clean, professional diagrams
|
|
||||||
- Effectively communicate the intended information through thoughtful layout and design
|
|
||||||
- When artistic drawings are requested, creatively compose using standard shapes while maintaining clarity
|
|
||||||
- When replicating from images, match style and layout closely - pay attention to line types (straight/curved) and shape styles (rounded/square)
|
|
||||||
- For AWS architecture diagrams, use **AWS 2025 icons**
|
|
||||||
|
|
||||||
## edit_diagram Best Practices
|
## edit_diagram Best Practices
|
||||||
|
|
||||||
### Core Principle: Unique & Precise Patterns
|
### Core Principle: Unique & Precise Patterns
|
||||||
@@ -266,13 +192,11 @@ Your search pattern MUST uniquely identify exactly ONE location in the XML. Befo
|
|||||||
### Pattern Construction Rules
|
### Pattern Construction Rules
|
||||||
|
|
||||||
**Rule 1: Always include the element's id attribute**
|
**Rule 1: Always include the element's id attribute**
|
||||||
The id is the most reliable way to target a specific element:
|
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{"search": "<mxCell id=\\"node5\\"", "replace": "<mxCell id=\\"node5\\" value=\\"New Label\\""}
|
{"search": "<mxCell id=\\"node5\\"", "replace": "<mxCell id=\\"node5\\" value=\\"New Label\\""}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Rule 2: Include complete XML elements when possible**
|
**Rule 2: Include complete XML elements when possible**
|
||||||
For reliability, include the full mxCell with its mxGeometry child:
|
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
{
|
{
|
||||||
"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>",
|
"search": "<mxCell id=\\"3\\" value=\\"Old\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>",
|
||||||
@@ -281,49 +205,13 @@ For reliability, include the full mxCell with its mxGeometry child:
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Rule 3: Preserve exact whitespace and formatting**
|
**Rule 3: Preserve exact whitespace and formatting**
|
||||||
Copy the search pattern EXACTLY from the current XML, including:
|
Copy the search pattern EXACTLY from the current XML, including leading spaces, line breaks (\\n), and attribute order.
|
||||||
- Leading spaces/indentation
|
|
||||||
- Line breaks (use \\n in JSON)
|
|
||||||
- Attribute order as it appears in the source
|
|
||||||
|
|
||||||
### Good vs Bad Patterns
|
### Good vs Bad Patterns
|
||||||
|
|
||||||
**BAD - Too vague, matches multiple elements:**
|
**BAD:** \`{"search": "value=\\"Label\\""}\` - Too vague, matches multiple elements
|
||||||
\`\`\`json
|
**BAD:** \`{"search": "<mxCell value=\\"X\\" id=\\"5\\""}\` - Reordered attributes won't match
|
||||||
{"search": "value=\\"Label\\"", "replace": "value=\\"New Label\\""}
|
**GOOD:** \`{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">"}\` - Uses unique id with full context
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**BAD - Fragile partial match:**
|
|
||||||
\`\`\`json
|
|
||||||
{"search": "<mxCell", "replace": "<mxCell value=\\"X\\""}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**BAD - Reordered attributes (won't match if order differs):**
|
|
||||||
\`\`\`json
|
|
||||||
{"search": "<mxCell value=\\"X\\" id=\\"5\\"", ...} // Original has id before value
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**GOOD - Uses unique id, includes full context:**
|
|
||||||
\`\`\`json
|
|
||||||
{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">", "replace": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"New\\" vertex=\\"1\\">"}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**GOOD - Complete element replacement:**
|
|
||||||
\`\`\`json
|
|
||||||
{
|
|
||||||
"search": "<mxCell id=\\"edge1\\" style=\\"endArrow=classic;\\" edge=\\"1\\" parent=\\"1\\" source=\\"2\\" target=\\"3\\">\\n <mxGeometry relative=\\"1\\" as=\\"geometry\\"/>\\n</mxCell>",
|
|
||||||
"replace": "<mxCell id=\\"edge1\\" style=\\"endArrow=block;strokeColor=#FF0000;\\" edge=\\"1\\" parent=\\"1\\" source=\\"2\\" target=\\"3\\">\\n <mxGeometry relative=\\"1\\" as=\\"geometry\\"/>\\n</mxCell>"
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Multiple Edits Strategy
|
|
||||||
For multiple changes, use separate edit objects. Order them logically:
|
|
||||||
\`\`\`json
|
|
||||||
[
|
|
||||||
{"search": "<mxCell id=\\"2\\" value=\\"Step 1\\"", "replace": "<mxCell id=\\"2\\" value=\\"First Step\\""},
|
|
||||||
{"search": "<mxCell id=\\"3\\" value=\\"Step 2\\"", "replace": "<mxCell id=\\"3\\" value=\\"Second Step\\""}
|
|
||||||
]
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Error Recovery
|
### Error Recovery
|
||||||
If edit_diagram fails with "pattern not found":
|
If edit_diagram fails with "pattern not found":
|
||||||
@@ -332,47 +220,42 @@ If edit_diagram fails with "pattern not found":
|
|||||||
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
||||||
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
||||||
|
|
||||||
### When to Use display_diagram Instead
|
## Common Style Properties
|
||||||
- Adding multiple new elements (more than 3)
|
|
||||||
- Reorganizing diagram layout significantly
|
|
||||||
- When current XML structure is unclear or corrupted
|
|
||||||
- After 3 failed edit_diagram attempts
|
|
||||||
|
|
||||||
## Draw.io XML Structure Reference
|
### Shape Styles
|
||||||
|
- rounded=1, fillColor=#hex, strokeColor=#hex, strokeWidth=2
|
||||||
|
- whiteSpace=wrap, html=1, opacity=50, shadow=1, glass=1
|
||||||
|
|
||||||
### Basic Structure
|
### Edge/Connector Styles
|
||||||
\`\`\`xml
|
- endArrow=classic/block/open/oval/diamond/none, startArrow=none/classic
|
||||||
<mxGraphModel>
|
- curved=1, edgeStyle=orthogonalEdgeStyle, strokeWidth=2
|
||||||
<root>
|
- dashed=1, dashPattern=3 3, flowAnimation=1
|
||||||
<mxCell id="0"/>
|
|
||||||
<mxCell id="1" parent="0"/>
|
|
||||||
<!-- All other elements go here as siblings -->
|
|
||||||
</root>
|
|
||||||
</mxGraphModel>
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Critical Structure Rules
|
### Text Styles
|
||||||
1. Always include the two root cells: <mxCell id="0"/> and <mxCell id="1" parent="0"/>
|
- fontSize=14, fontStyle=1 (1=bold, 2=italic, 4=underline, 3=bold+italic)
|
||||||
2. ALL mxCell elements must be DIRECT children of <root> - NEVER nest mxCell inside another mxCell
|
- fontColor=#hex, align=center/left/right, verticalAlign=middle/top/bottom
|
||||||
3. Use unique sequential IDs for all cells (start from "2" for user content)
|
|
||||||
4. Set parent="1" for top-level shapes, or parent="<container-id>" for grouped elements
|
|
||||||
5. Every mxCell (except id="0") must have a parent attribute
|
|
||||||
|
|
||||||
### Shape (Vertex) Example
|
## Common Shape Types
|
||||||
\`\`\`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
|
### Basic Shapes
|
||||||
\`\`\`xml
|
- Rectangle: rounded=0;whiteSpace=wrap;html=1;
|
||||||
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">
|
- Rounded Rectangle: rounded=1;whiteSpace=wrap;html=1;
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
- Ellipse/Circle: ellipse;whiteSpace=wrap;html=1;aspect=fixed;
|
||||||
</mxCell>
|
- Diamond: rhombus;whiteSpace=wrap;html=1;
|
||||||
\`\`\`
|
- Cylinder: shape=cylinder3;whiteSpace=wrap;html=1;
|
||||||
|
|
||||||
### Container/Group Example
|
### Flowchart Shapes
|
||||||
|
- Process: rounded=1;whiteSpace=wrap;html=1;
|
||||||
|
- Decision: rhombus;whiteSpace=wrap;html=1;
|
||||||
|
- Start/End: ellipse;whiteSpace=wrap;html=1;
|
||||||
|
- Document: shape=document;whiteSpace=wrap;html=1;
|
||||||
|
- Database: shape=cylinder3;whiteSpace=wrap;html=1;
|
||||||
|
|
||||||
|
### Container Types
|
||||||
|
- Swimlane: swimlane;whiteSpace=wrap;html=1;
|
||||||
|
- Group Box: rounded=1;whiteSpace=wrap;html=1;container=1;collapsible=0;
|
||||||
|
|
||||||
|
## Container/Group Example
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="container1" value="Group Title" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
<mxCell id="container1" value="Group Title" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
|
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
|
||||||
@@ -382,84 +265,6 @@ If edit_diagram fails with "pattern not found":
|
|||||||
</mxCell>
|
</mxCell>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Common Style Properties
|
|
||||||
|
|
||||||
### Shape Styles
|
|
||||||
- rounded=1 - Rounded corners
|
|
||||||
- fillColor=#hexcolor - Background fill color
|
|
||||||
- strokeColor=#hexcolor - Border color
|
|
||||||
- strokeWidth=2 - Border thickness
|
|
||||||
- whiteSpace=wrap - Enable text wrapping
|
|
||||||
- html=1 - Enable HTML formatting in labels
|
|
||||||
- opacity=50 - Transparency (0-100)
|
|
||||||
- shadow=1 - Drop shadow effect
|
|
||||||
- glass=1 - Glass/gradient effect
|
|
||||||
|
|
||||||
### Edge/Connector Styles
|
|
||||||
- endArrow=classic/block/open/oval/diamond/none - Arrow head style
|
|
||||||
- startArrow=none/classic/block/open - Arrow tail style
|
|
||||||
- curved=1 - Curved line
|
|
||||||
- edgeStyle=orthogonalEdgeStyle - Right-angle routing
|
|
||||||
- edgeStyle=entityRelationEdgeStyle - ER diagram style
|
|
||||||
- strokeWidth=2 - Line thickness
|
|
||||||
- dashed=1 - Dashed line
|
|
||||||
- dashPattern=3 3 - Custom dash pattern
|
|
||||||
- flowAnimation=1 - Animated flow effect
|
|
||||||
|
|
||||||
### Text Styles
|
|
||||||
- fontSize=14 - Font size
|
|
||||||
- fontStyle=1 - Bold (1=bold, 2=italic, 4=underline, can combine: 3=bold+italic)
|
|
||||||
- fontColor=#hexcolor - Text color
|
|
||||||
- align=center/left/right - Horizontal alignment
|
|
||||||
- verticalAlign=middle/top/bottom - Vertical alignment
|
|
||||||
- labelPosition=center/left/right - Label position relative to shape
|
|
||||||
- labelBackgroundColor=#hexcolor - Label background
|
|
||||||
|
|
||||||
## Common Shape Types
|
|
||||||
|
|
||||||
### Basic Shapes
|
|
||||||
- Rectangle: style="rounded=0;whiteSpace=wrap;html=1;"
|
|
||||||
- Rounded Rectangle: style="rounded=1;whiteSpace=wrap;html=1;"
|
|
||||||
- Ellipse/Circle: style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;"
|
|
||||||
- Diamond: style="rhombus;whiteSpace=wrap;html=1;"
|
|
||||||
- Triangle: style="triangle;whiteSpace=wrap;html=1;"
|
|
||||||
- Parallelogram: style="parallelogram;whiteSpace=wrap;html=1;"
|
|
||||||
- Hexagon: style="hexagon;whiteSpace=wrap;html=1;"
|
|
||||||
- Cylinder: style="shape=cylinder3;whiteSpace=wrap;html=1;"
|
|
||||||
|
|
||||||
### Flowchart Shapes
|
|
||||||
- Process: style="rounded=1;whiteSpace=wrap;html=1;"
|
|
||||||
- Decision: style="rhombus;whiteSpace=wrap;html=1;"
|
|
||||||
- Start/End: style="ellipse;whiteSpace=wrap;html=1;"
|
|
||||||
- Document: style="shape=document;whiteSpace=wrap;html=1;"
|
|
||||||
- Data: style="parallelogram;whiteSpace=wrap;html=1;"
|
|
||||||
- Database: style="shape=cylinder3;whiteSpace=wrap;html=1;"
|
|
||||||
|
|
||||||
### Container Types
|
|
||||||
- Swimlane: style="swimlane;whiteSpace=wrap;html=1;"
|
|
||||||
- Group Box: style="rounded=1;whiteSpace=wrap;html=1;container=1;collapsible=0;"
|
|
||||||
|
|
||||||
|
|
||||||
## Animated Connectors
|
|
||||||
|
|
||||||
For animated flow effects on connectors, add flowAnimation=1 to the edge style:
|
|
||||||
\`\`\`xml
|
|
||||||
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;flowAnimation=1;" edge="1" parent="1" source="node1" target="node2">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
|
|
||||||
## Validation Rules
|
|
||||||
|
|
||||||
The XML will be validated before rendering. Ensure:
|
|
||||||
1. All mxCell elements are DIRECT children of <root> - never nested
|
|
||||||
2. Every mxCell has a unique id attribute
|
|
||||||
3. Every mxCell (except id="0") has a valid parent attribute
|
|
||||||
4. Edge source/target attributes reference existing cell IDs
|
|
||||||
5. Special characters in values are escaped: < > & "
|
|
||||||
6. Always start with: <mxCell id="0"/><mxCell id="1" parent="0"/>
|
|
||||||
|
|
||||||
## Example: Complete Flowchart
|
## Example: Complete Flowchart
|
||||||
|
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
@@ -489,16 +294,17 @@ The XML will be validated before rendering. Ensure:
|
|||||||
</mxCell>
|
</mxCell>
|
||||||
</root>
|
</root>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
`
|
||||||
|
|
||||||
Remember: Quality diagrams communicate clearly. Choose appropriate shapes, use consistent styling, and maintain proper spacing for professional results.
|
// Extended system prompt = DEFAULT + EXTENDED_ADDITIONS
|
||||||
`;
|
export const EXTENDED_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT + EXTENDED_ADDITIONS
|
||||||
|
|
||||||
// Model patterns that require extended prompt (4000 token cache minimum)
|
// Model patterns that require extended prompt (4000 token cache minimum)
|
||||||
// These patterns match Opus 4.5 and Haiku 4.5 model IDs
|
// These patterns match Opus 4.5 and Haiku 4.5 model IDs
|
||||||
const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
||||||
'claude-opus-4-5', // Matches any Opus 4.5 variant
|
"claude-opus-4-5", // Matches any Opus 4.5 variant
|
||||||
'claude-haiku-4-5', // Matches any Haiku 4.5 variant
|
"claude-haiku-4-5", // Matches any Haiku 4.5 variant
|
||||||
];
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the appropriate system prompt based on the model ID
|
* Get the appropriate system prompt based on the model ID
|
||||||
@@ -507,10 +313,25 @@ const EXTENDED_PROMPT_MODEL_PATTERNS = [
|
|||||||
* @returns The system prompt string
|
* @returns The system prompt string
|
||||||
*/
|
*/
|
||||||
export function getSystemPrompt(modelId?: string): string {
|
export function getSystemPrompt(modelId?: string): string {
|
||||||
if (modelId && EXTENDED_PROMPT_MODEL_PATTERNS.some(pattern => modelId.includes(pattern))) {
|
const modelName = modelId || "AI"
|
||||||
console.log(`[System Prompt] Using EXTENDED prompt for model: ${modelId}`);
|
|
||||||
return EXTENDED_SYSTEM_PROMPT;
|
let prompt: string
|
||||||
}
|
if (
|
||||||
console.log(`[System Prompt] Using DEFAULT prompt for model: ${modelId || 'unknown'}`);
|
modelId &&
|
||||||
return DEFAULT_SYSTEM_PROMPT;
|
EXTENDED_PROMPT_MODEL_PATTERNS.some((pattern) =>
|
||||||
|
modelId.includes(pattern),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[System Prompt] Using EXTENDED prompt for model: ${modelId}`,
|
||||||
|
)
|
||||||
|
prompt = EXTENDED_SYSTEM_PROMPT
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`[System Prompt] Using DEFAULT prompt for model: ${modelId || "unknown"}`,
|
||||||
|
)
|
||||||
|
prompt = DEFAULT_SYSTEM_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt.replace("{{MODEL_NAME}}", modelName)
|
||||||
}
|
}
|
||||||
|
|||||||
959
lib/utils.ts
959
lib/utils.ts
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next"
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: 'standalone',
|
output: "standalone",
|
||||||
};
|
}
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig
|
||||||
|
|||||||
614
package-lock.json
generated
614
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.3.8",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@@ -61,6 +62,8 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-next": "16.0.5",
|
"eslint-config-next": "16.0.5",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^16.2.7",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
@@ -1452,6 +1455,169 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@biomejs/biome": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"biome": "bin/biome"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/biome"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@biomejs/cli-darwin-arm64": "2.3.8",
|
||||||
|
"@biomejs/cli-darwin-x64": "2.3.8",
|
||||||
|
"@biomejs/cli-linux-arm64": "2.3.8",
|
||||||
|
"@biomejs/cli-linux-arm64-musl": "2.3.8",
|
||||||
|
"@biomejs/cli-linux-x64": "2.3.8",
|
||||||
|
"@biomejs/cli-linux-x64-musl": "2.3.8",
|
||||||
|
"@biomejs/cli-win32-arm64": "2.3.8",
|
||||||
|
"@biomejs/cli-win32-x64": "2.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@csstools/color-helpers": {
|
"node_modules/@csstools/color-helpers": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
|
||||||
@@ -5678,6 +5844,35 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-escapes": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"environment": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
@@ -6203,6 +6398,39 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-cursor": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"restore-cursor": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-truncate": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"slice-ansi": "^7.1.0",
|
||||||
|
"string-width": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/client-only": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
@@ -6238,6 +6466,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "2.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||||
|
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -6260,6 +6495,16 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "14.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
|
||||||
|
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -6588,6 +6833,19 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/environment": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.24.0",
|
"version": "1.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
||||||
@@ -7250,6 +7508,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eventsource-parser": {
|
"node_modules/eventsource-parser": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
||||||
@@ -7499,6 +7764,19 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-east-asian-width": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -7841,6 +8119,22 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/husky": {
|
||||||
|
"version": "9.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||||
|
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"husky": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@@ -8116,6 +8410,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-generator-function": {
|
"node_modules/is-generator-function": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
@@ -8829,6 +9139,49 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lint-staged": {
|
||||||
|
"version": "16.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
|
||||||
|
"integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^14.0.2",
|
||||||
|
"listr2": "^9.0.5",
|
||||||
|
"micromatch": "^4.0.8",
|
||||||
|
"nano-spawn": "^2.0.0",
|
||||||
|
"pidtree": "^0.6.0",
|
||||||
|
"string-argv": "^0.3.2",
|
||||||
|
"yaml": "^2.8.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"lint-staged": "bin/lint-staged.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cli-truncate": "^5.0.0",
|
||||||
|
"colorette": "^2.0.20",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"log-update": "^6.1.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -8852,6 +9205,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/log-update": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-escapes": "^7.0.0",
|
||||||
|
"cli-cursor": "^5.0.0",
|
||||||
|
"slice-ansi": "^7.1.0",
|
||||||
|
"strip-ansi": "^7.1.0",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/long": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
@@ -9794,6 +10167,19 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-function": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -9832,6 +10218,19 @@
|
|||||||
"mustache": "bin/mustache"
|
"mustache": "bin/mustache"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nano-spawn": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -10122,6 +10521,22 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/onetime": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-function": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -10292,6 +10707,19 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pidtree": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"pidtree": "bin/pidtree.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -10748,6 +11176,23 @@
|
|||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restore-cursor": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"onetime": "^7.0.0",
|
||||||
|
"signal-exit": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
@@ -10759,6 +11204,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rrweb-cssom": {
|
"node_modules/rrweb-cssom": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
@@ -11074,6 +11526,49 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/slice-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"is-fullwidth-code-point": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/slice-ansi/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sonner": {
|
"node_modules/sonner": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||||
@@ -11124,6 +11619,33 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-argv": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.3.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -11251,6 +11773,22 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-bom": {
|
"node_modules/strip-bom": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
@@ -12167,6 +12705,62 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"string-width": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -12210,6 +12804,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack --port 6002",
|
"dev": "next dev --turbopack --port 6002",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start --port 6001",
|
"start": "next start --port 6001",
|
||||||
"lint": "next lint"
|
"lint": "biome lint .",
|
||||||
|
"format": "biome check --write .",
|
||||||
|
"check": "biome ci",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
||||||
@@ -53,7 +56,14 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,jsx,tsx,json,css}": [
|
||||||
|
"biome check --write --no-errors-on-unmatched",
|
||||||
|
"biome check --no-errors-on-unmatched"
|
||||||
|
]
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.3.8",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@@ -62,6 +72,8 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-next": "16.0.5",
|
"eslint-config-next": "16.0.5",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^16.2.7",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: ["@tailwindcss/postcss"],
|
||||||
};
|
}
|
||||||
|
|
||||||
export default config;
|
export default config
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
"allowJs": true,
|
||||||
"dom.iterable",
|
"skipLibCheck": true,
|
||||||
"esnext"
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"exclude": ["node_modules"]
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"./*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
".next/dev/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user