mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
Compare commits
319 Commits
fix/resize
...
3e053bc904
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e053bc904 | ||
|
|
037f32973a | ||
|
|
7bdc1fe612 | ||
|
|
03ac9a79de | ||
|
|
f97934d6e0 | ||
|
|
73a36cf9de | ||
|
|
69f9df1792 | ||
|
|
aaa2938dac | ||
|
|
24afa0b58a | ||
|
|
1d19127855 | ||
|
|
ca21a5bb27 | ||
|
|
ad80e9c6f5 | ||
|
|
1ab8d260a2 | ||
|
|
2d62496f9f | ||
|
|
c2aa7f49be | ||
|
|
30b30550d9 | ||
|
|
49b086cef3 | ||
|
|
27f26d8b26 | ||
|
|
6d1e12bb39 | ||
|
|
226c336671 | ||
|
|
1527883360 | ||
|
|
641a715d44 | ||
|
|
41184969fa | ||
|
|
c92975f831 | ||
|
|
9ac99a4690 | ||
|
|
6d84dade56 | ||
|
|
43f3fbb5ee | ||
|
|
1915c817c3 | ||
|
|
eeab1ba75d | ||
|
|
1f4eb02b0b | ||
|
|
5d60ca74f7 | ||
|
|
9fa1dd075b | ||
|
|
743b317387 | ||
|
|
5ed23784e7 | ||
|
|
3a22e11651 | ||
|
|
eb89b9c052 | ||
|
|
9c1117e8b0 | ||
|
|
39bf3d6a49 | ||
|
|
ecd689162f | ||
|
|
7a03aec9be | ||
|
|
95541dd284 | ||
|
|
49af6676b5 | ||
|
|
18ab1bffa0 | ||
|
|
571ba3c6b0 | ||
|
|
467561df47 | ||
|
|
e67ab37383 | ||
|
|
31644dbcd8 | ||
|
|
067d309927 | ||
|
|
d1d0de3dea | ||
|
|
8c736cee0d | ||
|
|
c5a04c9e50 | ||
|
|
44c453403f | ||
|
|
9727aa5b39 | ||
|
|
51858dbf5d | ||
|
|
3047d19238 | ||
|
|
ed069afdea | ||
|
|
d2e5afb298 | ||
|
|
d3fb2314ee | ||
|
|
447bb30745 | ||
|
|
63398d9f34 | ||
|
|
82f4deb23a | ||
|
|
1fab261cd0 | ||
|
|
7a4a04c263 | ||
|
|
0d2e7a7ad6 | ||
|
|
3218ccc909 | ||
|
|
d3be96de79 | ||
|
|
b2dfd5b890 | ||
|
|
72d647de7a | ||
|
|
c6b0e5ac62 | ||
|
|
7de192e1fa | ||
|
|
97ae9395cd | ||
|
|
5ec05eb100 | ||
|
|
9aec7eda79 | ||
|
|
a0fbc0ad33 | ||
|
|
0385c45a10 | ||
|
|
5262b7bfb2 | ||
|
|
8cb7494d16 | ||
|
|
98625dd72a | ||
|
|
b5734aa5e1 | ||
|
|
87cdc53665 | ||
|
|
b4fc259de8 | ||
|
|
28f9a81e7b | ||
|
|
0f67884ead | ||
|
|
3521495ead | ||
|
|
6446454cd7 | ||
|
|
84959637db | ||
|
|
9e9ea10beb | ||
|
|
deae5c2c38 | ||
|
|
6e2d98e52d | ||
|
|
85cb441e26 | ||
|
|
b088a0653e | ||
|
|
b25b944600 | ||
|
|
4f07a5fafc | ||
|
|
fc5eca877a | ||
|
|
f58274bb84 | ||
|
|
e03b65328d | ||
|
|
14c1aa8e1c | ||
|
|
9e651a51e6 | ||
|
|
2871265362 | ||
|
|
9d13bd7451 | ||
|
|
b97f3ccda9 | ||
|
|
864375b8e4 | ||
|
|
b9bc2a72c6 | ||
|
|
c215d80688 | ||
|
|
74b9e38114 | ||
|
|
68ea4958b8 | ||
|
|
938faff6b2 | ||
|
|
378bef435e | ||
|
|
f087b54ee4 | ||
|
|
6bb33eeda2 | ||
|
|
a91bd9d1e8 | ||
|
|
81eb71e704 | ||
|
|
58b6b19526 | ||
|
|
f65ef548b2 | ||
|
|
741a00db89 | ||
|
|
bcc6684ecb | ||
|
|
a9415d24e7 | ||
|
|
439bdd4577 | ||
|
|
98b890bb06 | ||
|
|
f039e4a3c8 | ||
|
|
7857858074 | ||
|
|
f0919117eb | ||
|
|
cd76fa615e | ||
|
|
c527ce1520 | ||
|
|
44840d27b3 | ||
|
|
f175276872 | ||
|
|
09c556e4c3 | ||
|
|
ac1c2ce044 | ||
|
|
78a77e102d | ||
|
|
55821301dd | ||
|
|
f743219c03 | ||
|
|
ff34f0baf1 | ||
|
|
0851b32b67 | ||
|
|
2e24071539 | ||
|
|
66bd0e5493 | ||
|
|
b33e09be05 | ||
|
|
987dc9f026 | ||
|
|
6024443816 | ||
|
|
4b838fd6d5 | ||
|
|
e321ba7959 | ||
|
|
aa15519fba | ||
|
|
c2c65973f9 | ||
|
|
b5db980f69 | ||
|
|
c9b60bfdb2 | ||
|
|
f170bb41ae | ||
|
|
a0f163fe9e | ||
|
|
8fd3830b9d | ||
|
|
77a25d2543 | ||
|
|
b9da24dd6d | ||
|
|
97cc0a07dc | ||
|
|
c42efdc702 | ||
|
|
dd027f1856 | ||
|
|
869391a029 | ||
|
|
8b9336466f | ||
|
|
ee514efa9e | ||
|
|
e2757a34b7 | ||
|
|
c0347dd55d | ||
|
|
a047a6ff97 | ||
|
|
d2ba133eaf | ||
|
|
43e5993f47 | ||
|
|
9a954ccb44 | ||
|
|
9d4c89ec43 | ||
|
|
5da4ef67ec | ||
|
|
67b0adf211 | ||
|
|
65af353852 | ||
|
|
b3deb65584 | ||
|
|
626f3a76b5 | ||
|
|
97bb350dd6 | ||
|
|
97ab82e027 | ||
|
|
77cb10393b | ||
|
|
967d63c57e | ||
|
|
914e914423 | ||
|
|
f6cfcab45a | ||
|
|
95c5a75ca3 | ||
|
|
ac09f9f8f9 | ||
|
|
622829b903 | ||
|
|
728dda5267 | ||
|
|
b68b1b0f33 | ||
|
|
bd23aed93b | ||
|
|
95aa4b8a56 | ||
|
|
4070772733 | ||
|
|
c4aaa7c915 | ||
|
|
ecea8a6005 | ||
|
|
ebc622144b | ||
|
|
ee9267d54c | ||
|
|
f6682fe3ac | ||
|
|
03db4c8096 | ||
|
|
167f5ed36a | ||
|
|
cd8e0e2263 | ||
|
|
8c431ee6ed | ||
|
|
86420a42c6 | ||
|
|
0baf21fadb | ||
|
|
a54068fec2 | ||
|
|
e25fd367d5 | ||
|
|
3264244fe9 | ||
|
|
d8cdd049d1 | ||
|
|
b1bc1a6dc6 | ||
|
|
8b578a456e | ||
|
|
05d58025c4 | ||
|
|
4be64317b3 | ||
|
|
2fac6323f0 | ||
|
|
a415c46b66 | ||
|
|
3894abd9ed | ||
|
|
6965a54f48 | ||
|
|
46567cb0b8 | ||
|
|
9f77199272 | ||
|
|
77f2569a3b | ||
|
|
cbb92bd636 | ||
|
|
8d898d8adc | ||
|
|
1e0b1ed970 | ||
|
|
1d03d10ba8 | ||
|
|
e893bd60f9 | ||
|
|
9aaf9bf31f | ||
|
|
150eb1ff63 | ||
|
|
215a101f54 | ||
|
|
e00938d9d3 | ||
|
|
dd27d034e2 | ||
|
|
9e781005af | ||
|
|
fe1aa2747e | ||
|
|
7205896c8c | ||
|
|
4e32a094b1 | ||
|
|
96a1111654 | ||
|
|
3f35c52527 | ||
|
|
0af5229477 | ||
|
|
3fb349fb3e | ||
|
|
ed29e32ba3 | ||
|
|
4cd78dc561 | ||
|
|
e0c5d966e3 | ||
|
|
33471d5b3a | ||
|
|
3ef9908df7 | ||
|
|
57bfc9cef7 | ||
|
|
0543f71c43 | ||
|
|
970b88612d | ||
|
|
c805277a76 | ||
|
|
95160f5a21 | ||
|
|
b206e16c02 | ||
|
|
563b18e8ff | ||
|
|
2366255e8f | ||
|
|
255308f829 | ||
|
|
a9493c8877 | ||
|
|
a0c3db100a | ||
|
|
ff6f130f8a | ||
|
|
562751c913 | ||
|
|
95e8a9c0c0 | ||
|
|
d9568562f0 | ||
|
|
7b8bd8c621 | ||
|
|
46cbc3354c | ||
|
|
46d2d4e078 | ||
|
|
d8f2c85dab | ||
|
|
5f4d31e708 | ||
|
|
489b377063 | ||
|
|
3534cb13f7 | ||
|
|
9d9613a8d1 | ||
|
|
bed04c82f8 | ||
|
|
fa1b02ad78 | ||
|
|
39322c2793 | ||
|
|
110cccb09c | ||
|
|
5021076864 | ||
|
|
efdf4f2b90 | ||
|
|
45f74df349 | ||
|
|
a61d37c818 | ||
|
|
c0cd393baa | ||
|
|
595f24857a | ||
|
|
33fed6fa9f | ||
|
|
a8e627f1f8 | ||
|
|
c458947553 | ||
|
|
443a937370 | ||
|
|
3f5cdd807d | ||
|
|
894740ba58 | ||
|
|
271f3b0f58 | ||
|
|
bc0f767ad7 | ||
|
|
61ef41addf | ||
|
|
5d38ed59eb | ||
|
|
53754e627a | ||
|
|
bca80c0856 | ||
|
|
e2adfb49aa | ||
|
|
45ab934288 | ||
|
|
af3173623a | ||
|
|
cd012f5e2f | ||
|
|
d4fb635d98 | ||
|
|
14740e35a8 | ||
|
|
5b31216917 | ||
|
|
c7d0260328 | ||
|
|
d2d4dd01cc | ||
|
|
b4679f6598 | ||
|
|
0d0d553e23 | ||
|
|
6e6de1eba6 | ||
|
|
00af87edbe | ||
|
|
468d6c0276 | ||
|
|
14f74b076f | ||
|
|
78d9229ca3 | ||
|
|
d8e0a1daad | ||
|
|
32e75ab556 | ||
|
|
b87e3a2de9 | ||
|
|
b758f63d7f | ||
|
|
b32d42d962 | ||
|
|
603865fdb0 | ||
|
|
4cb4e187b3 | ||
|
|
b4d0c6c18b | ||
|
|
c03c41d320 | ||
|
|
9d248e25ad | ||
|
|
4f4aae0e39 | ||
|
|
b00579b257 | ||
|
|
caf7ffe56c | ||
|
|
50d16cbe47 | ||
|
|
56167d363c | ||
|
|
3e2dbbb541 | ||
|
|
efebcea3ba | ||
|
|
68824bc951 | ||
|
|
794826550d | ||
|
|
de98cf60ae | ||
|
|
b3fc624e13 | ||
|
|
d2dd501f3f | ||
|
|
5964deeff7 | ||
|
|
663e1c8c77 | ||
|
|
455115935c | ||
|
|
8d36e0dfb0 | ||
|
|
60f4694752 | ||
|
|
7a6a7eaf7c |
60
.dockerignore
Normal file
60
.dockerignore
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Operating System
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
!env.example
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.log
|
||||||
|
.cache
|
||||||
|
.turbo
|
||||||
|
|
||||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
|
}
|
||||||
35
.github/CONTRIBUTING.md
vendored
Normal file
35
.github/CONTRIBUTING.md
vendored
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.
|
||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: dayuanjiang
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug to help us improve
|
||||||
|
title: '[Bug] '
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your thoughts.
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
A brief description of the issue.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
What actually happened.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain the problem.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- OS: [e.g. Windows 11, macOS 14]
|
||||||
|
- Browser: [e.g. Chrome 120, Safari 17]
|
||||||
|
- Version: [e.g. 1.0.0]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Any other information about the problem.
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Discussions
|
||||||
|
url: https://github.com/DayuanJiang/next-ai-draw-io/discussions
|
||||||
|
about: Have questions or ideas? Feel free to start a discussion
|
||||||
24
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement
|
||||||
|
about: Suggest an improvement to existing functionality
|
||||||
|
title: '[Enhancement] '
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
Describe how the feature currently works.
|
||||||
|
|
||||||
|
## Proposed Enhancement
|
||||||
|
How you'd like this to be improved.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why this enhancement would be beneficial.
|
||||||
|
|
||||||
|
## Screenshots / Mockups
|
||||||
|
If applicable, add screenshots or mockups to illustrate the proposed changes.
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Any other information about the enhancement request.
|
||||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature for this project
|
||||||
|
title: '[Feature] '
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
A brief description of the feature you'd like.
|
||||||
|
|
||||||
|
## Problem Context
|
||||||
|
Is this related to a problem? Please describe.
|
||||||
|
e.g. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
How you'd like this feature to work.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
Any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Any other information or screenshots about the feature request.
|
||||||
40
.github/renovate.json
vendored
Normal file
40
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"schedule": ["after 10am on saturday"],
|
||||||
|
"timezone": "Asia/Tokyo",
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"matchPackagePatterns": ["*"],
|
||||||
|
"groupName": "minor and patch dependencies",
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"matchPackagePatterns": ["*"],
|
||||||
|
"automerge": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["@ai-sdk/*"],
|
||||||
|
"groupName": "AI SDK packages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["@radix-ui/*"],
|
||||||
|
"groupName": "Radix UI packages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["electron", "electron-builder"],
|
||||||
|
"groupName": "Electron packages",
|
||||||
|
"automerge": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["@ai-sdk/*", "ai", "next"],
|
||||||
|
"groupName": "Core framework packages",
|
||||||
|
"automerge": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vulnerabilityAlerts": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
54
.github/workflows/auto-format.yml
vendored
Normal file
54
.github/workflows/auto-format.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Auto Format
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
|
||||||
|
- name: Run Biome format
|
||||||
|
run: npx @biomejs/biome@latest check --write --no-errors-on-unmatched .
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: changes
|
||||||
|
run: |
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For fork PRs, just fail if formatting is needed (can't push to forks)
|
||||||
|
- name: Fail if fork PR needs formatting
|
||||||
|
if: steps.changes.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name != github.repository
|
||||||
|
run: |
|
||||||
|
echo "::error::This PR has formatting issues. Please run 'npx @biomejs/biome check --write .' locally and push the changes."
|
||||||
|
git diff --stat
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# For same-repo PRs, commit and push the changes
|
||||||
|
- name: Commit changes
|
||||||
|
if: steps.changes.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
git add .
|
||||||
|
git commit -m "style: auto-format with Biome"
|
||||||
|
git push origin HEAD:${{ github.head_ref }}
|
||||||
44
.github/workflows/ci.yml
vendored
Normal file
44
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Lint check
|
||||||
|
run: npm run check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Security audit
|
||||||
|
run: npm audit --audit-level=high --omit=dev
|
||||||
92
.github/workflows/docker-build.yml
vendored
Normal file
92
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
name: Docker Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=sha,prefix=sha-
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=true
|
||||||
|
|
||||||
|
# Push to AWS ECR for App Runner auto-deploy
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: ap-northeast-1
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2
|
||||||
|
|
||||||
|
- name: Push to ECR (triggers App Runner auto-deploy)
|
||||||
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
REPO_LOWER: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
REPO_LOWER=$(echo "$REPO_LOWER" | tr '[:upper:]' '[:lower:]')
|
||||||
|
docker pull ghcr.io/${REPO_LOWER}:latest
|
||||||
|
docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||||
|
docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
|
||||||
|
|
||||||
46
.github/workflows/electron-release.yml
vendored
Normal file
46
.github/workflows/electron-release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Electron Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version tag (e.g., v0.4.5)"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
platform: mac
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build and publish Electron app
|
||||||
|
run: npm run dist:${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
27
.gitignore
vendored
27
.gitignore
vendored
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
packages/*/node_modules
|
||||||
|
packages/*/dist
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
.yarn/*
|
||||||
@@ -40,4 +42,27 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
push-via-ec2.sh
|
push-via-ec2.sh
|
||||||
.claude/settings.local.json
|
.claude/
|
||||||
|
.playwright-mcp/
|
||||||
|
# Cloudflare
|
||||||
|
.dev.vars
|
||||||
|
.open-next/
|
||||||
|
.wrangler/
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Electron
|
||||||
|
/dist-electron/
|
||||||
|
/release/
|
||||||
|
/electron-standalone/
|
||||||
|
*.dmg
|
||||||
|
*.exe
|
||||||
|
*.AppImage
|
||||||
|
*.deb
|
||||||
|
*.rpm
|
||||||
|
*.snap
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
|
.spec-workflow
|
||||||
|
|
||||||
|
# edgeone
|
||||||
|
.edgeone
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Dockerfile
Normal file
63
Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Multi-stage Dockerfile for Next.js
|
||||||
|
|
||||||
|
# Stage 1: Install dependencies
|
||||||
|
FROM node:24-alpine AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Stage 2: Build application
|
||||||
|
FROM node:24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy node_modules from deps stage
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Disable Next.js telemetry during build
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Build-time argument for self-hosted draw.io URL
|
||||||
|
ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net
|
||||||
|
ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL}
|
||||||
|
|
||||||
|
# Build-time argument to show About link and Notice icon
|
||||||
|
ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false
|
||||||
|
ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE}
|
||||||
|
|
||||||
|
# Build Next.js application (standalone mode)
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 3: Production runtime
|
||||||
|
FROM node:24-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Copy standalone build output
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# Start the application (HOSTNAME override needed for AWS App Runner)
|
||||||
|
CMD ["sh", "-c", "HOSTNAME=0.0.0.0 exec node server.js"]
|
||||||
|
|
||||||
190
LICENSE
Normal file
190
LICENSE
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2024 Dayuan Jiang
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
258
README.md
258
README.md
@@ -1,43 +1,155 @@
|
|||||||
# Next AI Draw.io
|
# Next AI Draw.io
|
||||||
|
|
||||||
A next.js web application that integrates AI capabilities with draw.io diagrams. This app allows you to create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
<div align="center">
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**
|
||||||
|
|
||||||
Demo site: [https://next-ai-draw-io.vercel.app](https://next-ai-draw-io.vercel.app)
|
English | [中文](./docs/cn/README_CN.md) | [日本語](./docs/ja/README_JA.md)
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://react.dev/)
|
||||||
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
A Next.js web application that integrates AI capabilities with draw.io diagrams. Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
|
||||||
|
|
||||||
|
> Note: Thanks to <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) sponsorship, the demo site now uses the powerful K2-thinking model!
|
||||||
|
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Next AI Draw.io](#next-ai-drawio)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Examples](#examples)
|
||||||
|
- [Features](#features)
|
||||||
|
- [MCP Server (Preview)](#mcp-server-preview)
|
||||||
|
- [Claude Code CLI](#claude-code-cli)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Try it Online](#try-it-online)
|
||||||
|
- [Desktop Application](#desktop-application)
|
||||||
|
- [Run with Docker](#run-with-docker)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Deploy to EdgeOne Pages](#deploy-to-edgeone-pages)
|
||||||
|
- [Deploy on Vercel (Recommended)](#deploy-on-vercel-recommended)
|
||||||
|
- [Deploy on Cloudflare Workers](#deploy-on-cloudflare-workers)
|
||||||
|
- [Multi-Provider Support](#multi-provider-support)
|
||||||
|
- [How It Works](#how-it-works)
|
||||||
|
- [Support \& Contact](#support--contact)
|
||||||
|
- [Star History](#star-history)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Here are some example prompts and their generated diagrams:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top" align="center">
|
||||||
|
<strong>Animated transformer connectors</strong><br />
|
||||||
|
<p><strong>Prompt:</strong> Give me a **animated connector** diagram of transformer's architecture.</p>
|
||||||
|
<img src="./public/animated_connectors.svg" alt="Transformer Architecture with Animated Connectors" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>GCP architecture diagram</strong><br />
|
||||||
|
<p><strong>Prompt:</strong> Generate a GCP architecture diagram with **GCP icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
|
||||||
|
<img src="./public/gcp_demo.svg" alt="GCP Architecture Diagram" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>AWS architecture diagram</strong><br />
|
||||||
|
<p><strong>Prompt:</strong> Generate a AWS architecture diagram with **AWS icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
|
||||||
|
<img src="./public/aws_demo.svg" alt="AWS Architecture Diagram" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>Azure architecture diagram</strong><br />
|
||||||
|
<p><strong>Prompt:</strong> Generate a Azure architecture diagram with **Azure icons**. In this diagram, users connect to a frontend hosted on an instance.</p>
|
||||||
|
<img src="./public/azure_demo.svg" alt="Azure Architecture Diagram" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>Cat sketch prompt</strong><br />
|
||||||
|
<p><strong>Prompt:</strong> Draw a cute cat for me.</p>
|
||||||
|
<img src="./public/cat_demo.svg" alt="Cat Drawing" width="240" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands
|
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands
|
||||||
- **Image-Based Diagram Replication**: Upload existing diagrams or images and have the AI replicate and enhance them automatically
|
- **Image-Based Diagram Replication**: Upload existing diagrams or images and have the AI replicate and enhance them automatically
|
||||||
|
- **PDF & Text File Upload**: Upload PDF documents and text files to extract content and generate diagrams from existing documents
|
||||||
|
- **AI Reasoning Display**: View the AI's thinking process for supported models (OpenAI o1/o3, Gemini, Claude, etc.)
|
||||||
- **Diagram History**: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing.
|
- **Diagram History**: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing.
|
||||||
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
|
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
|
||||||
- **Smart Editing**: Modify existing diagrams using simple text prompts
|
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
|
||||||
- **Targeted XML Editing**: AI can now make precise edits to specific parts of diagrams without regenerating the entire XML, making updates faster and more efficient
|
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
|
||||||
|
|
||||||
## How It Works
|
## MCP Server (Preview)
|
||||||
|
|
||||||
The application uses the following technologies:
|
> **Preview Feature**: This feature is experimental and may not be stable.
|
||||||
|
|
||||||
- **Next.js**: For the frontend framework and routing
|
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
|
||||||
- **@ai-sdk/react**: For the chat interface and AI interactions
|
|
||||||
- **react-drawio**: For diagram representation and manipulation
|
|
||||||
|
|
||||||
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Multi-Provider Support
|
### Claude Code CLI
|
||||||
|
|
||||||
- AWS Bedrock (default)
|
```bash
|
||||||
- OpenAI
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
- Anthropic
|
```
|
||||||
- Google AI
|
|
||||||
- Azure OpenAI
|
|
||||||
- Ollama
|
|
||||||
|
|
||||||
Note that `claude-sonnet-4-5` has trained on draw.io diagrams with AWS logos, so if you want to create AWS architecture diagrams, this is the best choice.
|
Then ask Claude to create diagrams:
|
||||||
|
> "Create a flowchart showing user authentication with login, MFA, and session management"
|
||||||
|
|
||||||
|
The diagram appears in your browser in real-time!
|
||||||
|
|
||||||
|
See the [MCP Server README](./packages/mcp-server/README.md) for VS Code, Cursor, and other client configurations.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
### Try it Online
|
||||||
|
|
||||||
|
No installation needed! Try the app directly on our demo site:
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> **Bring Your Own API Key**: You can use your own API key to bypass usage limits on the demo site. Click the Settings icon in the chat panel to configure your provider and API key. Your key is stored locally in your browser and is never stored on the server.
|
||||||
|
|
||||||
|
### Desktop Application
|
||||||
|
|
||||||
|
Download the native desktop app for your platform from the [Releases page](https://github.com/DayuanJiang/next-ai-draw-io/releases):
|
||||||
|
|
||||||
|
Supported platforms: Windows, macOS, Linux.
|
||||||
|
|
||||||
|
### Run with Docker
|
||||||
|
|
||||||
|
[Go to Docker Guide](./docs/en/docker.md)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
@@ -45,80 +157,96 @@ Note that `claude-sonnet-4-5` has trained on draw.io diagrams with AWS logos, so
|
|||||||
```bash
|
```bash
|
||||||
git clone https://github.com/DayuanJiang/next-ai-draw-io
|
git clone https://github.com/DayuanJiang/next-ai-draw-io
|
||||||
cd next-ai-draw-io
|
cd next-ai-draw-io
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
npm install
|
||||||
# or
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Configure your AI provider:
|
|
||||||
|
|
||||||
Create a `.env.local` file in the root directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp env.example .env.local
|
cp env.example .env.local
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `.env.local` and configure your chosen provider:
|
See the [Provider Configuration Guide](./docs/en/ai-providers.md) for detailed setup instructions for each provider.
|
||||||
|
|
||||||
- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama)
|
2. Run the development server:
|
||||||
- Set `AI_MODEL` to the specific model you want to use
|
|
||||||
- Add the required API keys for your provider
|
|
||||||
|
|
||||||
See the [Multi-Provider Support](#multi-provider-support) section above for provider-specific configuration examples.
|
|
||||||
|
|
||||||
4. Run the development server:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.
|
3. Open [http://localhost:6002](http://localhost:6002) in your browser to see the application.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js.
|
### Deploy to EdgeOne Pages
|
||||||
|
|
||||||
Check out the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
You can deploy with one click using [Tencent EdgeOne Pages](https://pages.edgeone.ai/).
|
||||||
|
|
||||||
|
Deploy by this button:
|
||||||
|
|
||||||
|
[](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
Check out the [Tencent EdgeOne Pages documentation](https://pages.edgeone.ai/document/deployment-overview) for more details.
|
||||||
|
|
||||||
|
Additionally, deploying through Tencent EdgeOne Pages will also grant you a [daily free quota for DeepSeek models](https://pages.edgeone.ai/document/edge-ai).
|
||||||
|
|
||||||
|
### Deploy on Vercel (Recommended)
|
||||||
|
|
||||||
Or you can deploy by this button.
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
|
The easiest way to deploy is using [Vercel](https://vercel.com/new), the creators of Next.js. Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
|
||||||
|
|
||||||
## Project Structure
|
See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
|
|
||||||
```
|
### Deploy on Cloudflare Workers
|
||||||
app/ # Next.js application routes and pages
|
|
||||||
extract_xml.ts # Utilities for XML processing
|
|
||||||
components/ # React components
|
|
||||||
chat-input.tsx # User input component for AI interaction
|
|
||||||
chatPanel.tsx # Chat interface with diagram control
|
|
||||||
ui/ # UI components (buttons, cards, etc.)
|
|
||||||
lib/ # Utility functions and helpers
|
|
||||||
utils.ts # General utilities including XML conversion
|
|
||||||
public/ # Static assets including example images
|
|
||||||
```
|
|
||||||
|
|
||||||
## TODOs
|
[Go to Cloudflare Deploy Guide](./docs/en/cloudflare-deploy.md)
|
||||||
|
|
||||||
- [x] Allow the LLM to modify the XML instead of generating it from scratch everytime.
|
|
||||||
- [x] Improve the smoothness of shape streaming updates.
|
|
||||||
- [x] Add multiple AI provider support (OpenAI, Anthropic, Google, Azure, Ollama)
|
|
||||||
- [ ] Solve the bug that generation will fail for session that longer than 60s.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
## Multi-Provider Support
|
||||||
|
|
||||||
|
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||||
|
- AWS Bedrock (default)
|
||||||
|
- OpenAI
|
||||||
|
- Anthropic
|
||||||
|
- Google AI
|
||||||
|
- Azure OpenAI
|
||||||
|
- Ollama
|
||||||
|
- OpenRouter
|
||||||
|
- DeepSeek
|
||||||
|
- SiliconFlow
|
||||||
|
- SGLang
|
||||||
|
- Vercel AI Gateway
|
||||||
|
|
||||||
|
|
||||||
|
All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
||||||
|
|
||||||
|
📖 **[Detailed Provider Configuration Guide](./docs/en/ai-providers.md)** - See setup instructions for each provider.
|
||||||
|
|
||||||
|
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
|
||||||
|
|
||||||
|
Note that the `claude` series has been trained on draw.io diagrams with cloud architecture logos like AWS, Azure, GCP. So if you want to create cloud architecture diagrams, this is the best choice.
|
||||||
|
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The application uses the following technologies:
|
||||||
|
|
||||||
|
- **Next.js**: For the frontend framework and routing
|
||||||
|
- **Vercel AI SDK** (`ai` + `@ai-sdk/*`): For streaming AI responses and multi-provider support
|
||||||
|
- **react-drawio**: For diagram representation and manipulation
|
||||||
|
|
||||||
|
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
|
||||||
|
|
||||||
|
|
||||||
## Support & Contact
|
## Support & Contact
|
||||||
|
|
||||||
|
**Special thanks to [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) for sponsoring the API token usage of the demo site!** Register on the ARK platform to get 500K free tokens for all models!
|
||||||
|
|
||||||
|
If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site!
|
||||||
|
|
||||||
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:
|
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:
|
||||||
|
|
||||||
- Email: me[at]jiang.jp
|
- Email: me[at]jiang.jp
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
416
app/[lang]/about/cn/page.tsx
Normal file
416
app/[lang]/about/cn/page.tsx
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import Image from "next/image"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "关于 - Next AI Draw.io",
|
||||||
|
description:
|
||||||
|
"AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
|
||||||
|
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000) {
|
||||||
|
return `${num / 1000}k`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AboutCN() {
|
||||||
|
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
|
||||||
|
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
|
||||||
|
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<header className="bg-white border-b border-gray-200">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xl font-bold text-gray-900 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Next AI Draw.io
|
||||||
|
</Link>
|
||||||
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
编辑器
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/about/cn"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
关于
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
aria-label="在GitHub上查看"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article className="prose prose-lg max-w-none">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||||
|
Next AI Draw.io
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
|
AI驱动的图表创建工具 - 对话、绘制、可视化
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
|
||||||
|
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
|
由字节跳动豆包提供支持
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Story */}
|
||||||
|
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
||||||
|
<p>
|
||||||
|
好消息!感谢{" "}
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-semibold text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
字节跳动豆包
|
||||||
|
</a>
|
||||||
|
的慷慨赞助,演示站点现已接入强大的{" "}
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
K2-thinking
|
||||||
|
</span>{" "}
|
||||||
|
模型,图表生成效果更佳!点击链接注册即可领取{" "}
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
50万免费Token
|
||||||
|
</span>
|
||||||
|
,适用于所有模型!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Limits */}
|
||||||
|
<p className="text-sm text-gray-600 mb-3">
|
||||||
|
当前使用限制:
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-5">
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(dailyRequestLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
请求/天
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(dailyTokenLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Token/天
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(tpmLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Token/分钟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 my-5">
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bring Your Own Key */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
|
使用自己的 API Key
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
||||||
|
您也可以使用自己的 API
|
||||||
|
Key,支持多种服务商。点击聊天面板中的设置图标即可配置。
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
||||||
|
您的 Key
|
||||||
|
仅保存在浏览器本地,不会被存储在服务器上。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-700">
|
||||||
|
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
功能特性
|
||||||
|
</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>LLM驱动的图表创建</strong>
|
||||||
|
:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>基于图像的图表复制</strong>
|
||||||
|
:上传现有图表或图像,让AI自动复制和增强
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>图表历史记录</strong>
|
||||||
|
:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>交互式聊天界面</strong>
|
||||||
|
:与AI实时对话来完善您的图表
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>AWS架构图支持</strong>
|
||||||
|
:专门支持生成AWS架构图
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>动画连接器</strong>
|
||||||
|
:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
示例
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
以下是一些示例提示词及其生成的图表:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Animated Transformer */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
动画Transformer连接器
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
<strong>提示词:</strong> 给我一个带有
|
||||||
|
<strong>动画连接器</strong>的Transformer架构图。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/animated_connectors.svg"
|
||||||
|
alt="带动画连接器的Transformer架构"
|
||||||
|
width={480}
|
||||||
|
height={360}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cloud Architecture Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
GCP架构图
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 使用
|
||||||
|
<strong>GCP图标</strong>
|
||||||
|
生成一个GCP架构图。用户连接到托管在实例上的前端。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/gcp_demo.svg"
|
||||||
|
alt="GCP架构图"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
AWS架构图
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 使用
|
||||||
|
<strong>AWS图标</strong>
|
||||||
|
生成一个AWS架构图。用户连接到托管在实例上的前端。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/aws_demo.svg"
|
||||||
|
alt="AWS架构图"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Azure架构图
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong> 使用
|
||||||
|
<strong>Azure图标</strong>
|
||||||
|
生成一个Azure架构图。用户连接到托管在实例上的前端。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/azure_demo.svg"
|
||||||
|
alt="Azure架构图"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
猫咪素描
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>提示词:</strong>{" "}
|
||||||
|
给我画一只可爱的猫。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/cat_demo.svg"
|
||||||
|
alt="猫咪绘图"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
工作原理
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4">本应用使用以下技术:</p>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>Next.js</strong>:用于前端框架和路由
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Vercel AI SDK</strong>(<code>ai</code> +{" "}
|
||||||
|
<code>@ai-sdk/*</code>
|
||||||
|
):用于流式AI响应和多提供商支持
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>react-drawio</strong>:用于图表表示和操作
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Multi-Provider Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
多提供商支持
|
||||||
|
</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
字节跳动豆包
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>AWS Bedrock(默认)</li>
|
||||||
|
<li>
|
||||||
|
OpenAI / OpenAI兼容API(通过{" "}
|
||||||
|
<code>OPENAI_BASE_URL</code>)
|
||||||
|
</li>
|
||||||
|
<li>Anthropic</li>
|
||||||
|
<li>Google AI</li>
|
||||||
|
<li>Azure OpenAI</li>
|
||||||
|
<li>Ollama</li>
|
||||||
|
<li>OpenRouter</li>
|
||||||
|
<li>DeepSeek</li>
|
||||||
|
<li>SiliconFlow</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
注意:<code>claude-sonnet-4-5</code>{" "}
|
||||||
|
已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
支持与联系
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4 font-semibold">
|
||||||
|
特别感谢{" "}
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
字节跳动豆包
|
||||||
|
</a>{" "}
|
||||||
|
为本站提供 API Token 支持!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
如果您觉得这个项目有用,请考虑{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
赞助
|
||||||
|
</a>{" "}
|
||||||
|
来帮助托管在线演示站点!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 mt-2">
|
||||||
|
如需支持或咨询,请在{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
GitHub仓库
|
||||||
|
</a>{" "}
|
||||||
|
上提交issue或联系:me[at]jiang.jp
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
打开编辑器
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<p className="text-center text-gray-600 text-sm">
|
||||||
|
Next AI Draw.io - 开源AI驱动的图表生成器
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
432
app/[lang]/about/ja/page.tsx
Normal file
432
app/[lang]/about/ja/page.tsx
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import Image from "next/image"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "概要 - Next AI Draw.io",
|
||||||
|
description:
|
||||||
|
"AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
|
||||||
|
keywords: [
|
||||||
|
"AIダイアグラム",
|
||||||
|
"draw.io",
|
||||||
|
"AWSアーキテクチャ",
|
||||||
|
"GCPダイアグラム",
|
||||||
|
"Azureダイアグラム",
|
||||||
|
"LLM",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000) {
|
||||||
|
return `${num / 1000}k`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AboutJA() {
|
||||||
|
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
|
||||||
|
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
|
||||||
|
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<header className="bg-white border-b border-gray-200">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xl font-bold text-gray-900 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Next AI Draw.io
|
||||||
|
</Link>
|
||||||
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
エディタ
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/about/ja"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
概要
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
aria-label="GitHubで見る"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article className="prose prose-lg max-w-none">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||||
|
Next AI Draw.io
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
|
AI搭載のダイアグラム作成ツール -
|
||||||
|
チャット、描画、可視化
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
|
||||||
|
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
|
ByteDance Doubao提供
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Story */}
|
||||||
|
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
||||||
|
<p>
|
||||||
|
朗報です!
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-semibold text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
ByteDance Doubao
|
||||||
|
</a>
|
||||||
|
様のご支援により、デモサイトでは強力な{" "}
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
K2-thinking
|
||||||
|
</span>{" "}
|
||||||
|
モデルを利用できるようになり、より高品質なダイアグラム生成が可能になりました。リンクから登録すると、すべてのモデルで使える{" "}
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
50万トークン
|
||||||
|
</span>
|
||||||
|
が無料でもらえます!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Limits */}
|
||||||
|
<p className="text-sm text-gray-600 mb-3">
|
||||||
|
現在の使用制限:
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-5">
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(dailyRequestLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
リクエスト/日
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(dailyTokenLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
トークン/日
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(tpmLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
トークン/分
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 my-5">
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bring Your Own Key */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
|
自分のAPIキーを使用
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
||||||
|
お好みのプロバイダーで自分のAPIキーを使用することもできます。チャットパネルの設定アイコンをクリックして設定してください。
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
||||||
|
キーはブラウザのローカルに保存され、サーバーには保存されません。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-700">
|
||||||
|
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
機能
|
||||||
|
</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>LLM搭載のダイアグラム作成</strong>
|
||||||
|
:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>画像ベースのダイアグラム複製</strong>
|
||||||
|
:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>ダイアグラム履歴</strong>
|
||||||
|
:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
インタラクティブなチャットインターフェース
|
||||||
|
</strong>
|
||||||
|
:AIとリアルタイムでコミュニケーションしてダイアグラムを改善
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
AWSアーキテクチャダイアグラムサポート
|
||||||
|
</strong>
|
||||||
|
:AWSアーキテクチャダイアグラムの生成を専門的にサポート
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>アニメーションコネクタ</strong>
|
||||||
|
:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
例
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Animated Transformer */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
アニメーションTransformerコネクタ
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>アニメーションコネクタ</strong>
|
||||||
|
付きのTransformerアーキテクチャ図を作成してください。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/animated_connectors.svg"
|
||||||
|
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
|
||||||
|
width={480}
|
||||||
|
height={360}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cloud Architecture Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
GCPアーキテクチャ図
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>GCPアイコン</strong>
|
||||||
|
を使用してGCPアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/gcp_demo.svg"
|
||||||
|
alt="GCPアーキテクチャ図"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
AWSアーキテクチャ図
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>AWSアイコン</strong>
|
||||||
|
を使用してAWSアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/aws_demo.svg"
|
||||||
|
alt="AWSアーキテクチャ図"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Azureアーキテクチャ図
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
<strong>Azureアイコン</strong>
|
||||||
|
を使用してAzureアーキテクチャ図を生成してください。ユーザーがインスタンス上でホストされているフロントエンドに接続します。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/azure_demo.svg"
|
||||||
|
alt="Azureアーキテクチャ図"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
猫のスケッチ
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>プロンプト:</strong>{" "}
|
||||||
|
かわいい猫を描いてください。
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/cat_demo.svg"
|
||||||
|
alt="猫の絵"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
仕組み
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4">
|
||||||
|
本アプリケーションは以下の技術を使用しています:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>Next.js</strong>
|
||||||
|
:フロントエンドフレームワークとルーティング
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Vercel AI SDK</strong>(<code>ai</code> +{" "}
|
||||||
|
<code>@ai-sdk/*</code>
|
||||||
|
):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>react-drawio</strong>
|
||||||
|
:ダイアグラムの表現と操作
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Multi-Provider Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
マルチプロバイダーサポート
|
||||||
|
</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-1">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
ByteDance Doubao
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>AWS Bedrock(デフォルト)</li>
|
||||||
|
<li>
|
||||||
|
OpenAI / OpenAI互換API(<code>OPENAI_BASE_URL</code>
|
||||||
|
経由)
|
||||||
|
</li>
|
||||||
|
<li>Anthropic</li>
|
||||||
|
<li>Google AI</li>
|
||||||
|
<li>Azure OpenAI</li>
|
||||||
|
<li>Ollama</li>
|
||||||
|
<li>OpenRouter</li>
|
||||||
|
<li>DeepSeek</li>
|
||||||
|
<li>SiliconFlow</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
注:<code>claude-sonnet-4-5</code>
|
||||||
|
はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
サポート&お問い合わせ
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4 font-semibold">
|
||||||
|
デモサイトのAPIトークン使用を支援してくださった{" "}
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
ByteDance Doubao
|
||||||
|
</a>{" "}
|
||||||
|
様に、心より感謝申し上げます。
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
スポンサー
|
||||||
|
</a>{" "}
|
||||||
|
をご検討ください!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 mt-2">
|
||||||
|
サポートやお問い合わせについては、{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
GitHubリポジトリ
|
||||||
|
</a>{" "}
|
||||||
|
でissueを開くか、ご連絡ください:me[at]jiang.jp
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
エディタを開く
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<p className="text-center text-gray-600 text-sm">
|
||||||
|
Next AI Draw.io -
|
||||||
|
オープンソースAI搭載ダイアグラムジェネレーター
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
453
app/[lang]/about/page.tsx
Normal file
453
app/[lang]/about/page.tsx
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import Image from "next/image"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
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.",
|
||||||
|
keywords: [
|
||||||
|
"AI diagram",
|
||||||
|
"draw.io",
|
||||||
|
"AWS architecture",
|
||||||
|
"GCP diagram",
|
||||||
|
"Azure diagram",
|
||||||
|
"LLM",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000) {
|
||||||
|
return `${num / 1000}k`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function About() {
|
||||||
|
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
|
||||||
|
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
|
||||||
|
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Navigation */}
|
||||||
|
<header className="bg-white border-b border-gray-200">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xl font-bold text-gray-900 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Next AI Draw.io
|
||||||
|
</Link>
|
||||||
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
Editor
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="text-blue-600 font-semibold"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
aria-label="View on GitHub"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article className="prose prose-lg max-w-none">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||||
|
Next AI Draw.io
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 font-medium">
|
||||||
|
AI-Powered Diagram Creation Tool - Chat, Draw,
|
||||||
|
Visualize
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
|
||||||
|
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
|
Sponsored by ByteDance Doubao
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Story */}
|
||||||
|
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
|
||||||
|
<p>
|
||||||
|
Great news! Thanks to the generous
|
||||||
|
sponsorship from{" "}
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-semibold text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
ByteDance Doubao
|
||||||
|
</a>
|
||||||
|
, the demo site now uses the powerful{" "}
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
K2-thinking
|
||||||
|
</span>{" "}
|
||||||
|
model for better diagram generation! Sign up
|
||||||
|
via the link to get{" "}
|
||||||
|
<span className="font-semibold text-amber-700">
|
||||||
|
500K free tokens
|
||||||
|
</span>{" "}
|
||||||
|
for all models!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Limits */}
|
||||||
|
<p className="text-sm text-gray-600 mb-3">
|
||||||
|
Please note the current usage limits:
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-5">
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(dailyRequestLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
requests/day
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(dailyTokenLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
tokens/day
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-white/60 rounded-lg">
|
||||||
|
<p className="text-lg font-bold text-amber-600">
|
||||||
|
{formatNumber(tpmLimit)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
tokens/min
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 my-5">
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bring Your Own Key */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h4 className="text-base font-bold text-gray-900 mb-2">
|
||||||
|
Bring Your Own API Key
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
|
||||||
|
You can also use your own API key with any
|
||||||
|
supported provider. Click the Settings icon
|
||||||
|
in the chat panel to configure your provider
|
||||||
|
and API key.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 max-w-md mx-auto">
|
||||||
|
Your key is stored locally in your browser
|
||||||
|
and is never stored on the server.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-700">
|
||||||
|
A Next.js web application that integrates AI
|
||||||
|
capabilities with draw.io diagrams. Create, modify, and
|
||||||
|
enhance diagrams through natural language commands and
|
||||||
|
AI-assisted visualization.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
Features
|
||||||
|
</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-700 space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>LLM-Powered Diagram Creation</strong>:
|
||||||
|
Leverage Large Language Models to create and
|
||||||
|
manipulate draw.io diagrams directly through natural
|
||||||
|
language commands
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Image-Based Diagram Replication</strong>:
|
||||||
|
Upload existing diagrams or images and have the AI
|
||||||
|
replicate and enhance them automatically
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Diagram History</strong>: Comprehensive
|
||||||
|
version control that tracks all changes, allowing
|
||||||
|
you to view and restore previous versions of your
|
||||||
|
diagrams before the AI editing
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Interactive Chat Interface</strong>:
|
||||||
|
Communicate with AI to refine your diagrams in
|
||||||
|
real-time
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>AWS Architecture Diagram Support</strong>:
|
||||||
|
Specialized support for generating AWS architecture
|
||||||
|
diagrams
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Animated Connectors</strong>: Create dynamic
|
||||||
|
and animated connectors between diagram elements for
|
||||||
|
better visualization
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Examples */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
Examples
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
Here are some example prompts and their generated
|
||||||
|
diagrams:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Animated Transformer */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Animated Transformer Connectors
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
<strong>Prompt:</strong> Give me an{" "}
|
||||||
|
<strong>animated connector</strong> diagram of
|
||||||
|
transformer's architecture.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/animated_connectors.svg"
|
||||||
|
alt="Transformer Architecture with Animated Connectors"
|
||||||
|
width={480}
|
||||||
|
height={360}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cloud Architecture Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
GCP Architecture Diagram
|
||||||
|
</h3>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/gcp_demo.svg"
|
||||||
|
alt="GCP Architecture Diagram"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
AWS Architecture Diagram
|
||||||
|
</h3>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/aws_demo.svg"
|
||||||
|
alt="AWS Architecture Diagram"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Azure Architecture Diagram
|
||||||
|
</h3>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/azure_demo.svg"
|
||||||
|
alt="Azure Architecture Diagram"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Cat Sketch
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 text-sm mb-4">
|
||||||
|
<strong>Prompt:</strong> Draw a cute cat for
|
||||||
|
me.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/cat_demo.svg"
|
||||||
|
alt="Cat Drawing"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
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">
|
||||||
|
<li>
|
||||||
|
<strong>Next.js</strong>: For the frontend framework
|
||||||
|
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>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Multi-Provider Support */}
|
||||||
|
<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">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
ByteDance Doubao
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>AWS Bedrock (default)</li>
|
||||||
|
<li>
|
||||||
|
OpenAI / OpenAI-compatible APIs (via{" "}
|
||||||
|
<code>OPENAI_BASE_URL</code>)
|
||||||
|
</li>
|
||||||
|
<li>Anthropic</li>
|
||||||
|
<li>Google AI</li>
|
||||||
|
<li>Azure OpenAI</li>
|
||||||
|
<li>Ollama</li>
|
||||||
|
<li>OpenRouter</li>
|
||||||
|
<li>DeepSeek</li>
|
||||||
|
<li>SiliconFlow</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-gray-700 mt-4">
|
||||||
|
Note that <code>claude-sonnet-4-5</code> has trained on
|
||||||
|
draw.io diagrams with AWS logos, so if you want to
|
||||||
|
create AWS architecture diagrams, this is the best
|
||||||
|
choice.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Support */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
|
||||||
|
Support & Contact
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mb-4 font-semibold">
|
||||||
|
Special thanks to{" "}
|
||||||
|
<a
|
||||||
|
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
ByteDance Doubao
|
||||||
|
</a>{" "}
|
||||||
|
for sponsoring the API token usage of the demo site!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
If you find this project useful, please consider{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
sponsoring
|
||||||
|
</a>{" "}
|
||||||
|
to help host the live demo site!
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 mt-2">
|
||||||
|
For support or inquiries, please open an issue on the{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
GitHub repository
|
||||||
|
</a>{" "}
|
||||||
|
or contact: me[at]jiang.jp
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Open Editor
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<p className="text-center text-gray-600 text-sm">
|
||||||
|
Next AI Draw.io - Open Source AI-Powered Diagram
|
||||||
|
Generator
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
172
app/[lang]/layout.tsx
Normal file
172
app/[lang]/layout.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { GoogleAnalytics } from "@next/third-parties/google"
|
||||||
|
import type { Metadata, Viewport } from "next"
|
||||||
|
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
import { DiagramProvider } from "@/contexts/diagram-context"
|
||||||
|
import { DictionaryProvider } from "@/hooks/use-dictionary"
|
||||||
|
import type { Locale } from "@/lib/i18n/config"
|
||||||
|
import { i18n } from "@/lib/i18n/config"
|
||||||
|
import { getDictionary, hasLocale } from "@/lib/i18n/dictionaries"
|
||||||
|
|
||||||
|
import "../globals.css"
|
||||||
|
|
||||||
|
const plusJakarta = Plus_Jakarta_Sans({
|
||||||
|
variable: "--font-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "600", "700"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
variable: "--font-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
userScalable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate static params for all locales
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return i18n.locales.map((locale) => ({ lang: locale }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate metadata per locale
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ lang: string }>
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { lang: rawLang } = await params
|
||||||
|
const lang = (rawLang in { en: 1, zh: 1, ja: 1 } ? rawLang : "en") as Locale
|
||||||
|
|
||||||
|
// Default to English metadata
|
||||||
|
const titles: Record<Locale, string> = {
|
||||||
|
en: "Next AI Draw.io - AI-Powered Diagram Generator",
|
||||||
|
zh: "Next AI Draw.io - AI powered diagram generator",
|
||||||
|
ja: "Next AI Draw.io - AI-powered diagram generator",
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptions: Record<Locale, string> = {
|
||||||
|
en: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
||||||
|
zh: "Use AI to create AWS architecture diagrams, flowcharts, and technical diagrams. Free online tool integrated with draw.io and AI assistance for professional diagram creation.",
|
||||||
|
ja: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Create professional diagrams with a free online tool that integrates draw.io with an AI assistant.",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: titles[lang],
|
||||||
|
description: descriptions[lang],
|
||||||
|
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" }],
|
||||||
|
creator: "Next AI Draw.io",
|
||||||
|
publisher: "Next AI Draw.io",
|
||||||
|
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
||||||
|
openGraph: {
|
||||||
|
title: titles[lang],
|
||||||
|
description: descriptions[lang],
|
||||||
|
type: "website",
|
||||||
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
|
siteName: "Next AI Draw.io",
|
||||||
|
locale: lang === "zh" ? "zh_CN" : lang === "ja" ? "ja_JP" : "en_US",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/architecture.png",
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Next AI Draw.io - AI-powered diagram creation tool",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: titles[lang],
|
||||||
|
description: descriptions[lang],
|
||||||
|
images: ["/architecture.png"],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-video-preview": -1,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.ico",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
en: "/en",
|
||||||
|
zh: "/zh",
|
||||||
|
ja: "/ja",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode
|
||||||
|
params: Promise<{ lang: string }>
|
||||||
|
}>) {
|
||||||
|
const { lang } = await params
|
||||||
|
if (!hasLocale(lang)) notFound()
|
||||||
|
const validLang = lang as Locale
|
||||||
|
const dictionary = await getDictionary(validLang)
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
name: "Next AI Draw.io",
|
||||||
|
applicationCategory: "DesignApplication",
|
||||||
|
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.",
|
||||||
|
url: "https://next-ai-drawio.jiang.jp",
|
||||||
|
inLanguage: validLang,
|
||||||
|
offers: {
|
||||||
|
"@type": "Offer",
|
||||||
|
price: "0",
|
||||||
|
priceCurrency: "USD",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={validLang} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<DictionaryProvider dictionary={dictionary}>
|
||||||
|
<DiagramProvider>{children}</DiagramProvider>
|
||||||
|
</DictionaryProvider>
|
||||||
|
</body>
|
||||||
|
{process.env.NEXT_PUBLIC_GA_ID && (
|
||||||
|
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
|
||||||
|
)}
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
268
app/[lang]/page.tsx
Normal file
268
app/[lang]/page.tsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"use client"
|
||||||
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { DrawIoEmbed } from "react-drawio"
|
||||||
|
import type { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
|
import ChatPanel from "@/components/chat-panel"
|
||||||
|
import { STORAGE_CLOSE_PROTECTION_KEY } from "@/components/settings-dialog"
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable"
|
||||||
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
|
||||||
|
const drawioBaseUrl =
|
||||||
|
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const {
|
||||||
|
drawioRef,
|
||||||
|
handleDiagramExport,
|
||||||
|
onDrawioLoad,
|
||||||
|
resetDrawioReady,
|
||||||
|
saveDiagramToStorage,
|
||||||
|
showSaveDialog,
|
||||||
|
setShowSaveDialog,
|
||||||
|
} = useDiagram()
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
// Extract current language from pathname (e.g., "/zh/about" → "zh")
|
||||||
|
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||||
|
const [darkMode, setDarkMode] = useState(false)
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
const [closeProtection, setCloseProtection] = useState(false)
|
||||||
|
|
||||||
|
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
const isSavingRef = useRef(false)
|
||||||
|
const mouseOverDrawioRef = useRef(false)
|
||||||
|
const isMobileRef = useRef(false)
|
||||||
|
|
||||||
|
// Reset saving flag when dialog closes (with delay to ignore lingering save events from draw.io)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSaveDialog) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
isSavingRef.current = false
|
||||||
|
}, 1000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [showSaveDialog])
|
||||||
|
|
||||||
|
// Handle save from draw.io's built-in save button
|
||||||
|
// Note: draw.io sends save events for various reasons (focus changes, etc.)
|
||||||
|
// We use mouse position to determine if the user is interacting with draw.io
|
||||||
|
const handleDrawioSave = useCallback(() => {
|
||||||
|
if (!mouseOverDrawioRef.current) return
|
||||||
|
if (isSavingRef.current) return
|
||||||
|
isSavingRef.current = true
|
||||||
|
setShowSaveDialog(true)
|
||||||
|
}, [setShowSaveDialog])
|
||||||
|
|
||||||
|
// Load preferences from localStorage after mount
|
||||||
|
useEffect(() => {
|
||||||
|
// Restore saved locale and redirect if needed
|
||||||
|
const savedLocale = localStorage.getItem("next-ai-draw-io-locale")
|
||||||
|
if (savedLocale && i18n.locales.includes(savedLocale as Locale)) {
|
||||||
|
const pathParts = pathname.split("/").filter(Boolean)
|
||||||
|
const currentLocale = pathParts[0]
|
||||||
|
if (currentLocale !== savedLocale) {
|
||||||
|
pathParts[0] = savedLocale
|
||||||
|
router.replace(`/${pathParts.join("/")}`)
|
||||||
|
return // Wait for redirect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedUi = localStorage.getItem("drawio-theme")
|
||||||
|
if (savedUi === "min" || savedUi === "sketch") {
|
||||||
|
setDrawioUi(savedUi)
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
|
||||||
|
if (savedDarkMode !== null) {
|
||||||
|
const isDark = savedDarkMode === "true"
|
||||||
|
setDarkMode(isDark)
|
||||||
|
document.documentElement.classList.toggle("dark", isDark)
|
||||||
|
} else {
|
||||||
|
const prefersDark = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)",
|
||||||
|
).matches
|
||||||
|
setDarkMode(prefersDark)
|
||||||
|
document.documentElement.classList.toggle("dark", prefersDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedCloseProtection = localStorage.getItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
)
|
||||||
|
if (savedCloseProtection === "true") {
|
||||||
|
setCloseProtection(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoaded(true)
|
||||||
|
}, [pathname, router])
|
||||||
|
|
||||||
|
const handleDarkModeChange = async () => {
|
||||||
|
await saveDiagramToStorage()
|
||||||
|
const newValue = !darkMode
|
||||||
|
setDarkMode(newValue)
|
||||||
|
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
|
||||||
|
document.documentElement.classList.toggle("dark", newValue)
|
||||||
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrawioUiChange = async () => {
|
||||||
|
await saveDiagramToStorage()
|
||||||
|
const newUi = drawioUi === "min" ? "sketch" : "min"
|
||||||
|
localStorage.setItem("drawio-theme", newUi)
|
||||||
|
setDrawioUi(newUi)
|
||||||
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mobile - save diagram and reset draw.io before crossing breakpoint
|
||||||
|
const isInitialRenderRef = useRef(true)
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
const newIsMobile = window.innerWidth < 768
|
||||||
|
if (
|
||||||
|
!isInitialRenderRef.current &&
|
||||||
|
newIsMobile !== isMobileRef.current
|
||||||
|
) {
|
||||||
|
saveDiagramToStorage().catch(() => {})
|
||||||
|
resetDrawioReady()
|
||||||
|
}
|
||||||
|
isMobileRef.current = newIsMobile
|
||||||
|
isInitialRenderRef.current = false
|
||||||
|
setIsMobile(newIsMobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMobile()
|
||||||
|
window.addEventListener("resize", checkMobile)
|
||||||
|
return () => window.removeEventListener("resize", checkMobile)
|
||||||
|
}, [saveDiagramToStorage, resetDrawioReady])
|
||||||
|
|
||||||
|
const toggleChatPanel = () => {
|
||||||
|
const panel = chatPanelRef.current
|
||||||
|
if (panel) {
|
||||||
|
if (panel.isCollapsed()) {
|
||||||
|
panel.expand()
|
||||||
|
setIsChatVisible(true)
|
||||||
|
} else {
|
||||||
|
panel.collapse()
|
||||||
|
setIsChatVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcut for toggling chat panel
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
||||||
|
event.preventDefault()
|
||||||
|
toggleChatPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Show confirmation dialog when user tries to leave the page
|
||||||
|
useEffect(() => {
|
||||||
|
if (!closeProtection) return
|
||||||
|
|
||||||
|
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", handleBeforeUnload)
|
||||||
|
return () =>
|
||||||
|
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||||
|
}, [closeProtection])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen bg-background relative overflow-hidden">
|
||||||
|
<ResizablePanelGroup
|
||||||
|
id="main-panel-group"
|
||||||
|
direction={isMobile ? "vertical" : "horizontal"}
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
<ResizablePanel
|
||||||
|
id="drawio-panel"
|
||||||
|
defaultSize={isMobile ? 50 : 67}
|
||||||
|
minSize={20}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`h-full relative ${
|
||||||
|
isMobile ? "p-1" : "p-2"
|
||||||
|
}`}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
mouseOverDrawioRef.current = true
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
mouseOverDrawioRef.current = false
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
|
||||||
|
{isLoaded ? (
|
||||||
|
<DrawIoEmbed
|
||||||
|
key={`${drawioUi}-${darkMode}-${currentLang}`}
|
||||||
|
ref={drawioRef}
|
||||||
|
onExport={handleDiagramExport}
|
||||||
|
onLoad={onDrawioLoad}
|
||||||
|
onSave={handleDrawioSave}
|
||||||
|
baseUrl={drawioBaseUrl}
|
||||||
|
urlParameters={{
|
||||||
|
ui: drawioUi,
|
||||||
|
spin: true,
|
||||||
|
libraries: false,
|
||||||
|
saveAndExit: false,
|
||||||
|
noExitBtn: true,
|
||||||
|
dark: darkMode,
|
||||||
|
lang: currentLang,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex items-center justify-center bg-background">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
|
{/* Chat Panel */}
|
||||||
|
<ResizablePanel
|
||||||
|
key={isMobile ? "mobile" : "desktop"}
|
||||||
|
id="chat-panel"
|
||||||
|
ref={chatPanelRef}
|
||||||
|
defaultSize={isMobile ? 50 : 33}
|
||||||
|
minSize={isMobile ? 20 : 15}
|
||||||
|
maxSize={isMobile ? 80 : 50}
|
||||||
|
collapsible={!isMobile}
|
||||||
|
collapsedSize={isMobile ? 0 : 3}
|
||||||
|
onCollapse={() => setIsChatVisible(false)}
|
||||||
|
onExpand={() => setIsChatVisible(true)}
|
||||||
|
>
|
||||||
|
<div className={`h-full ${isMobile ? "p-1" : "py-2 pr-2"}`}>
|
||||||
|
<ChatPanel
|
||||||
|
isVisible={isChatVisible}
|
||||||
|
onToggleVisibility={toggleChatPanel}
|
||||||
|
drawioUi={drawioUi}
|
||||||
|
onToggleDrawioUi={handleDrawioUiChange}
|
||||||
|
darkMode={darkMode}
|
||||||
|
onToggleDarkMode={handleDarkModeChange}
|
||||||
|
isMobile={isMobile}
|
||||||
|
onCloseProtectionChange={setCloseProtection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { FaGithub } from "react-icons/fa";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "About - AI-Powered Diagram Generator | Next AI Draw.io",
|
|
||||||
description: "Learn about Next AI Draw.io, a free AI-powered diagram creation tool. Create AWS architecture diagrams, flowcharts, and UML diagrams using Claude Sonnet and GPT-4. No login required.",
|
|
||||||
keywords: ["about AI diagram generator", "diagram tool features", "how to create diagrams", "AI drawing tool capabilities", "draw.io integration"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function About() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
{/* Navigation */}
|
|
||||||
<header className="bg-white border-b border-gray-200">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
|
|
||||||
Next AI Draw.io
|
|
||||||
</Link>
|
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
|
||||||
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
|
|
||||||
Editor
|
|
||||||
</Link>
|
|
||||||
<Link href="/about" className="text-blue-600 font-semibold">
|
|
||||||
About
|
|
||||||
</Link>
|
|
||||||
<a
|
|
||||||
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
|
||||||
aria-label="View on GitHub"
|
|
||||||
>
|
|
||||||
<FaGithub className="w-5 h-5" />
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
||||||
<article>
|
|
||||||
{/* Hero Section */}
|
|
||||||
<header className="mb-12">
|
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
|
||||||
AI-Powered Diagram Generator | Create Professional Diagrams Instantly
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-gray-600">
|
|
||||||
Free, open-source diagram creation tool powered by AI. No login required, no installation needed.
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Introduction */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-4">What is Next AI Draw.io?</h2>
|
|
||||||
<div className="prose prose-lg max-w-none text-gray-700">
|
|
||||||
<p className="mb-4">
|
|
||||||
<strong>Next AI Draw.io</strong> is a free, AI-powered diagram creation tool that integrates seamlessly with draw.io.
|
|
||||||
Generate AWS architecture diagrams, flowcharts, UML diagrams, and technical documentation diagrams using natural language
|
|
||||||
prompts. No login required, no installation needed—start creating professional diagrams instantly in your browser.
|
|
||||||
</p>
|
|
||||||
<p className="mb-4">
|
|
||||||
Our intelligent diagram generator uses advanced AI models including <strong>Claude Sonnet</strong> and <strong>GPT-4</strong> to
|
|
||||||
understand your requirements and automatically create properly structured diagrams with appropriate symbols, layouts, and connections.
|
|
||||||
Simply describe what you need, upload reference images, or ask the AI to modify existing diagrams with our targeted XML editing feature.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Whether you're a software architect designing system infrastructure, a developer documenting APIs, a business analyst creating
|
|
||||||
process flows, or a student working on technical assignments, Next AI Draw.io makes diagram creation fast, accurate, and effortless.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Key Features */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Key Features</h2>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
AI-Powered Diagram Creation
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Generate diagrams from natural language descriptions using Claude Sonnet or GPT-4.
|
|
||||||
Describe your diagram in plain English, and watch the AI create it with proper symbols,
|
|
||||||
layouts, and connections automatically.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
AWS Architecture Diagrams
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Create professional cloud infrastructure diagrams with AWS-style icons and layouts.
|
|
||||||
Perfect for designing EC2 instances, Lambda functions, S3 buckets, RDS databases, VPCs,
|
|
||||||
and complete AWS solution architectures.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
Image-Based Diagram Replication
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Upload existing diagrams or sketches, and the AI will automatically recreate them in draw.io format.
|
|
||||||
Modify uploaded images by describing the changes you want—the AI handles the rest.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
Diagram History & Version Control
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Access previous versions of your diagrams and restore any version from your session history.
|
|
||||||
Never lose work—every AI modification is saved and can be undone with a single click.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
Targeted XML Editing
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Precise diagram modifications using intelligent XML manipulation. Unlike full diagram regeneration,
|
|
||||||
targeted edits preserve your existing layout while making specific changes, ensuring consistent
|
|
||||||
and predictable results.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<span className="text-blue-600 mr-2">✓</span>
|
|
||||||
Multi-Provider AI Support
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Choose between Claude Sonnet, GPT-4, and other leading AI models for optimal results.
|
|
||||||
Each model has unique strengths—select the one that best fits your diagram complexity and style.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Use Cases */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Popular Use Cases</h2>
|
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
|
||||||
<div className="bg-blue-50 p-6 rounded-lg border border-blue-200">
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">AWS Cloud Architecture</h3>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
Design scalable cloud infrastructure with EC2 instances, Lambda functions, S3 storage,
|
|
||||||
RDS databases, and VPC networking. Perfect for solution architects, cloud engineers,
|
|
||||||
and DevOps teams planning AWS deployments.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 italic">
|
|
||||||
Example: "Create an AWS diagram with an Application Load Balancer, two EC2 instances
|
|
||||||
in different availability zones, an RDS database, and an S3 bucket for static assets."
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-green-50 p-6 rounded-lg border border-green-200">
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">Flowcharts & Process Diagrams</h3>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
Create business process flows, decision trees, workflow diagrams, and algorithm flowcharts
|
|
||||||
for documentation, presentations, and process optimization. Ideal for business analysts,
|
|
||||||
project managers, and operations teams.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 italic">
|
|
||||||
Example: "Draw a flowchart for user authentication: check if user exists, verify password,
|
|
||||||
generate JWT token on success, show error message on failure."
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-purple-50 p-6 rounded-lg border border-purple-200">
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">System Design & UML Diagrams</h3>
|
|
||||||
<p className="text-gray-700 mb-4">
|
|
||||||
Generate system architecture diagrams, class diagrams, sequence diagrams, and
|
|
||||||
entity-relationship diagrams for software projects. Essential for software engineers,
|
|
||||||
system designers, and technical documentation.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 italic">
|
|
||||||
Example: "Create a class diagram for an e-commerce system with User, Product, Order,
|
|
||||||
and Payment classes showing their relationships and key methods."
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* How It Works */}
|
|
||||||
<section className="mb-12 bg-white p-8 rounded-lg border border-gray-200">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">How to Use Next AI Draw.io</h2>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
|
||||||
1
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Open the Editor</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Navigate to the main page and you'll see the draw.io editor with an AI chat panel on the right.
|
|
||||||
No account creation or login required—start immediately.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
|
||||||
2
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Describe Your Diagram</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Type your diagram request in natural language. Be as detailed or as general as you like.
|
|
||||||
You can also upload reference images for the AI to analyze and replicate.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
|
||||||
3
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">AI Generates Your Diagram</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
The AI processes your request and automatically creates your diagram in seconds.
|
|
||||||
Watch as it appears in the editor with proper symbols, layouts, and connections.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mr-4">
|
|
||||||
4
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Refine and Export</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Request modifications using the chat, manually edit in draw.io, or export to PNG, SVG,
|
|
||||||
or XML format. Access diagram history to restore previous versions anytime.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Benefits */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Why Choose Next AI Draw.io?</h2>
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">⚡</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Save Time</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Create complex diagrams in seconds instead of hours. No more dragging, aligning,
|
|
||||||
or searching for the right symbols—AI handles it all.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">🎯</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Precision Editing</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
Targeted XML editing ensures changes are precise and predictable, unlike tools
|
|
||||||
that regenerate entire diagrams and lose your layout.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">🆓</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Completely Free</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
No subscriptions, no usage limits, no hidden costs. Open-source and free forever.
|
|
||||||
Use it for personal projects, work, or education.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="flex-shrink-0 text-blue-600 text-2xl mr-3">🔒</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Privacy First</h3>
|
|
||||||
<p className="text-gray-700">
|
|
||||||
No account required means your diagrams stay private. Work on sensitive
|
|
||||||
architecture designs without worrying about data storage or privacy policies.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<section className="bg-blue-600 text-white p-8 rounded-lg text-center">
|
|
||||||
<h2 className="text-3xl font-bold mb-4">Ready to Create Your First AI Diagram?</h2>
|
|
||||||
<p className="text-xl mb-6">
|
|
||||||
Start generating professional diagrams in seconds. No signup required.
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="inline-block bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors"
|
|
||||||
>
|
|
||||||
Open Editor
|
|
||||||
</Link>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="bg-white border-t border-gray-200 mt-16">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<div className="text-center text-gray-600 text-sm">
|
|
||||||
<p className="mb-2">
|
|
||||||
Next AI Draw.io - Free AI-Powered Diagram Generator
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Perfect for developers, architects, students, and business analysts.
|
|
||||||
Open source. No login required.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -81,16 +81,15 @@ Contains the actual diagram data.
|
|||||||
|
|
||||||
## Root Cell Container: `<root>`
|
## Root Cell Container: `<root>`
|
||||||
|
|
||||||
Contains all the cells in the diagram.
|
Contains all the cells in the diagram. **Note:** When generating diagrams, you only need to provide the mxCell elements - the root container and root cells (id="0", id="1") are added automatically.
|
||||||
|
|
||||||
**Example:**
|
**Internal structure (auto-generated):**
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<root>
|
<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/> <!-- Auto-added -->
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/> <!-- Auto-added -->
|
||||||
|
<!-- Your mxCell elements go here (start from id="2") -->
|
||||||
<!-- Other cells go here -->
|
|
||||||
</root>
|
</root>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -203,14 +202,15 @@ Draw.io files contain two special cells that are always present:
|
|||||||
1. **Root Cell** (id = "0"): The parent of all cells
|
1. **Root Cell** (id = "0"): The parent of all cells
|
||||||
2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells
|
2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells
|
||||||
|
|
||||||
## Tips for Manually Creating Draw.io XML
|
## Tips for Creating Draw.io XML
|
||||||
|
|
||||||
1. Start with the basic structure (`mxfile`, `diagram`, `mxGraphModel`, `root`)
|
1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically
|
||||||
2. Always include the two special cells (id = "0" and id = "1")
|
2. Start IDs from "2" (id="0" and id="1" are reserved for root cells)
|
||||||
3. Assign unique and sequential IDs to all cells
|
3. Assign unique and sequential IDs to all cells
|
||||||
4. Define parent relationships correctly
|
4. Define parent relationships correctly (use parent="1" for top-level shapes)
|
||||||
5. Use `mxGeometry` elements to position shapes
|
5. Use `mxGeometry` elements to position shapes
|
||||||
6. For connectors, specify `source` and `target` attributes
|
6. For connectors, specify `source` and `target` attributes
|
||||||
|
7. **CRITICAL: All mxCell elements must be siblings. NEVER nest mxCell inside another mxCell.**
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
@@ -234,12 +234,33 @@ To group elements, create a parent cell and set other cells' `parent` attribute
|
|||||||
|
|
||||||
### Swimlanes
|
### Swimlanes
|
||||||
|
|
||||||
Swimlanes use the `swimlane` shape style:
|
Swimlanes use the `swimlane` shape style. **IMPORTANT: All mxCell elements (swimlanes, steps, and edges) must be siblings under `<root>`. Edges are NOT nested inside swimlanes or steps.**
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<mxCell id="20" value="Swimlane 1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
<root>
|
||||||
<mxGeometry x="200" y="200" width="140" height="120" as="geometry" />
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<!-- Swimlane 1 -->
|
||||||
|
<mxCell id="lane1" value="Frontend" style="swimlane;startSize=30;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="40" width="200" height="300" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
|
<!-- Swimlane 2 -->
|
||||||
|
<mxCell id="lane2" value="Backend" style="swimlane;startSize=30;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="280" y="40" width="200" height="300" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Step inside lane1 (parent="lane1") -->
|
||||||
|
<mxCell id="step1" value="Send Request" style="rounded=1;" vertex="1" parent="lane1">
|
||||||
|
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Step inside lane2 (parent="lane2") -->
|
||||||
|
<mxCell id="step2" value="Process" style="rounded=1;" vertex="1" parent="lane2">
|
||||||
|
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<!-- Edge connecting step1 to step2 (sibling element, NOT nested inside steps) -->
|
||||||
|
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
|
|||||||
10
app/api/config/route.ts
Normal file
10
app/api/config/route.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
accessCodeRequired: !!process.env.ACCESS_CODE_LIST,
|
||||||
|
dailyRequestLimit: Number(process.env.DAILY_REQUEST_LIMIT) || 0,
|
||||||
|
dailyTokenLimit: Number(process.env.DAILY_TOKEN_LIMIT) || 0,
|
||||||
|
tpmLimit: Number(process.env.TPM_LIMIT) || 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
117
app/api/log-feedback/route.ts
Normal file
117
app/api/log-feedback/route.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { randomUUID } from "crypto"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { getLangfuseClient } from "@/lib/langfuse"
|
||||||
|
import { getUserIdFromRequest } from "@/lib/user-id"
|
||||||
|
|
||||||
|
const feedbackSchema = z.object({
|
||||||
|
messageId: z.string().min(1).max(200),
|
||||||
|
feedback: z.enum(["good", "bad"]),
|
||||||
|
sessionId: z.string().min(1).max(200).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const langfuse = getLangfuseClient()
|
||||||
|
if (!langfuse) {
|
||||||
|
return Response.json({ success: true, logged: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = feedbackSchema.parse(await req.json())
|
||||||
|
} catch {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Invalid input" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageId, feedback, sessionId } = data
|
||||||
|
|
||||||
|
// Skip logging if no sessionId - prevents attaching to wrong user's trace
|
||||||
|
if (!sessionId) {
|
||||||
|
return Response.json({ success: true, logged: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID for tracking
|
||||||
|
const userId = getUserIdFromRequest(req)
|
||||||
|
|
||||||
|
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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/api/log-save/route.ts
Normal file
76
app/api/log-save/route.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { randomUUID } from "crypto"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { getLangfuseClient } from "@/lib/langfuse"
|
||||||
|
|
||||||
|
const saveSchema = z.object({
|
||||||
|
filename: z.string().min(1).max(255),
|
||||||
|
format: z.enum(["drawio", "png", "svg"]),
|
||||||
|
sessionId: z.string().min(1).max(200).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const langfuse = getLangfuseClient()
|
||||||
|
if (!langfuse) {
|
||||||
|
return Response.json({ success: true, logged: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = saveSchema.parse(await req.json())
|
||||||
|
} catch {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Invalid input" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filename, format, sessionId } = data
|
||||||
|
|
||||||
|
// Skip logging if no sessionId - prevents attaching to wrong user's trace
|
||||||
|
if (!sessionId) {
|
||||||
|
return Response.json({ success: true, logged: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
317
app/api/validate-model/route.ts
Normal file
317
app/api/validate-model/route.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
||||||
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||||
|
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
|
||||||
|
import { createGateway } from "@ai-sdk/gateway"
|
||||||
|
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
||||||
|
import { createOpenAI } from "@ai-sdk/openai"
|
||||||
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||||
|
import { generateText } from "ai"
|
||||||
|
import { NextResponse } from "next/server"
|
||||||
|
import { createOllama } from "ollama-ai-provider-v2"
|
||||||
|
|
||||||
|
export const runtime = "nodejs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SECURITY: Check if URL points to private/internal network (SSRF protection)
|
||||||
|
* Blocks: localhost, private IPs, link-local, AWS metadata service
|
||||||
|
*/
|
||||||
|
function isPrivateUrl(urlString: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(urlString)
|
||||||
|
const hostname = url.hostname.toLowerCase()
|
||||||
|
|
||||||
|
// Block localhost
|
||||||
|
if (
|
||||||
|
hostname === "localhost" ||
|
||||||
|
hostname === "127.0.0.1" ||
|
||||||
|
hostname === "::1"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block AWS/cloud metadata endpoints
|
||||||
|
if (
|
||||||
|
hostname === "169.254.169.254" ||
|
||||||
|
hostname === "metadata.google.internal"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for private IPv4 ranges
|
||||||
|
const ipv4Match = hostname.match(
|
||||||
|
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
|
||||||
|
)
|
||||||
|
if (ipv4Match) {
|
||||||
|
const [, a, b] = ipv4Match.map(Number)
|
||||||
|
// 10.0.0.0/8
|
||||||
|
if (a === 10) return true
|
||||||
|
// 172.16.0.0/12
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return true
|
||||||
|
// 192.168.0.0/16
|
||||||
|
if (a === 192 && b === 168) return true
|
||||||
|
// 169.254.0.0/16 (link-local)
|
||||||
|
if (a === 169 && b === 254) return true
|
||||||
|
// 127.0.0.0/8 (loopback)
|
||||||
|
if (a === 127) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block common internal hostnames
|
||||||
|
if (
|
||||||
|
hostname.endsWith(".local") ||
|
||||||
|
hostname.endsWith(".internal") ||
|
||||||
|
hostname.endsWith(".localhost")
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
// Invalid URL - block it
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidateRequest {
|
||||||
|
provider: string
|
||||||
|
apiKey: string
|
||||||
|
baseUrl?: string
|
||||||
|
modelId: string
|
||||||
|
// AWS Bedrock specific
|
||||||
|
awsAccessKeyId?: string
|
||||||
|
awsSecretAccessKey?: string
|
||||||
|
awsRegion?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const body: ValidateRequest = await req.json()
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
apiKey,
|
||||||
|
baseUrl,
|
||||||
|
modelId,
|
||||||
|
awsAccessKeyId,
|
||||||
|
awsSecretAccessKey,
|
||||||
|
awsRegion,
|
||||||
|
} = body
|
||||||
|
|
||||||
|
if (!provider || !modelId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ valid: false, error: "Provider and model ID are required" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECURITY: Block SSRF attacks via custom baseUrl
|
||||||
|
if (baseUrl && isPrivateUrl(baseUrl)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ valid: false, error: "Invalid base URL" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate credentials based on provider
|
||||||
|
if (provider === "bedrock") {
|
||||||
|
if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
valid: false,
|
||||||
|
error: "AWS credentials (Access Key ID, Secret Access Key, Region) are required",
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ valid: false, error: "API key is required" },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let model: any
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case "openai": {
|
||||||
|
const openai = createOpenAI({
|
||||||
|
apiKey,
|
||||||
|
...(baseUrl && { baseURL: baseUrl }),
|
||||||
|
})
|
||||||
|
model = openai.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "anthropic": {
|
||||||
|
const anthropic = createAnthropic({
|
||||||
|
apiKey,
|
||||||
|
baseURL: baseUrl || "https://api.anthropic.com/v1",
|
||||||
|
})
|
||||||
|
model = anthropic(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "google": {
|
||||||
|
const google = createGoogleGenerativeAI({
|
||||||
|
apiKey,
|
||||||
|
...(baseUrl && { baseURL: baseUrl }),
|
||||||
|
})
|
||||||
|
model = google(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "azure": {
|
||||||
|
const azure = createOpenAI({
|
||||||
|
apiKey,
|
||||||
|
baseURL: baseUrl,
|
||||||
|
})
|
||||||
|
model = azure.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "bedrock": {
|
||||||
|
const bedrock = createAmazonBedrock({
|
||||||
|
accessKeyId: awsAccessKeyId,
|
||||||
|
secretAccessKey: awsSecretAccessKey,
|
||||||
|
region: awsRegion,
|
||||||
|
})
|
||||||
|
model = bedrock(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "openrouter": {
|
||||||
|
const openrouter = createOpenRouter({
|
||||||
|
apiKey,
|
||||||
|
...(baseUrl && { baseURL: baseUrl }),
|
||||||
|
})
|
||||||
|
model = openrouter(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "deepseek": {
|
||||||
|
if (baseUrl || apiKey) {
|
||||||
|
const ds = createDeepSeek({
|
||||||
|
apiKey,
|
||||||
|
...(baseUrl && { baseURL: baseUrl }),
|
||||||
|
})
|
||||||
|
model = ds(modelId)
|
||||||
|
} else {
|
||||||
|
model = deepseek(modelId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "siliconflow": {
|
||||||
|
const sf = createOpenAI({
|
||||||
|
apiKey,
|
||||||
|
baseURL: baseUrl || "https://api.siliconflow.com/v1",
|
||||||
|
})
|
||||||
|
model = sf.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ollama": {
|
||||||
|
const ollama = createOllama({
|
||||||
|
baseURL: baseUrl || "http://localhost:11434",
|
||||||
|
})
|
||||||
|
model = ollama(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "gateway": {
|
||||||
|
const gw = createGateway({
|
||||||
|
apiKey,
|
||||||
|
...(baseUrl && { baseURL: baseUrl }),
|
||||||
|
})
|
||||||
|
model = gw(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "edgeone": {
|
||||||
|
// EdgeOne uses OpenAI-compatible API via Edge Functions
|
||||||
|
// Need to pass cookies for EdgeOne Pages authentication
|
||||||
|
const cookieHeader = req.headers.get("cookie") || ""
|
||||||
|
const edgeone = createOpenAI({
|
||||||
|
apiKey: "edgeone", // EdgeOne doesn't require API key
|
||||||
|
baseURL: baseUrl || "/api/edgeai",
|
||||||
|
headers: {
|
||||||
|
cookie: cookieHeader,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
model = edgeone.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "sglang": {
|
||||||
|
// SGLang is OpenAI-compatible
|
||||||
|
const sglang = createOpenAI({
|
||||||
|
apiKey: apiKey || "not-needed",
|
||||||
|
baseURL: baseUrl || "http://127.0.0.1:8000/v1",
|
||||||
|
})
|
||||||
|
model = sglang.chat(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "doubao": {
|
||||||
|
// ByteDance Doubao uses DeepSeek-compatible API
|
||||||
|
const doubao = createDeepSeek({
|
||||||
|
apiKey,
|
||||||
|
baseURL:
|
||||||
|
baseUrl || "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
})
|
||||||
|
model = doubao(modelId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json(
|
||||||
|
{ valid: false, error: `Unknown provider: ${provider}` },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a minimal test request
|
||||||
|
const startTime = Date.now()
|
||||||
|
await generateText({
|
||||||
|
model,
|
||||||
|
prompt: "Say 'OK'",
|
||||||
|
maxOutputTokens: 20,
|
||||||
|
})
|
||||||
|
const responseTime = Date.now() - startTime
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
valid: true,
|
||||||
|
responseTime,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[validate-model] Error:", error)
|
||||||
|
|
||||||
|
let errorMessage = "Validation failed"
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// Extract meaningful error message
|
||||||
|
if (
|
||||||
|
error.message.includes("401") ||
|
||||||
|
error.message.includes("Unauthorized")
|
||||||
|
) {
|
||||||
|
errorMessage = "Invalid API key"
|
||||||
|
} else if (
|
||||||
|
error.message.includes("404") ||
|
||||||
|
error.message.includes("not found")
|
||||||
|
) {
|
||||||
|
errorMessage = "Model not found"
|
||||||
|
} else if (
|
||||||
|
error.message.includes("429") ||
|
||||||
|
error.message.includes("rate limit")
|
||||||
|
) {
|
||||||
|
errorMessage = "Rate limited - try again later"
|
||||||
|
} else if (error.message.includes("ECONNREFUSED")) {
|
||||||
|
errorMessage = "Cannot connect to server"
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message.slice(0, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ valid: false, error: errorMessage },
|
||||||
|
{ status: 200 }, // Return 200 so client can read error message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/api/verify-access-code/route.ts
Normal file
32
app/api/verify-access-code/route.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export async function POST(req: Request) {
|
||||||
|
const accessCodes =
|
||||||
|
process.env.ACCESS_CODE_LIST?.split(",")
|
||||||
|
.map((code) => code.trim())
|
||||||
|
.filter(Boolean) || []
|
||||||
|
|
||||||
|
// If no access codes configured, verification always passes
|
||||||
|
if (accessCodes.length === 0) {
|
||||||
|
return Response.json({
|
||||||
|
valid: true,
|
||||||
|
message: "No access code required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessCodeHeader = req.headers.get("x-access-code")
|
||||||
|
|
||||||
|
if (!accessCodeHeader) {
|
||||||
|
return Response.json(
|
||||||
|
{ valid: false, message: "Access code is required" },
|
||||||
|
{ status: 401 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessCodes.includes(accessCodeHeader)) {
|
||||||
|
return Response.json(
|
||||||
|
{ valid: false, message: "Invalid access code" },
|
||||||
|
{ status: 401 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ valid: true, message: "Access code is valid" })
|
||||||
|
}
|
||||||
406
app/globals.css
406
app/globals.css
@@ -1,14 +1,15 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@plugin "tailwindcss-animate";
|
@plugin "tailwindcss-animate";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-mono);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -45,72 +46,164 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.75rem;
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
/* Clean Light Modern Palette */
|
||||||
|
--background: oklch(0.985 0.002 240);
|
||||||
|
--foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.23 0.02 260);
|
||||||
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.23 0.02 260);
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
/* Dark primary - slightly lighter */
|
||||||
--secondary: oklch(0.97 0 0);
|
--primary: oklch(0.35 0.01 260);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
/* Warm gray secondary */
|
||||||
--accent: oklch(0.97 0 0);
|
--secondary: oklch(0.96 0.005 260);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.35 0.02 260);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
/* Light muted tones */
|
||||||
--input: oklch(0.922 0 0);
|
--muted: oklch(0.965 0.005 260);
|
||||||
--ring: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.5 0.02 260);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
/* Soft lavender accent */
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--accent: oklch(0.94 0.03 280);
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--accent-foreground: oklch(0.35 0.08 270);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
/* Coral destructive */
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--destructive: oklch(0.6 0.2 25);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
/* Subtle borders */
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--border: oklch(0.92 0.01 260);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--input: oklch(0.94 0.01 260);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--ring: oklch(0.25 0.01 260);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
|
/* Chart colors - harmonious palette */
|
||||||
|
--chart-1: oklch(0.55 0.18 265);
|
||||||
|
--chart-2: oklch(0.65 0.15 170);
|
||||||
|
--chart-3: oklch(0.7 0.18 45);
|
||||||
|
--chart-4: oklch(0.6 0.2 330);
|
||||||
|
--chart-5: oklch(0.5 0.15 200);
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar: oklch(0.99 0.002 260);
|
||||||
|
--sidebar-foreground: oklch(0.23 0.02 260);
|
||||||
|
--sidebar-primary: oklch(0.55 0.18 265);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||||
|
--sidebar-accent: oklch(0.96 0.02 270);
|
||||||
|
--sidebar-accent-foreground: oklch(0.35 0.05 265);
|
||||||
|
--sidebar-border: oklch(0.93 0.01 260);
|
||||||
|
--sidebar-ring: oklch(0.55 0.18 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.15 0.015 260);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.95 0.01 260);
|
||||||
--card: oklch(0.205 0 0);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card: oklch(0.2 0.015 260);
|
||||||
--popover: oklch(0.205 0 0);
|
--card-foreground: oklch(0.95 0.01 260);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
--popover: oklch(0.2 0.015 260);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--popover-foreground: oklch(0.95 0.01 260);
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--primary: oklch(0.7 0.16 265);
|
||||||
--muted: oklch(0.269 0 0);
|
--primary-foreground: oklch(0.15 0.02 260);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
--secondary: oklch(0.25 0.015 260);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.9 0.01 260);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
--muted: oklch(0.25 0.015 260);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--muted-foreground: oklch(0.65 0.02 260);
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--accent: oklch(0.3 0.04 280);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--accent-foreground: oklch(0.9 0.03 270);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--destructive: oklch(0.65 0.22 25);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
--border: oklch(0.28 0.015 260);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--input: oklch(0.25 0.015 260);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--ring: oklch(0.7 0.16 265);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--chart-1: oklch(0.7 0.16 265);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--chart-2: oklch(0.7 0.13 170);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--chart-3: oklch(0.75 0.16 45);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--chart-4: oklch(0.7 0.18 330);
|
||||||
|
--chart-5: oklch(0.6 0.13 200);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.18 0.015 260);
|
||||||
|
--sidebar-foreground: oklch(0.95 0.01 260);
|
||||||
|
--sidebar-primary: oklch(0.7 0.16 265);
|
||||||
|
--sidebar-primary-foreground: oklch(0.15 0.02 260);
|
||||||
|
--sidebar-accent: oklch(0.25 0.03 270);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9 0.02 265);
|
||||||
|
--sidebar-border: oklch(0.28 0.015 260);
|
||||||
|
--sidebar-ring: oklch(0.7 0.16 265);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
REFINED MINIMAL DESIGN SYSTEM
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Surface layers for depth */
|
||||||
|
--surface-0: oklch(1 0 0);
|
||||||
|
--surface-1: oklch(0.985 0.002 240);
|
||||||
|
--surface-2: oklch(0.97 0.004 240);
|
||||||
|
--surface-elevated: oklch(1 0 0);
|
||||||
|
|
||||||
|
/* Subtle borders */
|
||||||
|
--border-subtle: oklch(0.94 0.008 260);
|
||||||
|
--border-default: oklch(0.91 0.012 260);
|
||||||
|
|
||||||
|
/* Interactive states */
|
||||||
|
--interactive-hover: oklch(0.96 0.015 260);
|
||||||
|
--interactive-active: oklch(0.93 0.02 265);
|
||||||
|
|
||||||
|
/* Success state */
|
||||||
|
--success: oklch(0.65 0.18 145);
|
||||||
|
--success-muted: oklch(0.95 0.03 145);
|
||||||
|
|
||||||
|
/* Animation timing */
|
||||||
|
--duration-fast: 120ms;
|
||||||
|
--duration-normal: 200ms;
|
||||||
|
--duration-slow: 300ms;
|
||||||
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--surface-0: oklch(0.15 0.015 260);
|
||||||
|
--surface-1: oklch(0.18 0.015 260);
|
||||||
|
--surface-2: oklch(0.22 0.015 260);
|
||||||
|
--surface-elevated: oklch(0.25 0.015 260);
|
||||||
|
|
||||||
|
--border-subtle: oklch(0.25 0.012 260);
|
||||||
|
--border-default: oklch(0.3 0.015 260);
|
||||||
|
|
||||||
|
--interactive-hover: oklch(0.25 0.02 265);
|
||||||
|
--interactive-active: oklch(0.3 0.025 270);
|
||||||
|
|
||||||
|
--success: oklch(0.7 0.16 145);
|
||||||
|
--success-muted: oklch(0.25 0.04 145);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expose surface colors to Tailwind */
|
||||||
|
@theme inline {
|
||||||
|
--color-surface-0: var(--surface-0);
|
||||||
|
--color-surface-1: var(--surface-1);
|
||||||
|
--color-surface-2: var(--surface-2);
|
||||||
|
--color-surface-elevated: var(--surface-elevated);
|
||||||
|
--color-border-subtle: var(--border-subtle);
|
||||||
|
--color-border-default: var(--border-default);
|
||||||
|
--color-interactive-hover: var(--interactive-hover);
|
||||||
|
--color-interactive-active: var(--interactive-active);
|
||||||
|
--color-success: var(--success);
|
||||||
|
--color-success-muted: var(--success-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -118,6 +211,191 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for Radix ScrollArea viewport horizontal overflow */
|
||||||
|
[data-slot="scroll-area-viewport"] > div {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar-thin {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: oklch(0.85 0.01 260) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||||
|
background-color: oklch(0.85 0.01 260);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: oklch(0.75 0.01 260);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth page transitions */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slideInRight 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message bubble animations */
|
||||||
|
@keyframes messageIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-message-in {
|
||||||
|
animation: messageIn 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle floating shadow for cards */
|
||||||
|
.shadow-soft {
|
||||||
|
box-shadow:
|
||||||
|
0 1px 2px oklch(0.23 0.02 260 / 0.04),
|
||||||
|
0 4px 12px oklch(0.23 0.02 260 / 0.06),
|
||||||
|
0 8px 24px oklch(0.23 0.02 260 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-soft-lg {
|
||||||
|
box-shadow:
|
||||||
|
0 2px 4px oklch(0.23 0.02 260 / 0.04),
|
||||||
|
0 8px 20px oklch(0.23 0.02 260 / 0.08),
|
||||||
|
0 16px 40px oklch(0.23 0.02 260 / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient text utility */
|
||||||
|
.text-gradient-primary {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.55 0.18 265),
|
||||||
|
oklch(0.6 0.2 290)
|
||||||
|
);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
REFINED DIALOG STYLES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Refined dialog shadow - multi-layer soft shadow */
|
||||||
|
.shadow-dialog {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px oklch(0 0 0 / 0.03),
|
||||||
|
0 2px 4px oklch(0 0 0 / 0.02),
|
||||||
|
0 12px 24px oklch(0 0 0 / 0.06),
|
||||||
|
0 24px 48px oklch(0 0 0 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .shadow-dialog {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px oklch(1 0 0 / 0.05),
|
||||||
|
0 2px 4px oklch(0 0 0 / 0.2),
|
||||||
|
0 12px 24px oklch(0 0 0 / 0.3),
|
||||||
|
0 24px 48px oklch(0 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog animations */
|
||||||
|
@keyframes dialog-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -48%) scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -48%) scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-dialog-in {
|
||||||
|
animation: dialog-in var(--duration-normal) var(--ease-out) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-dialog-out {
|
||||||
|
animation: dialog-out 150ms var(--ease-out) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check pop animation for validation success */
|
||||||
|
@keyframes check-pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-check-pop {
|
||||||
|
animation: check-pop 0.25s var(--ease-spring) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion support */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-dialog-in,
|
||||||
|
.animate-dialog-out,
|
||||||
|
.animate-check-pop {
|
||||||
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
app/layout.tsx
101
app/layout.tsx
@@ -1,101 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
|
||||||
import { DiagramProvider } from "@/contexts/diagram-context";
|
|
||||||
|
|
||||||
import "./globals.css";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
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.",
|
|
||||||
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" }],
|
|
||||||
creator: "Next AI Draw.io",
|
|
||||||
publisher: "Next AI Draw.io",
|
|
||||||
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
|
|
||||||
openGraph: {
|
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
|
||||||
description: "Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
|
|
||||||
type: "website",
|
|
||||||
url: "https://next-ai-drawio.jiang.jp",
|
|
||||||
siteName: "Next AI Draw.io",
|
|
||||||
locale: "en_US",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: "/architecture.png",
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: "Next AI Draw.io - AI-powered diagram creation tool",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: "Next AI Draw.io - AI Diagram Generator",
|
|
||||||
description: "Create professional diagrams with AI assistance. Free, no login required.",
|
|
||||||
images: ["/architecture.png"],
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
googleBot: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
"max-video-preview": -1,
|
|
||||||
"max-image-preview": "large",
|
|
||||||
"max-snippet": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icons: {
|
|
||||||
icon: "/favicon.ico",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
const jsonLd = {
|
|
||||||
'@context': 'https://schema.org',
|
|
||||||
'@type': 'SoftwareApplication',
|
|
||||||
name: 'Next AI Draw.io',
|
|
||||||
applicationCategory: 'DesignApplication',
|
|
||||||
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.',
|
|
||||||
url: 'https://next-ai-drawio.jiang.jp',
|
|
||||||
offers: {
|
|
||||||
'@type': 'Offer',
|
|
||||||
price: '0',
|
|
||||||
priceCurrency: 'USD',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script
|
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
<DiagramProvider>{children}</DiagramProvider>
|
|
||||||
|
|
||||||
<Analytics />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
app/manifest.ts
Normal file
28
app/manifest.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { MetadataRoute } from "next"
|
||||||
|
import { getAssetUrl } from "@/lib/base-path"
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: "Next AI Draw.io",
|
||||||
|
short_name: "AIDraw.io",
|
||||||
|
description:
|
||||||
|
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
|
||||||
|
start_url: getAssetUrl("/"),
|
||||||
|
display: "standalone",
|
||||||
|
background_color: "#f9fafb",
|
||||||
|
theme_color: "#171d26",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: getAssetUrl("/favicon-192x192.png"),
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: getAssetUrl("/favicon-512x512.png"),
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/page.tsx
76
app/page.tsx
@@ -1,76 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { DrawIoEmbed } from "react-drawio";
|
|
||||||
import ChatPanel from "@/components/chat-panel";
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const { drawioRef, handleDiagramExport } = useDiagram();
|
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkMobile = () => {
|
|
||||||
setIsMobile(window.innerWidth < 768);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check on mount
|
|
||||||
checkMobile();
|
|
||||||
|
|
||||||
// Add event listener for resize
|
|
||||||
window.addEventListener("resize", checkMobile);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => window.removeEventListener("resize", checkMobile);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Add keyboard shortcut for toggling chat panel (Ctrl+B)
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsChatVisible((prev) => !prev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen bg-gray-100 relative">
|
|
||||||
{/* Mobile warning overlay - keeps components mounted */}
|
|
||||||
{isMobile && (
|
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-gray-100">
|
|
||||||
<div className="text-center p-8">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-800">
|
|
||||||
Please open this application on a desktop or laptop
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={`${isChatVisible ? 'w-2/3' : 'w-full'} p-1 h-full relative transition-all duration-300 ease-in-out`}>
|
|
||||||
<DrawIoEmbed
|
|
||||||
ref={drawioRef}
|
|
||||||
onExport={handleDiagramExport}
|
|
||||||
urlParameters={{
|
|
||||||
spin: true,
|
|
||||||
libraries: false,
|
|
||||||
saveAndExit: false,
|
|
||||||
noExitBtn: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={`${isChatVisible ? 'w-1/3' : 'w-12'} h-full p-1 transition-all duration-300 ease-in-out`}>
|
|
||||||
<ChatPanel
|
|
||||||
isVisible={isChatVisible}
|
|
||||||
onToggleVisibility={() => setIsChatVisible(!isChatVisible)}
|
|
||||||
/>
|
|
||||||
</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.10/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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
170
components/ai-elements/model-selector.tsx
Normal file
170
components/ai-elements/model-selector.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Cloud } from "lucide-react"
|
||||||
|
import type { ComponentProps, ReactNode } from "react"
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
CommandShortcut,
|
||||||
|
} from "@/components/ui/command"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export type ModelSelectorProps = ComponentProps<typeof Dialog>
|
||||||
|
|
||||||
|
export const ModelSelector = (props: ModelSelectorProps) => (
|
||||||
|
<Dialog {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>
|
||||||
|
|
||||||
|
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
|
||||||
|
<DialogTrigger {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
|
||||||
|
title?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelSelectorContent = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
title = "Model Selector",
|
||||||
|
...props
|
||||||
|
}: ModelSelectorContentProps) => (
|
||||||
|
<DialogContent className={cn("p-0", className)} {...props}>
|
||||||
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||||
|
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>
|
||||||
|
|
||||||
|
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
|
||||||
|
<CommandDialog {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>
|
||||||
|
|
||||||
|
export const ModelSelectorInput = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ModelSelectorInputProps) => (
|
||||||
|
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorListProps = ComponentProps<typeof CommandList>
|
||||||
|
|
||||||
|
export const ModelSelectorList = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ModelSelectorListProps) => (
|
||||||
|
<div className="relative">
|
||||||
|
<CommandList
|
||||||
|
className={cn(
|
||||||
|
// Hide scrollbar on all platforms
|
||||||
|
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{/* Bottom shadow indicator for scrollable content */}
|
||||||
|
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-muted/80 via-muted/40 to-transparent" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
|
||||||
|
|
||||||
|
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
|
||||||
|
<CommandEmpty {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>
|
||||||
|
|
||||||
|
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
|
||||||
|
<CommandGroup {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>
|
||||||
|
|
||||||
|
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
|
||||||
|
<CommandItem {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>
|
||||||
|
|
||||||
|
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
|
||||||
|
<CommandShortcut {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorSeparatorProps = ComponentProps<
|
||||||
|
typeof CommandSeparator
|
||||||
|
>
|
||||||
|
|
||||||
|
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
|
||||||
|
<CommandSeparator {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorLogoProps = Omit<
|
||||||
|
ComponentProps<"img">,
|
||||||
|
"src" | "alt"
|
||||||
|
> & {
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelSelectorLogo = ({
|
||||||
|
provider,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ModelSelectorLogoProps) => {
|
||||||
|
// Use Lucide icon for bedrock since models.dev doesn't have a good AWS icon
|
||||||
|
if (provider === "amazon-bedrock") {
|
||||||
|
return <Cloud className={cn("size-4", className)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
alt={`${provider} logo`}
|
||||||
|
className={cn("size-4 dark:invert", className)}
|
||||||
|
height={16}
|
||||||
|
src={`https://models.dev/logos/${provider}.svg`}
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelSelectorLogoGroupProps = ComponentProps<"div">
|
||||||
|
|
||||||
|
export const ModelSelectorLogoGroup = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ModelSelectorLogoGroupProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"-space-x-1 flex shrink-0 items-center [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ModelSelectorNameProps = ComponentProps<"span">
|
||||||
|
|
||||||
|
export const ModelSelectorName = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ModelSelectorNameProps) => (
|
||||||
|
<span className={cn("flex-1 truncate text-left", className)} {...props} />
|
||||||
|
)
|
||||||
186
components/ai-elements/reasoning.tsx
Normal file
186
components/ai-elements/reasoning.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
||||||
|
import { BrainIcon, ChevronDownIcon } from "lucide-react"
|
||||||
|
import type { ComponentProps, ReactNode } from "react"
|
||||||
|
import { createContext, memo, useContext, useEffect, useState } from "react"
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Shimmer } from "./shimmer"
|
||||||
|
|
||||||
|
type ReasoningContextValue = {
|
||||||
|
isStreaming: boolean
|
||||||
|
isOpen: boolean
|
||||||
|
setIsOpen: (open: boolean) => void
|
||||||
|
duration: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null)
|
||||||
|
|
||||||
|
export const useReasoning = () => {
|
||||||
|
const context = useContext(ReasoningContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Reasoning components must be used within Reasoning")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
||||||
|
isStreaming?: boolean
|
||||||
|
open?: boolean
|
||||||
|
defaultOpen?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
duration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTO_CLOSE_DELAY = 1000
|
||||||
|
const MS_IN_S = 1000
|
||||||
|
|
||||||
|
export const Reasoning = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
isStreaming = false,
|
||||||
|
open,
|
||||||
|
defaultOpen = true,
|
||||||
|
onOpenChange,
|
||||||
|
duration: durationProp,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ReasoningProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useControllableState({
|
||||||
|
prop: open,
|
||||||
|
defaultProp: defaultOpen,
|
||||||
|
onChange: onOpenChange,
|
||||||
|
})
|
||||||
|
const [duration, setDuration] = useControllableState({
|
||||||
|
prop: durationProp,
|
||||||
|
defaultProp: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [hasAutoClosed, setHasAutoClosed] = useState(false)
|
||||||
|
const [startTime, setStartTime] = useState<number | null>(null)
|
||||||
|
|
||||||
|
// Track duration when streaming starts and ends
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming) {
|
||||||
|
if (startTime === null) {
|
||||||
|
setStartTime(Date.now())
|
||||||
|
}
|
||||||
|
} else if (startTime !== null) {
|
||||||
|
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
|
||||||
|
setStartTime(null)
|
||||||
|
}
|
||||||
|
}, [isStreaming, startTime, setDuration])
|
||||||
|
|
||||||
|
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
||||||
|
// Add a small delay before closing to allow user to see the content
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsOpen(false)
|
||||||
|
setHasAutoClosed(true)
|
||||||
|
}, AUTO_CLOSE_DELAY)
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed])
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
setIsOpen(newOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReasoningContext.Provider
|
||||||
|
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
||||||
|
>
|
||||||
|
<Collapsible
|
||||||
|
className={cn("not-prose mb-4", className)}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
open={isOpen}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Collapsible>
|
||||||
|
</ReasoningContext.Provider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ReasoningTriggerProps = ComponentProps<
|
||||||
|
typeof CollapsibleTrigger
|
||||||
|
> & {
|
||||||
|
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
||||||
|
if (isStreaming || duration === 0) {
|
||||||
|
return <Shimmer duration={1}>Thinking...</Shimmer>
|
||||||
|
}
|
||||||
|
if (duration === undefined) {
|
||||||
|
return <p>Thought for a few seconds</p>
|
||||||
|
}
|
||||||
|
return <p>Thought for {duration} seconds</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReasoningTrigger = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
getThinkingMessage = defaultGetThinkingMessage,
|
||||||
|
...props
|
||||||
|
}: ReasoningTriggerProps) => {
|
||||||
|
const { isStreaming, isOpen, duration } = useReasoning()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<BrainIcon className="size-4" />
|
||||||
|
{getThinkingMessage(isStreaming, duration)}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
"size-4 transition-transform",
|
||||||
|
isOpen ? "rotate-180" : "rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ReasoningContentProps = ComponentProps<
|
||||||
|
typeof CollapsibleContent
|
||||||
|
> & {
|
||||||
|
children: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReasoningContent = memo(
|
||||||
|
({ className, children, ...props }: ReasoningContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
"mt-4 text-sm",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="whitespace-pre-wrap">{children}</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Reasoning.displayName = "Reasoning"
|
||||||
|
ReasoningTrigger.displayName = "ReasoningTrigger"
|
||||||
|
ReasoningContent.displayName = "ReasoningContent"
|
||||||
64
components/ai-elements/shimmer.tsx
Normal file
64
components/ai-elements/shimmer.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { motion } from "motion/react"
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
type ElementType,
|
||||||
|
type JSX,
|
||||||
|
memo,
|
||||||
|
useMemo,
|
||||||
|
} from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export type TextShimmerProps = {
|
||||||
|
children: string
|
||||||
|
as?: ElementType
|
||||||
|
className?: string
|
||||||
|
duration?: number
|
||||||
|
spread?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShimmerComponent = ({
|
||||||
|
children,
|
||||||
|
as: Component = "p",
|
||||||
|
className,
|
||||||
|
duration = 2,
|
||||||
|
spread = 2,
|
||||||
|
}: TextShimmerProps) => {
|
||||||
|
const MotionComponent = motion.create(
|
||||||
|
Component as keyof JSX.IntrinsicElements,
|
||||||
|
)
|
||||||
|
|
||||||
|
const dynamicSpread = useMemo(
|
||||||
|
() => (children?.length ?? 0) * spread,
|
||||||
|
[children, spread],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
animate={{ backgroundPosition: "0% center" }}
|
||||||
|
className={cn(
|
||||||
|
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
||||||
|
"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
initial={{ backgroundPosition: "100% center" }}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--spread": `${dynamicSpread}px`,
|
||||||
|
backgroundImage:
|
||||||
|
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
transition={{
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
duration,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MotionComponent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Shimmer = memo(ShimmerComponent)
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import 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>{tooltipContent}</TooltipContent>
|
<TooltipContent className="max-w-xs text-wrap">
|
||||||
|
{tooltipContent}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,219 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Cloud,
|
||||||
|
FileText,
|
||||||
|
GitBranch,
|
||||||
|
Palette,
|
||||||
|
Terminal,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { getAssetUrl } from "@/lib/base-path"
|
||||||
|
|
||||||
|
interface ExampleCardProps {
|
||||||
|
icon: React.ReactNode
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
onClick: () => void
|
||||||
|
isNew?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExampleCard({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onClick,
|
||||||
|
isNew,
|
||||||
|
}: ExampleCardProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`group w-full text-left p-4 rounded-xl border bg-card hover:bg-accent/50 hover:border-primary/30 transition-all duration-200 hover:shadow-sm ${
|
||||||
|
isNew
|
||||||
|
? "border-primary/40 ring-1 ring-primary/20"
|
||||||
|
: "border-border/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors ${
|
||||||
|
isNew
|
||||||
|
? "bg-primary/20 group-hover:bg-primary/25"
|
||||||
|
: "bg-primary/10 group-hover:bg-primary/15"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{isNew && (
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-primary text-primary-foreground rounded">
|
||||||
|
{dict.common.new}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function ExamplePanel({
|
export default function ExamplePanel({
|
||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
}: {
|
}: {
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void
|
||||||
}) {
|
}) {
|
||||||
// New handler for the "Replicate this flowchart" button
|
const dict = useDictionary()
|
||||||
|
|
||||||
const handleReplicateFlowchart = async () => {
|
const handleReplicateFlowchart = async () => {
|
||||||
setInput("Replicate this flowchart.");
|
setInput("Replicate this flowchart.")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the example image
|
const response = await fetch(getAssetUrl("/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])
|
||||||
|
|
||||||
// Set the file to the files state
|
|
||||||
setFiles([file]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading example image:", error);
|
console.error(dict.errors.failedToLoadExample, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Handler for the "Replicate this in aws style" button
|
|
||||||
const handleReplicateArchitecture = async () => {
|
const handleReplicateArchitecture = async () => {
|
||||||
setInput("Replicate this in aws style");
|
setInput("Replicate this in aws style")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the architecture image
|
const response = await fetch(getAssetUrl("/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])
|
||||||
// Set the file to the files state
|
|
||||||
setFiles([file]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading architecture image:", error);
|
console.error(dict.errors.failedToLoadExample, error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const handlePdfExample = async () => {
|
||||||
|
setInput("Summarize this paper as a diagram")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(getAssetUrl("/chain-of-thought.txt"))
|
||||||
|
const blob = await response.blob()
|
||||||
|
const file = new File([blob], "chain-of-thought.txt", {
|
||||||
|
type: "text/plain",
|
||||||
|
})
|
||||||
|
setFiles([file])
|
||||||
|
} catch (error) {
|
||||||
|
console.error(dict.errors.failedToLoadExample, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-2 border-t border-b border-gray-100">
|
<div className="py-6 px-2 animate-fade-in">
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
{/* MCP Server Notice */}
|
||||||
{" "}
|
<a
|
||||||
Start a conversation to generate or modify diagrams.
|
href="https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block mb-4 p-3 rounded-xl bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-500/20 hover:border-purple-500/40 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center shrink-0">
|
||||||
|
<Terminal className="w-4 h-4 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
|
||||||
|
{dict.examples.mcpServer}
|
||||||
|
</span>
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
|
||||||
|
{dict.examples.preview}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{dict.examples.mcpDescription}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
</div>
|
||||||
{" "}
|
</div>
|
||||||
You can also upload images to use as references.
|
</a>
|
||||||
|
|
||||||
|
{/* Welcome section */}
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">
|
||||||
|
{dict.examples.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-xs mx-auto">
|
||||||
|
{dict.examples.subtitle}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mb-2">Try these examples:</p>
|
</div>
|
||||||
<div className="flex flex-wrap gap-5">
|
|
||||||
<button
|
{/* Examples grid */}
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
<div className="space-y-3">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1">
|
||||||
|
{dict.examples.quickExamples}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<ExampleCard
|
||||||
|
icon={<FileText className="w-4 h-4 text-primary" />}
|
||||||
|
title={dict.examples.paperToDiagram}
|
||||||
|
description={dict.examples.paperDescription}
|
||||||
|
onClick={handlePdfExample}
|
||||||
|
isNew
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExampleCard
|
||||||
|
icon={<Zap className="w-4 h-4 text-primary" />}
|
||||||
|
title={dict.examples.animatedDiagram}
|
||||||
|
description={dict.examples.animatedDescription}
|
||||||
|
onClick={() => {
|
||||||
|
setInput(
|
||||||
|
"Give me a **animated connector** diagram of transformer's architecture",
|
||||||
|
)
|
||||||
|
setFiles([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExampleCard
|
||||||
|
icon={<Cloud className="w-4 h-4 text-primary" />}
|
||||||
|
title={dict.examples.awsArchitecture}
|
||||||
|
description={dict.examples.awsDescription}
|
||||||
onClick={handleReplicateArchitecture}
|
onClick={handleReplicateArchitecture}
|
||||||
>
|
/>
|
||||||
Create this diagram in aws style
|
|
||||||
</button>
|
<ExampleCard
|
||||||
<button
|
icon={<GitBranch className="w-4 h-4 text-primary" />}
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
title={dict.examples.replicateFlowchart}
|
||||||
|
description={dict.examples.replicateDescription}
|
||||||
onClick={handleReplicateFlowchart}
|
onClick={handleReplicateFlowchart}
|
||||||
>
|
/>
|
||||||
Replicate this flowchart
|
|
||||||
</button>
|
<ExampleCard
|
||||||
<button
|
icon={<Palette className="w-4 h-4 text-primary" />}
|
||||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
title={dict.examples.creativeDrawing}
|
||||||
onClick={() => setInput("Draw a cat for me")}
|
description={dict.examples.creativeDescription}
|
||||||
>
|
onClick={() => {
|
||||||
Draw a cat for me
|
setInput("Draw a cat for me")
|
||||||
</button>
|
setFiles([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-muted-foreground/60 text-center mt-4">
|
||||||
|
{dict.examples.cachedNote}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,161 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useCallback, useRef, useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { ResetWarningModal } from "@/components/reset-warning-modal";
|
|
||||||
import {
|
import {
|
||||||
|
Download,
|
||||||
|
History,
|
||||||
|
Image as ImageIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
Send,
|
Send,
|
||||||
RotateCcw,
|
Trash2,
|
||||||
Image as ImageIcon,
|
} from "lucide-react"
|
||||||
History,
|
import type React from "react"
|
||||||
} from "lucide-react";
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
import { toast } from "sonner"
|
||||||
import { FilePreviewList } from "./file-preview-list";
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { ErrorToast } from "@/components/error-toast"
|
||||||
import { HistoryDialog } from "@/components/history-dialog";
|
import { HistoryDialog } from "@/components/history-dialog"
|
||||||
|
import { ModelSelector } from "@/components/model-selector"
|
||||||
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
||||||
|
import { SaveDialog } from "@/components/save-dialog"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
|
import type { FlattenedModel } from "@/lib/types/model-config"
|
||||||
|
import { FilePreviewList } from "./file-preview-list"
|
||||||
|
|
||||||
|
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||||
|
const MAX_FILES = 5
|
||||||
|
|
||||||
|
function isValidFileType(file: File): boolean {
|
||||||
|
return file.type.startsWith("image/") || isPdfFile(file) || isTextFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
const mb = bytes / 1024 / 1024
|
||||||
|
if (mb < 0.01) return `${(bytes / 1024).toFixed(0)}KB`
|
||||||
|
return `${mb.toFixed(2)}MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrorToast(message: React.ReactNode) {
|
||||||
|
toast.custom(
|
||||||
|
(t) => (
|
||||||
|
<ErrorToast message={message} onDismiss={() => toast.dismiss(t)} />
|
||||||
|
),
|
||||||
|
{ duration: 5000 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
validFiles: File[]
|
||||||
|
errors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFiles(
|
||||||
|
newFiles: File[],
|
||||||
|
existingCount: number,
|
||||||
|
dict: any,
|
||||||
|
): ValidationResult {
|
||||||
|
const errors: string[] = []
|
||||||
|
const validFiles: File[] = []
|
||||||
|
|
||||||
|
const availableSlots = MAX_FILES - existingCount
|
||||||
|
|
||||||
|
if (availableSlots <= 0) {
|
||||||
|
errors.push(formatMessage(dict.errors.maxFiles, { max: MAX_FILES }))
|
||||||
|
return { validFiles, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
if (validFiles.length >= availableSlots) {
|
||||||
|
errors.push(
|
||||||
|
formatMessage(dict.errors.onlyMoreAllowed, {
|
||||||
|
slots: availableSlots,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!isValidFileType(file)) {
|
||||||
|
errors.push(
|
||||||
|
formatMessage(dict.errors.unsupportedType, { name: file.name }),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter)
|
||||||
|
const isExtractedFile = isPdfFile(file) || isTextFile(file)
|
||||||
|
if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) {
|
||||||
|
const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024
|
||||||
|
errors.push(
|
||||||
|
formatMessage(dict.errors.fileExceeds, {
|
||||||
|
name: file.name,
|
||||||
|
size: formatFileSize(file.size),
|
||||||
|
max: maxSizeMB,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
validFiles.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { validFiles, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showValidationErrors(errors: string[], dict: any) {
|
||||||
|
if (errors.length === 0) return
|
||||||
|
|
||||||
|
if (errors.length === 1) {
|
||||||
|
showErrorToast(
|
||||||
|
<span className="text-muted-foreground">{errors[0]}</span>,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showErrorToast(
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatMessage(dict.errors.filesRejected, {
|
||||||
|
count: errors.length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
||||||
|
{errors.slice(0, 3).map((err) => (
|
||||||
|
<li key={err}>{err}</li>
|
||||||
|
))}
|
||||||
|
{errors.length > 3 && (
|
||||||
|
<li>
|
||||||
|
{formatMessage(dict.errors.andMore, {
|
||||||
|
count: errors.length - 3,
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
input: string;
|
input: string
|
||||||
status: "submitted" | "streaming" | "ready" | "error";
|
status: "submitted" | "streaming" | "ready" | "error"
|
||||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
||||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||||
onClearChat: () => void;
|
onClearChat: () => void
|
||||||
files?: File[];
|
files?: File[]
|
||||||
onFileChange?: (files: File[]) => void;
|
onFileChange?: (files: File[]) => void
|
||||||
showHistory?: boolean;
|
pdfData?: Map<
|
||||||
onToggleHistory?: (show: boolean) => void;
|
File,
|
||||||
|
{ text: string; charCount: number; isExtracting: boolean }
|
||||||
|
>
|
||||||
|
|
||||||
|
sessionId?: string
|
||||||
|
error?: Error | null
|
||||||
|
// Model selector props
|
||||||
|
models?: FlattenedModel[]
|
||||||
|
selectedModelId?: string
|
||||||
|
onModelSelect?: (modelId: string | undefined) => void
|
||||||
|
showUnvalidatedModels?: boolean
|
||||||
|
onConfigureModels?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -36,245 +166,307 @@ export function ChatInput({
|
|||||||
onClearChat,
|
onClearChat,
|
||||||
files = [],
|
files = [],
|
||||||
onFileChange = () => {},
|
onFileChange = () => {},
|
||||||
showHistory = false,
|
pdfData = new Map(),
|
||||||
onToggleHistory = () => {},
|
sessionId,
|
||||||
|
error = null,
|
||||||
|
models = [],
|
||||||
|
selectedModelId,
|
||||||
|
onModelSelect = () => {},
|
||||||
|
showUnvalidatedModels = false,
|
||||||
|
onConfigureModels = () => {},
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory } = useDiagram();
|
const dict = useDictionary()
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const { diagramHistory, saveDiagramToFile } = useDiagram()
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [showClearDialog, setShowClearDialog] = useState(false);
|
|
||||||
|
|
||||||
// Debug: Log status changes
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const isDisabled = status === "streaming" || status === "submitted";
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
useEffect(() => {
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled);
|
const [showClearDialog, setShowClearDialog] = useState(false)
|
||||||
}, [status, isDisabled]);
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
|
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
|
||||||
|
const isDisabled =
|
||||||
|
(status === "streaming" || status === "submitted") && !error
|
||||||
|
|
||||||
// Auto-resize textarea based on content
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
|
||||||
// Handle keyboard shortcuts and paste events
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Handle clipboard paste
|
|
||||||
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) => {
|
await Promise.all(
|
||||||
const file = item.getAsFile();
|
imageItems.map(async (item, index) => {
|
||||||
if (!file) return null;
|
const file = item.getAsFile()
|
||||||
// Create a new file with a unique name
|
if (!file) return null
|
||||||
return new File(
|
return new File(
|
||||||
[file],
|
[file],
|
||||||
`pasted-image-${Date.now()}.${file.type.split("/")[1]}`,
|
`pasted-image-${Date.now()}-${index}.${file.type.split("/")[1]}`,
|
||||||
{
|
{ type: file.type },
|
||||||
type: file.type,
|
)
|
||||||
}
|
}),
|
||||||
);
|
)
|
||||||
})
|
).filter((f): f is File => f !== null)
|
||||||
);
|
|
||||||
|
|
||||||
const validFiles = imageFiles.filter(
|
const { validFiles, errors } = validateFiles(
|
||||||
(file): file is File => file !== null
|
imageFiles,
|
||||||
);
|
files.length,
|
||||||
|
dict,
|
||||||
|
)
|
||||||
|
showValidationErrors(errors, dict)
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
onFileChange([...files, ...validFiles]);
|
onFileChange([...files, ...validFiles])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Handle file changes
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newFiles = Array.from(e.target.files || []);
|
const newFiles = Array.from(e.target.files || [])
|
||||||
onFileChange([...files, ...newFiles]);
|
const { validFiles, errors } = validateFiles(
|
||||||
};
|
newFiles,
|
||||||
|
files.length,
|
||||||
// Remove individual file
|
dict,
|
||||||
const handleRemoveFile = (fileToRemove: File) => {
|
)
|
||||||
onFileChange(files.filter((file) => file !== fileToRemove));
|
showValidationErrors(errors, dict)
|
||||||
if (fileInputRef.current) {
|
if (validFiles.length > 0) {
|
||||||
fileInputRef.current.value = "";
|
onFileChange([...files, ...validFiles])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveFile = (fileToRemove: File) => {
|
||||||
|
onFileChange(files.filter((file) => file !== fileToRemove))
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Trigger file input click
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click()
|
||||||
};
|
}
|
||||||
|
|
||||||
// Handle drag events
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
setIsDragging(true);
|
setIsDragging(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
setIsDragging(false);
|
setIsDragging(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
|
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
setIsDragging(false);
|
setIsDragging(false)
|
||||||
|
|
||||||
if (isDisabled) return;
|
if (isDisabled) return
|
||||||
|
|
||||||
const droppedFiles = e.dataTransfer.files;
|
const droppedFiles = e.dataTransfer.files
|
||||||
|
const supportedFiles = Array.from(droppedFiles).filter((file) =>
|
||||||
|
isValidFileType(file),
|
||||||
|
)
|
||||||
|
|
||||||
// Only process image files
|
const { validFiles, errors } = validateFiles(
|
||||||
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
supportedFiles,
|
||||||
file.type.startsWith("image/")
|
files.length,
|
||||||
);
|
dict,
|
||||||
|
)
|
||||||
if (imageFiles.length > 0) {
|
showValidationErrors(errors, dict)
|
||||||
onFileChange([...files, ...imageFiles]);
|
if (validFiles.length > 0) {
|
||||||
|
onFileChange([...files, ...validFiles])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Handle clearing conversation and diagram
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onClearChat();
|
onClearChat()
|
||||||
setShowClearDialog(false);
|
setShowClearDialog(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
className={`w-full space-y-2 ${
|
className={`w-full transition-all duration-200 ${
|
||||||
isDragging
|
isDragging
|
||||||
? "border-2 border-dashed border-primary p-4 rounded-lg bg-muted/20"
|
? "ring-2 ring-primary ring-offset-2 rounded-2xl"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} />
|
{/* File previews */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<FilePreviewList
|
||||||
|
files={files}
|
||||||
|
onRemoveFile={handleRemoveFile}
|
||||||
|
pdfData={pdfData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200">
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={onChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder="Describe what changes you want to make to the diagram
|
placeholder={dict.chat.placeholder}
|
||||||
or upload(paste) an image to replicate a diagram.
|
|
||||||
(Press Cmd/Ctrl + Enter to send)"
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
aria-label="Chat input"
|
aria-label="Chat input"
|
||||||
className="min-h-[80px] resize-none transition-all duration-200 px-1 py-0"
|
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
|
||||||
<div className="mr-auto">
|
<div className="flex items-center gap-1 overflow-x-hidden">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
onClick={() => setShowClearDialog(true)}
|
onClick={() => setShowClearDialog(true)}
|
||||||
tooltipContent="Clear current conversation and diagram"
|
tooltipContent={dict.chat.clearConversation}
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
>
|
>
|
||||||
<RotateCcw className="mr-2 h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
{/* Warning Modal */}
|
|
||||||
<ResetWarningModal
|
<ResetWarningModal
|
||||||
open={showClearDialog}
|
open={showClearDialog}
|
||||||
onOpenChange={setShowClearDialog}
|
onOpenChange={setShowClearDialog}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HistoryDialog
|
|
||||||
showHistory={showHistory}
|
|
||||||
onToggleHistory={onToggleHistory}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
|
||||||
{/* History Button */}
|
<div className="flex items-center gap-1 overflow-hidden justify-end">
|
||||||
|
<div className="flex items-center gap-1 overflow-x-hidden">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
onClick={() => onToggleHistory(true)}
|
onClick={() => setShowHistory(true)}
|
||||||
disabled={
|
disabled={
|
||||||
isDisabled ||
|
isDisabled || diagramHistory.length === 0
|
||||||
diagramHistory.length === 0
|
|
||||||
}
|
}
|
||||||
title="Diagram History"
|
tooltipContent={dict.chat.diagramHistory}
|
||||||
tooltipContent="View diagram history"
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" />
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
<Button
|
<ButtonWithTooltip
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
|
onClick={() => setShowSaveDialog(true)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
tooltipContent={dict.chat.saveDiagram}
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
|
<ButtonWithTooltip
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={triggerFileInput}
|
onClick={triggerFileInput}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
title="Upload image"
|
tooltipContent={dict.chat.uploadFile}
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ImageIcon className="h-4 w-4" />
|
<ImageIcon className="h-4 w-4" />
|
||||||
</Button>
|
</ButtonWithTooltip>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept="image/*"
|
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
|
||||||
multiple
|
multiple
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ModelSelector
|
||||||
|
models={models}
|
||||||
|
selectedModelId={selectedModelId}
|
||||||
|
onSelect={onModelSelect}
|
||||||
|
onConfigure={onConfigureModels}
|
||||||
|
disabled={isDisabled}
|
||||||
|
showUnvalidatedModels={showUnvalidatedModels}
|
||||||
|
/>
|
||||||
|
<div className="w-px h-5 bg-border mx-1" />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isDisabled || !input.trim()}
|
disabled={isDisabled || !input.trim()}
|
||||||
className="transition-opacity"
|
size="sm"
|
||||||
|
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||||
aria-label={
|
aria-label={
|
||||||
isDisabled
|
isDisabled ? dict.chat.sending : dict.chat.send
|
||||||
? "Sending message..."
|
|
||||||
: "Send message"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Send className="mr-2 h-4 w-4" />
|
<>
|
||||||
|
<Send className="h-4 w-4 mr-1.5" />
|
||||||
|
{dict.chat.send}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
Send
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
<HistoryDialog
|
||||||
|
showHistory={showHistory}
|
||||||
|
onToggleHistory={setShowHistory}
|
||||||
|
/>
|
||||||
|
<SaveDialog
|
||||||
|
open={showSaveDialog}
|
||||||
|
onOpenChange={setShowSaveDialog}
|
||||||
|
onSave={(filename, format) =>
|
||||||
|
saveDiagramToFile(filename, format, sessionId)
|
||||||
|
}
|
||||||
|
defaultFilename={`diagram-${new Date()
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10)}`}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
53
components/code-block.tsx
Normal file
53
components/code-block.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Highlight, themes } from "prism-react-renderer"
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
code: string
|
||||||
|
language?: "xml" | "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden w-full">
|
||||||
|
<Highlight theme={themes.github} code={code} language={language}>
|
||||||
|
{({
|
||||||
|
className: _className,
|
||||||
|
style,
|
||||||
|
tokens,
|
||||||
|
getLineProps,
|
||||||
|
getTokenProps,
|
||||||
|
}) => (
|
||||||
|
<pre
|
||||||
|
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-mono), ui-monospace, monospace",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
wordBreak: "break-all",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tokens.map((line, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
{...getLineProps({ line })}
|
||||||
|
style={{ wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
|
{line.map((token, key) => (
|
||||||
|
<span
|
||||||
|
key={key}
|
||||||
|
{...getTokenProps({ token })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
363
components/dev-xml-simulator.tsx
Normal file
363
components/dev-xml-simulator.tsx
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { wrapWithMxFile } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Dev XML presets for streaming simulator
|
||||||
|
const DEV_XML_PRESETS: Record<string, string> = {
|
||||||
|
"Simple Box": `<mxCell id="2" value="Hello World" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="120" y="100" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>`,
|
||||||
|
"Two Boxes with Arrow": `<mxCell id="2" value="Start" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="100" y="100" width="100" height="50" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" value="End" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="300" y="100" width="100" height="50" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="4" value="" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="3">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>`,
|
||||||
|
Flowchart: `<mxCell id="2" value="Start" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="160" y="40" width="80" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" value="Process A" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="140" y="120" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="4" value="Decision" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="150" y="220" width="100" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="5" value="Process B" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="300" y="230" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="6" value="End" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="160" y="340" width="80" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="7" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="3">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="8" style="endArrow=classic;html=1;" edge="1" parent="1" source="3" target="4">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="9" value="Yes" style="endArrow=classic;html=1;" edge="1" parent="1" source="4" target="6">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="10" value="No" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="4" target="5">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>`,
|
||||||
|
"Truncated (Error Test)": `<mxCell id="2" value="This cell is truncated" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="120" y="100" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" value="Incomplete" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor`,
|
||||||
|
"HTML Escape + Cell Truncate": `<mxCell id="2" value="<b>Chain-of-Thought Prompting</b><br/><font size='12'>Eliciting Reasoning in Large Language Models</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=16;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="40" width="720" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" value="<b>Problem: LLM Reasoning Limitations</b><br/>• Scaling parameters alone insufficient for logical tasks<br/>• Arithmetic, commonsense, symbolic reasoning challenges<br/>• Standard prompting fails on multi-step problems" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="120" width="340" height="120" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="4" value="<b>Traditional Approaches</b><br/>1. <b>Finetuning:</b> Expensive, task-specific<br/>2. <b>Standard Few-Shot:</b> Input→Output pairs<br/> (No explanation of reasoning)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="120" width="340" height="120" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="5" value="<b>CoT Methodology</b><br/>• Add reasoning steps to few-shot examples<br/>• Natural language intermediate steps<br/>• No parameter updates needed<br/>• Model learns to generate own thought process" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="260" width="340" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="6" value="<b>Example Comparison</b><br/><b>Standard:</b><br/>Q: Roger has 5 balls. He buys 2 cans of 3 balls. How many?<br/>A: 11.<br/><br/><b>CoT:</b><br/>Q: Roger has 5 balls. He buys 2 cans of 3 balls. How many?<br/>A: Roger started with 5 balls. 2 cans of 3 tennis balls each is 6 tennis balls. 5 + 6 = 11. The answer is 11." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="260" width="340" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="7" value="<b>Experimental Models</b><br/>• GPT-3 (175B)<br/>• LaMDA (137B)<br/>• PaLM (540B)<br/>• UL2 (20B)<br/>• Codex" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="380" width="340" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="8" value="<b>Reasoning Domains Tested</b><br/>1. <b>Arithmetic:</b> GSM8K, SVAMP, ASDiv, AQuA, MAWPS<br/>2. <b>Commonsense:</b> CSQA, StrategyQA, Date Understanding, Sports Understanding<br/>3. <b>Symbolic:</b> Last Letter Concatenation, Coin Flip" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="420" width="340" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="9" value="<b>Key Results: Arithmetic</b><br/>• PaLM 540B + CoT: <b>56.9%</b> on GSM8K<br/> (vs 17.9% standard)<br/>• Surpassed finetuned GPT-3 (55%)<br/>• With calculator: <b>58.6%</b>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="500" width="220" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="10" value="<b>Key Results: Commonsense</b><br/>• StrategyQA: <b>75.6%</b><br/> (vs 69.4% SOTA)<br/>• Sports Understanding: <b>95.4%</b><br/> (vs 84% human)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="280" y="500" width="220" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="11" value="<b>Key Results: Symbolic</b><br/>• OOD Generalization<br/>• Coin Flip: Trained on 2 flips<br/> Works on 3-4 flips with CoT<br/>• Standard prompting fails" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="540" y="500" width="220" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="12" value="<b>Emergent Ability of Scale</b><br/>• Small models (<10B): No benefit, often harmful<br/>• Large models (100B+): Reasoning emerges<br/>• CoT gains increase dramatically with scale" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="620" width="340" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="13" value="<b>Ablation Studies</b><br/>1. Equation only: Worse than CoT<br/>2. Variable compute (...): No improvement<br/>3. Answer first, then reasoning: Same as baseline<br/>→ Content matters, not just extra tokens" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="620" width="340" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="14" value="<b>Error Analysis</b><br/>• Semantic understanding errors<br/>• One-step missing errors<br/>• Calculation errors<br/>• Larger models reduce semantic/missing-step errors" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="720" width="340" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="15" value="<b>Conclusion</b><br/>• CoT unlocks reasoning potential<br/>• Simple paradigm: "show your work"<br/>• Emergent capability of large models<br/>• No specialized architecture needed" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="720" width="340" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="3" target="5">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="4" target="6">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="5" target="7">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="6" target="8">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.25;entryY=0;" edge="1" parent="1" source="7" target="9">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="7" target="10">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.75;entryY=0;" edge="1" parent="1" source="7" target="11">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="9" target="12">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="10" target="13">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="11" target="14">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="12" target="15">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="13" target="15">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="14" target="15">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DevXmlSimulatorProps {
|
||||||
|
setMessages: React.Dispatch<React.SetStateAction<any[]>>
|
||||||
|
onDisplayChart: (xml: string) => void
|
||||||
|
onShowQuotaToast?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DevXmlSimulator({
|
||||||
|
setMessages,
|
||||||
|
onDisplayChart,
|
||||||
|
onShowQuotaToast,
|
||||||
|
}: DevXmlSimulatorProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
const [devXml, setDevXml] = useState("")
|
||||||
|
const [isSimulating, setIsSimulating] = useState(false)
|
||||||
|
const [devIntervalMs, setDevIntervalMs] = useState(1)
|
||||||
|
const [devChunkSize, setDevChunkSize] = useState(10)
|
||||||
|
const devStopRef = useRef(false)
|
||||||
|
const devXmlInitializedRef = useRef(false)
|
||||||
|
|
||||||
|
// Restore dev XML from localStorage on mount (after hydration)
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem("dev-xml-simulator")
|
||||||
|
if (saved) setDevXml(saved)
|
||||||
|
devXmlInitializedRef.current = true
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Save dev XML to localStorage (only after initial load)
|
||||||
|
useEffect(() => {
|
||||||
|
if (devXmlInitializedRef.current) {
|
||||||
|
localStorage.setItem("dev-xml-simulator", devXml)
|
||||||
|
}
|
||||||
|
}, [devXml])
|
||||||
|
|
||||||
|
const handleDevSimulate = async () => {
|
||||||
|
if (!devXml.trim() || isSimulating) return
|
||||||
|
|
||||||
|
setIsSimulating(true)
|
||||||
|
devStopRef.current = false
|
||||||
|
const toolCallId = `dev-sim-${Date.now()}`
|
||||||
|
const xml = devXml.trim()
|
||||||
|
|
||||||
|
// Add user message and initial assistant message with empty XML
|
||||||
|
const userMsg = {
|
||||||
|
id: `user-${Date.now()}`,
|
||||||
|
role: "user" as const,
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: dict.dev.simulatingMessage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const assistantMsg = {
|
||||||
|
id: `assistant-${Date.now()}`,
|
||||||
|
role: "assistant" as const,
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "tool-display_diagram" as const,
|
||||||
|
toolCallId,
|
||||||
|
state: "input-streaming" as const,
|
||||||
|
input: { xml: "" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, userMsg, assistantMsg] as any)
|
||||||
|
|
||||||
|
// Stream characters progressively
|
||||||
|
for (let i = 0; i < xml.length; i += devChunkSize) {
|
||||||
|
if (devStopRef.current) {
|
||||||
|
setIsSimulating(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = xml.slice(0, i + devChunkSize)
|
||||||
|
|
||||||
|
setMessages((prev) => {
|
||||||
|
const updated = [...prev]
|
||||||
|
const lastMsg = updated[updated.length - 1] as any
|
||||||
|
if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) {
|
||||||
|
lastMsg.parts[0].input = { xml: chunk }
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, devIntervalMs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devStopRef.current) {
|
||||||
|
setIsSimulating(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize: set state to output-available
|
||||||
|
setMessages((prev) => {
|
||||||
|
const updated = [...prev]
|
||||||
|
const lastMsg = updated[updated.length - 1] as any
|
||||||
|
if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) {
|
||||||
|
lastMsg.parts[0].state = "output-available"
|
||||||
|
lastMsg.parts[0].output = dict.dev.successMessage
|
||||||
|
lastMsg.parts[0].input = { xml }
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
|
||||||
|
// Display the final diagram
|
||||||
|
const fullXml = wrapWithMxFile(xml)
|
||||||
|
onDisplayChart(fullXml)
|
||||||
|
|
||||||
|
setIsSimulating(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-t border-dashed border-orange-500/50 px-4 py-2 bg-orange-50/50 dark:bg-orange-950/30">
|
||||||
|
<details>
|
||||||
|
<summary className="text-xs text-orange-600 dark:text-orange-400 cursor-pointer font-medium">
|
||||||
|
{dict.dev.title}
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{dict.dev.preset}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value) {
|
||||||
|
setDevXml(DEV_XML_PRESETS[e.target.value])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 text-xs p-1 border rounded bg-background"
|
||||||
|
defaultValue=""
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{dict.dev.selectPreset}
|
||||||
|
</option>
|
||||||
|
{Object.keys(DEV_XML_PRESETS).map((name) => (
|
||||||
|
<option key={name} value={name}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDevXml("")}
|
||||||
|
className="px-2 py-1 text-xs text-muted-foreground hover:text-foreground border rounded"
|
||||||
|
>
|
||||||
|
{dict.dev.clear}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={devXml}
|
||||||
|
onChange={(e) => setDevXml(e.target.value)}
|
||||||
|
placeholder={dict.dev.placeholder}
|
||||||
|
className="w-full h-24 text-xs font-mono p-2 border rounded bg-background"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2 flex-1">
|
||||||
|
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{dict.dev.interval}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="200"
|
||||||
|
step="1"
|
||||||
|
value={devIntervalMs}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDevIntervalMs(Number(e.target.value))
|
||||||
|
}
|
||||||
|
className="flex-1 h-1 accent-orange-500"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground w-12">
|
||||||
|
{devIntervalMs}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{dict.dev.chars}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={devChunkSize}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDevChunkSize(
|
||||||
|
Math.max(1, Number(e.target.value)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-14 text-xs p-1 border rounded bg-background"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDevSimulate}
|
||||||
|
disabled={isSimulating || !devXml.trim()}
|
||||||
|
className="px-3 py-1 text-xs bg-orange-500 text-white rounded hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isSimulating
|
||||||
|
? dict.dev.streaming
|
||||||
|
: `${dict.dev.simulate} (${devChunkSize} chars/${devIntervalMs}ms)`}
|
||||||
|
</button>
|
||||||
|
{isSimulating && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
devStopRef.current = true
|
||||||
|
}}
|
||||||
|
className="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
|
>
|
||||||
|
{dict.dev.stop}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onShowQuotaToast && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onShowQuotaToast}
|
||||||
|
className="px-3 py-1 text-xs bg-purple-500 text-white rounded hover:bg-purple-600"
|
||||||
|
>
|
||||||
|
{dict.dev.testQuotaToast}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
components/error-toast.tsx
Normal file
44
components/error-toast.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
interface ErrorToastProps {
|
||||||
|
message: React.ReactNode
|
||||||
|
onDismiss: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" || e.key === " " || e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onDismiss}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm cursor-pointer hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-destructive/10 flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-destructive"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-foreground">{message}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,49 +1,140 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { FileCode, FileText, Loader2, 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"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
||||||
|
|
||||||
interface FilePreviewListProps {
|
function formatCharCount(count: number): string {
|
||||||
files: File[];
|
if (count >= 1000) {
|
||||||
onRemoveFile: (fileToRemove: File) => void;
|
return `${(count / 1000).toFixed(1)}k`
|
||||||
|
}
|
||||||
|
return String(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
interface FilePreviewListProps {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
files: File[]
|
||||||
|
onRemoveFile: (fileToRemove: File) => void
|
||||||
|
pdfData?: Map<
|
||||||
|
File,
|
||||||
|
{ text: string; charCount: number; isExtracting: boolean }
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup object URLs on unmount
|
export function FilePreviewList({
|
||||||
|
files,
|
||||||
|
onRemoveFile,
|
||||||
|
pdfData = new Map(),
|
||||||
|
}: FilePreviewListProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
||||||
|
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
||||||
|
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
||||||
|
// Create and cleanup object URLs when files change
|
||||||
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]);
|
})
|
||||||
|
// Clear the ref so StrictMode remount creates fresh URLs
|
||||||
|
imageUrlsRef.current = new Map()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
// 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;
|
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
|
||||||
|
const pdfInfo = pdfData.get(file)
|
||||||
return (
|
return (
|
||||||
<div key={file.name + index} className="relative group">
|
<div key={file.name + index} className="relative group">
|
||||||
<div
|
<div
|
||||||
className="w-20 h-20 border rounded-md overflow-hidden bg-muted cursor-pointer"
|
className={`w-20 h-20 border rounded-md overflow-hidden bg-muted ${
|
||||||
onClick={() => imageUrl && setSelectedImage(imageUrl)}
|
file.type.startsWith("image/") && imageUrl
|
||||||
|
? "cursor-pointer"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() =>
|
||||||
|
file.type.startsWith("image/") &&
|
||||||
|
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}
|
||||||
className="object-cover w-full h-full"
|
className="object-cover w-full h-full"
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
|
) : isPdfFile(file) || isTextFile(file) ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-1">
|
||||||
|
{pdfInfo?.isExtracting ? (
|
||||||
|
<Loader2 className="h-6 w-6 text-blue-500 mb-1 animate-spin" />
|
||||||
|
) : isPdfFile(file) ? (
|
||||||
|
<FileText className="h-6 w-6 text-red-500 mb-1" />
|
||||||
|
) : (
|
||||||
|
<FileCode className="h-6 w-6 text-blue-500 mb-1" />
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-center truncate w-full px-1">
|
||||||
|
{file.name.length > 10
|
||||||
|
? `${file.name.slice(0, 7)}...`
|
||||||
|
: file.name}
|
||||||
|
</span>
|
||||||
|
{pdfInfo?.isExtracting ? (
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{dict.file.reading}
|
||||||
|
</span>
|
||||||
|
) : pdfInfo?.charCount ? (
|
||||||
|
<span className="text-[10px] text-green-600 font-medium">
|
||||||
|
{formatCharCount(
|
||||||
|
pdfInfo.charCount,
|
||||||
|
)}{" "}
|
||||||
|
{dict.file.chars}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-xs text-center p-1">
|
<div className="flex items-center justify-center h-full text-xs text-center p-1">
|
||||||
{file.name}
|
{file.name}
|
||||||
@@ -54,15 +145,14 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRemoveFile(file)}
|
onClick={() => onRemoveFile(file)}
|
||||||
className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
aria-label="Remove file"
|
aria-label={dict.file.removeFile}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Modal/Lightbox */}
|
{/* Image Modal/Lightbox */}
|
||||||
{selectedImage && (
|
{selectedImage && (
|
||||||
<div
|
<div
|
||||||
@@ -72,7 +162,7 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
|||||||
<button
|
<button
|
||||||
className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors"
|
className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors"
|
||||||
onClick={() => setSelectedImage(null)}
|
onClick={() => setSelectedImage(null)}
|
||||||
aria-label="Close"
|
aria-label={dict.common.close}
|
||||||
>
|
>
|
||||||
<X className="h-6 w-6" />
|
<X className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -84,10 +174,11 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
|||||||
height={900}
|
height={900}
|
||||||
className="object-contain max-w-full max-h-[90vh] w-auto h-auto"
|
className="object-contain max-w-full max-h-[90vh] w-auto h-auto"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
|
import Image from "next/image"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,61 +10,74 @@ 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 { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import { useDiagram } from "@/contexts/diagram-context";
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
|
|
||||||
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 dict = useDictionary()
|
||||||
|
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSelectedIndex(null)
|
||||||
|
onToggleHistory(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmRestore = () => {
|
||||||
|
if (selectedIndex !== null) {
|
||||||
|
// Skip validation for trusted history snapshots
|
||||||
|
onDisplayChart(diagramHistory[selectedIndex].xml, true)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
<Dialog open={showHistory} onOpenChange={onToggleHistory}>
|
||||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Diagram History</DialogTitle>
|
<DialogTitle>{dict.history.title}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Here saved each diagram before AI modification.
|
{dict.history.description}
|
||||||
<br />
|
|
||||||
Click on a diagram to restore it
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{diagramHistory.length === 0 ? (
|
{diagramHistory.length === 0 ? (
|
||||||
<div className="text-center p-4 text-gray-500">
|
<div className="text-center p-4 text-gray-500">
|
||||||
No history available yet. Send messages to create
|
{dict.history.noHistory}
|
||||||
diagram history.
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 py-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 py-4">
|
||||||
{diagramHistory.map((item, index) => (
|
{diagramHistory.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border rounded-md p-2 cursor-pointer hover:border-primary transition-colors"
|
className={`border rounded-md p-2 cursor-pointer hover:border-primary transition-colors ${
|
||||||
onClick={() => {
|
selectedIndex === index
|
||||||
onDisplayChart(item.xml);
|
? "border-primary ring-2 ring-primary"
|
||||||
onToggleHistory(false);
|
: ""
|
||||||
}}
|
}`}
|
||||||
|
onClick={() => setSelectedIndex(index)}
|
||||||
>
|
>
|
||||||
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
|
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
src={item.svg}
|
src={item.svg}
|
||||||
alt={`Diagram version ${index + 1}`}
|
alt={`${dict.history.version} ${index + 1}`}
|
||||||
width={200}
|
width={200}
|
||||||
height={100}
|
height={100}
|
||||||
className="object-contain w-full h-full p-1"
|
className="object-contain w-full h-full p-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-center mt-1 text-gray-500">
|
<div className="text-xs text-center mt-1 text-gray-500">
|
||||||
Version {index + 1}
|
{dict.history.version} {index + 1}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -69,14 +85,30 @@ export function HistoryDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
{selectedIndex !== null ? (
|
||||||
|
<>
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
|
{formatMessage(dict.history.restoreTo, {
|
||||||
|
version: selectedIndex + 1,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onToggleHistory(false)}
|
onClick={() => setSelectedIndex(null)}
|
||||||
>
|
>
|
||||||
Close
|
{dict.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={handleConfirmRestore}>
|
||||||
|
{dict.common.confirm}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" onClick={handleClose}>
|
||||||
|
{dict.common.close}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
1588
components/model-config-dialog.tsx
Normal file
1588
components/model-config-dialog.tsx
Normal file
File diff suppressed because it is too large
Load Diff
303
components/model-selector.tsx
Normal file
303
components/model-selector.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
Bot,
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
Server,
|
||||||
|
Settings2,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import {
|
||||||
|
ModelSelectorContent,
|
||||||
|
ModelSelectorEmpty,
|
||||||
|
ModelSelectorGroup,
|
||||||
|
ModelSelectorInput,
|
||||||
|
ModelSelectorItem,
|
||||||
|
ModelSelectorList,
|
||||||
|
ModelSelectorLogo,
|
||||||
|
ModelSelectorName,
|
||||||
|
ModelSelector as ModelSelectorRoot,
|
||||||
|
ModelSelectorSeparator,
|
||||||
|
ModelSelectorTrigger,
|
||||||
|
} from "@/components/ai-elements/model-selector"
|
||||||
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import type { FlattenedModel } from "@/lib/types/model-config"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface ModelSelectorProps {
|
||||||
|
models: FlattenedModel[]
|
||||||
|
selectedModelId: string | undefined
|
||||||
|
onSelect: (modelId: string | undefined) => void
|
||||||
|
onConfigure: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
showUnvalidatedModels?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map our provider names to models.dev logo names
|
||||||
|
const PROVIDER_LOGO_MAP: Record<string, string> = {
|
||||||
|
openai: "openai",
|
||||||
|
anthropic: "anthropic",
|
||||||
|
google: "google",
|
||||||
|
azure: "azure",
|
||||||
|
bedrock: "amazon-bedrock",
|
||||||
|
openrouter: "openrouter",
|
||||||
|
deepseek: "deepseek",
|
||||||
|
siliconflow: "siliconflow",
|
||||||
|
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
|
||||||
|
gateway: "vercel",
|
||||||
|
edgeone: "tencent-cloud",
|
||||||
|
doubao: "bytedance",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group models by providerLabel (handles duplicate providers)
|
||||||
|
function groupModelsByProvider(
|
||||||
|
models: FlattenedModel[],
|
||||||
|
): Map<string, { provider: string; models: FlattenedModel[] }> {
|
||||||
|
const groups = new Map<
|
||||||
|
string,
|
||||||
|
{ provider: string; models: FlattenedModel[] }
|
||||||
|
>()
|
||||||
|
for (const model of models) {
|
||||||
|
const key = model.providerLabel
|
||||||
|
const existing = groups.get(key)
|
||||||
|
if (existing) {
|
||||||
|
existing.models.push(model)
|
||||||
|
} else {
|
||||||
|
groups.set(key, { provider: model.provider, models: [model] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelSelector({
|
||||||
|
models,
|
||||||
|
selectedModelId,
|
||||||
|
onSelect,
|
||||||
|
onConfigure,
|
||||||
|
disabled = false,
|
||||||
|
showUnvalidatedModels = false,
|
||||||
|
}: ModelSelectorProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
// Filter models based on showUnvalidatedModels setting
|
||||||
|
const displayModels = useMemo(() => {
|
||||||
|
if (showUnvalidatedModels) {
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
return models.filter((m) => m.validated === true)
|
||||||
|
}, [models, showUnvalidatedModels])
|
||||||
|
const groupedModels = useMemo(
|
||||||
|
() => groupModelsByProvider(displayModels),
|
||||||
|
[displayModels],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Find selected model for display
|
||||||
|
const selectedModel = useMemo(
|
||||||
|
() => models.find((m) => m.id === selectedModelId),
|
||||||
|
[models, selectedModelId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSelect = (value: string) => {
|
||||||
|
if (value === "__configure__") {
|
||||||
|
onConfigure()
|
||||||
|
} else if (value === "__server_default__") {
|
||||||
|
onSelect(undefined)
|
||||||
|
} else {
|
||||||
|
onSelect(value)
|
||||||
|
}
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipContent = selectedModel
|
||||||
|
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
|
||||||
|
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
|
||||||
|
|
||||||
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [showLabel, setShowLabel] = useState(true)
|
||||||
|
|
||||||
|
// Threshold (px) under which we hide the label (tweak as needed)
|
||||||
|
const HIDE_THRESHOLD = 240
|
||||||
|
const SHOW_THRESHOLD = 260
|
||||||
|
useEffect(() => {
|
||||||
|
const el = wrapperRef.current
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const target = el.parentElement ?? el
|
||||||
|
|
||||||
|
const ro = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const width = entry.contentRect.width
|
||||||
|
setShowLabel((prev) => {
|
||||||
|
// if currently showing and width dropped below hide threshold -> hide
|
||||||
|
if (prev && width <= HIDE_THRESHOLD) return false
|
||||||
|
// if currently hidden and width rose above show threshold -> show
|
||||||
|
if (!prev && width >= SHOW_THRESHOLD) return true
|
||||||
|
// otherwise keep previous state (hysteresis)
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ro.observe(target)
|
||||||
|
|
||||||
|
const initialWidth = target.getBoundingClientRect().width
|
||||||
|
setShowLabel(initialWidth >= SHOW_THRESHOLD)
|
||||||
|
|
||||||
|
return () => ro.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={wrapperRef} className="inline-block">
|
||||||
|
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
||||||
|
<ModelSelectorTrigger asChild>
|
||||||
|
<ButtonWithTooltip
|
||||||
|
tooltipContent={tooltipContent}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-accent gap-1.5 h-8 px-2 transition-all duration-150 ease-in-out",
|
||||||
|
!showLabel && "px-1.5 justify-center",
|
||||||
|
)}
|
||||||
|
// accessibility: expose label to screen readers
|
||||||
|
aria-label={tooltipContent}
|
||||||
|
>
|
||||||
|
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||||
|
{/* show/hide visible label based on measured width */}
|
||||||
|
{showLabel ? (
|
||||||
|
<span className="text-xs truncate">
|
||||||
|
{selectedModel
|
||||||
|
? selectedModel.modelId
|
||||||
|
: dict.modelConfig.default}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
// Keep an sr-only label for screen readers when hidden
|
||||||
|
<span className="sr-only">
|
||||||
|
{selectedModel
|
||||||
|
? selectedModel.modelId
|
||||||
|
: dict.modelConfig.default}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
</ModelSelectorTrigger>
|
||||||
|
|
||||||
|
<ModelSelectorContent title={dict.modelConfig.selectModel}>
|
||||||
|
<ModelSelectorInput
|
||||||
|
placeholder={dict.modelConfig.searchModels}
|
||||||
|
/>
|
||||||
|
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
|
<ModelSelectorEmpty>
|
||||||
|
{displayModels.length === 0 && models.length > 0
|
||||||
|
? dict.modelConfig.noVerifiedModels
|
||||||
|
: dict.modelConfig.noModelsFound}
|
||||||
|
</ModelSelectorEmpty>
|
||||||
|
|
||||||
|
{/* Server Default Option */}
|
||||||
|
<ModelSelectorGroup heading={dict.modelConfig.default}>
|
||||||
|
<ModelSelectorItem
|
||||||
|
value="__server_default__"
|
||||||
|
onSelect={handleSelect}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer",
|
||||||
|
!selectedModelId && "bg-accent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
!selectedModelId
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<ModelSelectorName>
|
||||||
|
{dict.modelConfig.serverDefault}
|
||||||
|
</ModelSelectorName>
|
||||||
|
</ModelSelectorItem>
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
|
||||||
|
{/* Configured Models by Provider */}
|
||||||
|
{Array.from(groupedModels.entries()).map(
|
||||||
|
([
|
||||||
|
providerLabel,
|
||||||
|
{ provider, models: providerModels },
|
||||||
|
]) => (
|
||||||
|
<ModelSelectorGroup
|
||||||
|
key={providerLabel}
|
||||||
|
heading={providerLabel}
|
||||||
|
>
|
||||||
|
{providerModels.map((model) => (
|
||||||
|
<ModelSelectorItem
|
||||||
|
key={model.id}
|
||||||
|
value={model.modelId}
|
||||||
|
onSelect={() =>
|
||||||
|
handleSelect(model.id)
|
||||||
|
}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
selectedModelId === model.id
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ModelSelectorLogo
|
||||||
|
provider={
|
||||||
|
PROVIDER_LOGO_MAP[
|
||||||
|
provider
|
||||||
|
] || provider
|
||||||
|
}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<ModelSelectorName>
|
||||||
|
{model.modelId}
|
||||||
|
</ModelSelectorName>
|
||||||
|
{model.validated !== true && (
|
||||||
|
<span
|
||||||
|
title={
|
||||||
|
dict.modelConfig
|
||||||
|
.unvalidatedModelWarning
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ModelSelectorItem>
|
||||||
|
))}
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Configure Option */}
|
||||||
|
<ModelSelectorSeparator />
|
||||||
|
<ModelSelectorGroup>
|
||||||
|
<ModelSelectorItem
|
||||||
|
value="__configure__"
|
||||||
|
onSelect={handleSelect}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Settings2 className="mr-2 h-4 w-4" />
|
||||||
|
<ModelSelectorName>
|
||||||
|
{dict.modelConfig.configureModels}
|
||||||
|
</ModelSelectorName>
|
||||||
|
</ModelSelectorItem>
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
{/* Info text */}
|
||||||
|
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
||||||
|
{showUnvalidatedModels
|
||||||
|
? dict.modelConfig.allModelsShown
|
||||||
|
: dict.modelConfig.onlyVerifiedShown}
|
||||||
|
</div>
|
||||||
|
</ModelSelectorList>
|
||||||
|
</ModelSelectorContent>
|
||||||
|
</ModelSelectorRoot>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
125
components/quota-limit-toast.tsx
Normal file
125
components/quota-limit-toast.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Coffee, Settings, X } from "lucide-react"
|
||||||
|
import type React from "react"
|
||||||
|
import { FaGithub } from "react-icons/fa"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
|
|
||||||
|
interface QuotaLimitToastProps {
|
||||||
|
type?: "request" | "token"
|
||||||
|
used: number
|
||||||
|
limit: number
|
||||||
|
onDismiss: () => void
|
||||||
|
onConfigModel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuotaLimitToast({
|
||||||
|
type = "request",
|
||||||
|
used,
|
||||||
|
limit,
|
||||||
|
onDismiss,
|
||||||
|
onConfigModel,
|
||||||
|
}: QuotaLimitToastProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
const isTokenLimit = type === "token"
|
||||||
|
const formatNumber = (n: number) =>
|
||||||
|
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toString()
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="relative w-[400px] overflow-hidden rounded-xl border border-border/50 bg-card p-5 shadow-soft animate-message-in"
|
||||||
|
>
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={onDismiss}
|
||||||
|
className="absolute right-3 top-3 p-1.5 rounded-full text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{/* Title row with icon */}
|
||||||
|
<div className="flex items-center gap-2.5 mb-3 pr-6">
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-accent flex items-center justify-center">
|
||||||
|
<Coffee
|
||||||
|
className="w-4 h-4 text-accent-foreground"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-foreground text-sm">
|
||||||
|
{isTokenLimit
|
||||||
|
? dict.quota.tokenLimit
|
||||||
|
: dict.quota.dailyLimit}
|
||||||
|
</h3>
|
||||||
|
<span className="px-2 py-0.5 text-xs font-medium rounded-md bg-muted text-muted-foreground">
|
||||||
|
{formatMessage(dict.quota.usedOf, {
|
||||||
|
used: formatNumber(used),
|
||||||
|
limit: formatNumber(limit),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Message */}
|
||||||
|
<div className="text-sm text-muted-foreground leading-relaxed mb-4 space-y-2">
|
||||||
|
<p>
|
||||||
|
{isTokenLimit
|
||||||
|
? dict.quota.messageToken
|
||||||
|
: dict.quota.messageApi}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: formatMessage(dict.quota.doubaoSponsorship, {
|
||||||
|
link: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
|
||||||
|
<p>{dict.quota.reset}</p>
|
||||||
|
</div>{" "}
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{onConfigModel && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onConfigModel()
|
||||||
|
onDismiss()
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="w-3.5 h-3.5" />
|
||||||
|
{dict.quota.configModel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<FaGithub className="w-3.5 h-3.5" />
|
||||||
|
{dict.quota.selfHost}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/DayuanJiang"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<Coffee className="w-3.5 h-3.5" />
|
||||||
|
{dict.quota.sponsor}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
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({
|
||||||
@@ -21,14 +22,15 @@ export function ResetWarningModal({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onClear,
|
onClear,
|
||||||
}: ResetWarningModalProps) {
|
}: ResetWarningModalProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Clear Everything?</DialogTitle>
|
<DialogTitle>{dict.dialogs.clearTitle}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This will clear the current conversation and reset the
|
{dict.dialogs.clearDescription}
|
||||||
diagram. This action cannot be undone.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -36,13 +38,13 @@ export function ResetWarningModal({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
{dict.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" onClick={onClear}>
|
<Button variant="destructive" onClick={onClear}>
|
||||||
Clear Everything
|
{dict.dialogs.clearEverything}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
146
components/save-dialog.tsx
Normal file
146
components/save-dialog.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
|
||||||
|
export type ExportFormat = "drawio" | "png" | "svg"
|
||||||
|
|
||||||
|
interface SaveDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onSave: (filename: string, format: ExportFormat) => void
|
||||||
|
defaultFilename: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
defaultFilename,
|
||||||
|
}: SaveDialogProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
const [filename, setFilename] = useState(defaultFilename)
|
||||||
|
const [format, setFormat] = useState<ExportFormat>("drawio")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFilename(defaultFilename)
|
||||||
|
}
|
||||||
|
}, [open, defaultFilename])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const finalFilename = filename.trim() || defaultFilename
|
||||||
|
onSave(finalFilename, format)
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMAT_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: "drawio" as const,
|
||||||
|
label: dict.save.formats.drawio,
|
||||||
|
extension: ".drawio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "png" as const,
|
||||||
|
label: dict.save.formats.png,
|
||||||
|
extension: ".png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "svg" as const,
|
||||||
|
label: dict.save.formats.svg,
|
||||||
|
extension: ".svg",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{dict.save.title}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{dict.save.description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
{dict.save.format}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={format}
|
||||||
|
onValueChange={(v) => setFormat(v as ExportFormat)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FORMAT_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
{dict.save.filename}
|
||||||
|
</label>
|
||||||
|
<div className="flex items-stretch">
|
||||||
|
<Input
|
||||||
|
value={filename}
|
||||||
|
onChange={(e) => setFilename(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={dict.save.filenamePlaceholder}
|
||||||
|
autoFocus
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
className="rounded-r-none border-r-0 focus-visible:z-10"
|
||||||
|
/>
|
||||||
|
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
|
||||||
|
{currentFormat?.extension || ".drawio"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{dict.common.cancel}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>{dict.common.save}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
430
components/settings-dialog.tsx
Normal file
430
components/settings-dialog.tsx
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Github, Info, Moon, Sun, Tag } from "lucide-react"
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { Suspense, useEffect, useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
||||||
|
|
||||||
|
// Reusable setting item component for consistent layout
|
||||||
|
function SettingItem({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-4 first:pt-0 last:pb-0">
|
||||||
|
<div className="space-y-0.5 pr-4">
|
||||||
|
<Label className="text-sm font-medium">{label}</Label>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs text-muted-foreground max-w-[260px]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LANGUAGE_LABELS: Record<Locale, string> = {
|
||||||
|
en: "English",
|
||||||
|
zh: "中文",
|
||||||
|
ja: "日本語",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
|
drawioUi: "min" | "sketch"
|
||||||
|
onToggleDrawioUi: () => void
|
||||||
|
darkMode: boolean
|
||||||
|
onToggleDarkMode: () => void
|
||||||
|
minimalStyle?: boolean
|
||||||
|
onMinimalStyleChange?: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
|
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
||||||
|
const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required"
|
||||||
|
|
||||||
|
function getStoredAccessCodeRequired(): boolean | null {
|
||||||
|
if (typeof window === "undefined") return null
|
||||||
|
const stored = localStorage.getItem(STORAGE_ACCESS_CODE_REQUIRED_KEY)
|
||||||
|
if (stored === null) return null
|
||||||
|
return stored === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsContent({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onCloseProtectionChange,
|
||||||
|
drawioUi,
|
||||||
|
onToggleDrawioUi,
|
||||||
|
darkMode,
|
||||||
|
onToggleDarkMode,
|
||||||
|
minimalStyle = false,
|
||||||
|
onMinimalStyleChange = () => {},
|
||||||
|
}: SettingsDialogProps) {
|
||||||
|
const dict = useDictionary()
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname() || "/"
|
||||||
|
const search = useSearchParams()
|
||||||
|
const [accessCode, setAccessCode] = useState("")
|
||||||
|
const [closeProtection, setCloseProtection] = useState(true)
|
||||||
|
const [isVerifying, setIsVerifying] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
||||||
|
() => getStoredAccessCodeRequired() ?? false,
|
||||||
|
)
|
||||||
|
const [currentLang, setCurrentLang] = useState("en")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only fetch if not cached in localStorage
|
||||||
|
if (getStoredAccessCodeRequired() !== null) return
|
||||||
|
|
||||||
|
fetch(getApiEndpoint("/api/config"))
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
const required = data?.accessCodeRequired === true
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_ACCESS_CODE_REQUIRED_KEY,
|
||||||
|
String(required),
|
||||||
|
)
|
||||||
|
setAccessCodeRequired(required)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Don't cache on error - allow retry on next mount
|
||||||
|
setAccessCodeRequired(false)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Detect current language from pathname
|
||||||
|
useEffect(() => {
|
||||||
|
const seg = pathname.split("/").filter(Boolean)
|
||||||
|
const first = seg[0]
|
||||||
|
if (first && i18n.locales.includes(first as Locale)) {
|
||||||
|
setCurrentLang(first)
|
||||||
|
} else {
|
||||||
|
setCurrentLang(i18n.defaultLocale)
|
||||||
|
}
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
const storedCode =
|
||||||
|
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
|
setAccessCode(storedCode)
|
||||||
|
|
||||||
|
const storedCloseProtection = localStorage.getItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
)
|
||||||
|
// Default to true if not set
|
||||||
|
setCloseProtection(storedCloseProtection !== "false")
|
||||||
|
|
||||||
|
setError("")
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
const changeLanguage = (lang: string) => {
|
||||||
|
// Save locale to localStorage for persistence across restarts
|
||||||
|
localStorage.setItem("next-ai-draw-io-locale", lang)
|
||||||
|
|
||||||
|
const parts = pathname.split("/")
|
||||||
|
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
|
||||||
|
parts[1] = lang
|
||||||
|
} else {
|
||||||
|
parts.splice(1, 0, lang)
|
||||||
|
}
|
||||||
|
const newPath = parts.join("/") || "/"
|
||||||
|
const searchStr = search?.toString() ? `?${search.toString()}` : ""
|
||||||
|
router.push(newPath + searchStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!accessCodeRequired) return
|
||||||
|
|
||||||
|
setError("")
|
||||||
|
setIsVerifying(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
getApiEndpoint("/api/verify-access-code"),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode.trim(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!data.valid) {
|
||||||
|
setError(data.message || dict.errors.invalidAccessCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
||||||
|
onOpenChange(false)
|
||||||
|
} catch {
|
||||||
|
setError(dict.errors.networkError)
|
||||||
|
} finally {
|
||||||
|
setIsVerifying(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent className="sm:max-w-lg p-0 gap-0">
|
||||||
|
{/* Header */}
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-4">
|
||||||
|
<DialogTitle>{dict.settings.title}</DialogTitle>
|
||||||
|
<DialogDescription className="mt-1">
|
||||||
|
{dict.settings.description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="divide-y divide-border-subtle">
|
||||||
|
{/* Access Code (conditional) */}
|
||||||
|
{accessCodeRequired && (
|
||||||
|
<div className="py-4 first:pt-0 space-y-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label
|
||||||
|
htmlFor="access-code"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
{dict.settings.accessCode}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{dict.settings.accessCodeDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="access-code"
|
||||||
|
type="password"
|
||||||
|
value={accessCode}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAccessCode(e.target.value)
|
||||||
|
}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={
|
||||||
|
dict.settings.accessCodePlaceholder
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isVerifying || !accessCode.trim()}
|
||||||
|
className="h-9 px-4 rounded-xl"
|
||||||
|
>
|
||||||
|
{isVerifying ? "..." : dict.common.save}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Language */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.language}
|
||||||
|
description={dict.settings.languageDescription}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={currentLang}
|
||||||
|
onValueChange={changeLanguage}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="language-select"
|
||||||
|
className="w-[120px] h-9 rounded-xl"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{i18n.locales.map((locale) => (
|
||||||
|
<SelectItem key={locale} value={locale}>
|
||||||
|
{LANGUAGE_LABELS[locale]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Theme */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.theme}
|
||||||
|
description={dict.settings.themeDescription}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="theme-toggle"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={onToggleDarkMode}
|
||||||
|
className="h-9 w-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Draw.io Style */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.drawioStyle}
|
||||||
|
description={`${dict.settings.drawioStyleDescription} ${
|
||||||
|
drawioUi === "min"
|
||||||
|
? dict.settings.minimal
|
||||||
|
: dict.settings.sketch
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="drawio-ui"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onToggleDrawioUi}
|
||||||
|
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
|
||||||
|
>
|
||||||
|
{dict.settings.switchTo}{" "}
|
||||||
|
{drawioUi === "min"
|
||||||
|
? dict.settings.sketch
|
||||||
|
: dict.settings.minimal}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Close Protection */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.closeProtection}
|
||||||
|
description={dict.settings.closeProtectionDescription}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="close-protection"
|
||||||
|
checked={closeProtection}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCloseProtection(checked)
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
checked.toString(),
|
||||||
|
)
|
||||||
|
onCloseProtectionChange?.(checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Diagram Style */}
|
||||||
|
<SettingItem
|
||||||
|
label={dict.settings.diagramStyle}
|
||||||
|
description={dict.settings.diagramStyleDescription}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="minimal-style"
|
||||||
|
checked={minimalStyle}
|
||||||
|
onCheckedChange={onMinimalStyleChange}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{minimalStyle
|
||||||
|
? dict.chat.minimalStyle
|
||||||
|
: dict.chat.styledMode}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<Tag className="h-3 w-3" />
|
||||||
|
{process.env.APP_VERSION}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">·</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Github className="h-3 w-3" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
{process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
||||||
|
"true" && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">·</span>
|
||||||
|
<a
|
||||||
|
href={`/${currentLang}/about${currentLang === "zh" ? "/cn" : currentLang === "ja" ? "/ja" : ""}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Info className="h-3 w-3" />
|
||||||
|
{dict.nav.about}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsDialog(props: SettingsDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<DialogContent className="sm:max-w-lg p-0">
|
||||||
|
<div className="h-80 flex items-center justify-center">
|
||||||
|
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsContent {...props} />
|
||||||
|
</Suspense>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
157
components/ui/alert-dialog.tsx
Normal file
157
components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogAction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
"bg-primary text-primary-foreground shadow-xs hover:brightness-75",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
33
components/ui/collapsible.tsx
Normal file
33
components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
191
components/ui/command.tsx
Normal file
191
components/ui/command.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent className={cn("overflow-hidden p-0", className)}>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
// Ensure hover updates selection for visual feedback
|
||||||
|
const item = e.currentTarget
|
||||||
|
item.setAttribute("data-selected", "true")
|
||||||
|
// Deselect siblings
|
||||||
|
const siblings = item.parentElement?.querySelectorAll("[cmdk-item]")
|
||||||
|
siblings?.forEach((sibling) => {
|
||||||
|
if (sibling !== item) {
|
||||||
|
sibling.setAttribute("data-selected", "false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
@@ -38,7 +38,10 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
"duration-200",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -57,13 +60,32 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
// Base styles
|
||||||
|
"fixed top-[50%] left-[50%] z-50 w-full",
|
||||||
|
"max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%]",
|
||||||
|
"grid gap-4 p-6",
|
||||||
|
// Refined visual treatment
|
||||||
|
"bg-surface-0 rounded-2xl border border-border-subtle shadow-dialog",
|
||||||
|
// Entry/exit animations
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
"data-[state=closed]:zoom-out-[0.98] data-[state=open]:zoom-in-[0.98]",
|
||||||
|
"data-[state=closed]:slide-out-to-top-[2%] data-[state=open]:slide-in-from-top-[2%]",
|
||||||
|
"duration-200 sm:max-w-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
<DialogPrimitive.Close className={cn(
|
||||||
|
"absolute top-4 right-4 rounded-xl p-1.5",
|
||||||
|
"text-muted-foreground/60 hover:text-foreground",
|
||||||
|
"hover:bg-interactive-hover",
|
||||||
|
"transition-all duration-150",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
"disabled:pointer-events-none",
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4"
|
||||||
|
)}>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
@@ -102,7 +124,10 @@ function DialogTitle({
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn(
|
||||||
|
"text-xl font-semibold tracking-tight leading-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -115,7 +140,10 @@ function DialogDescription({
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,30 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
// Base styles
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"flex h-10 w-full min-w-0 rounded-xl px-3.5 py-2",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"border border-border-subtle bg-surface-1",
|
||||||
|
"text-sm text-foreground",
|
||||||
|
// Placeholder
|
||||||
|
"placeholder:text-muted-foreground/60",
|
||||||
|
// Selection
|
||||||
|
"selection:bg-primary selection:text-primary-foreground",
|
||||||
|
// Transitions
|
||||||
|
"transition-all duration-150 ease-out",
|
||||||
|
// Hover state
|
||||||
|
"hover:border-border-default",
|
||||||
|
// Focus state - refined ring
|
||||||
|
"focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10",
|
||||||
|
// File input
|
||||||
|
"file:text-foreground file:inline-flex file:h-7 file:border-0",
|
||||||
|
"file:bg-transparent file:text-sm file:font-medium",
|
||||||
|
// Disabled
|
||||||
|
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
// Invalid state
|
||||||
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
||||||
|
"dark:aria-invalid:ring-destructive/40",
|
||||||
|
// Dark mode background
|
||||||
|
"dark:bg-surface-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
48
components/ui/popover.tsx
Normal file
48
components/ui/popover.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
56
components/ui/resizable.tsx
Normal file
56
components/ui/resizable.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { GripVerticalIcon } from "lucide-react"
|
||||||
|
import * as ResizablePrimitive from "react-resizable-panels"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ResizablePanelGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||||
|
return (
|
||||||
|
<ResizablePrimitive.PanelGroup
|
||||||
|
data-slot="resizable-panel-group"
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizablePanel({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||||
|
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizableHandle({
|
||||||
|
withHandle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||||
|
withHandle?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ResizablePrimitive.PanelResizeHandle
|
||||||
|
data-slot="resizable-handle"
|
||||||
|
className={cn(
|
||||||
|
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{withHandle && (
|
||||||
|
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||||
|
<GripVerticalIcon className="size-2.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
@@ -18,7 +18,7 @@ function ScrollArea({
|
|||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 !overflow-x-hidden"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
|||||||
187
components/ui/select.tsx
Normal file
187
components/ui/select.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
31
components/ui/switch.tsx
Normal file
31
components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
@@ -1,72 +1,344 @@
|
|||||||
"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, useEffect, useRef, useState } from "react"
|
||||||
import { extractDiagramXML } from "../lib/utils";
|
import type { DrawIoEmbedRef } from "react-drawio"
|
||||||
|
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
|
||||||
|
import type { ExportFormat } from "@/components/save-dialog"
|
||||||
|
import { getApiEndpoint } from "@/lib/base-path"
|
||||||
|
import { extractDiagramXML, validateAndFixXml } 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, skipValidation?: boolean) => string | null
|
||||||
handleExport: () => void;
|
handleExport: () => void
|
||||||
resolverRef: React.Ref<((value: string) => void) | null>;
|
handleExportWithoutHistory: () => void
|
||||||
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
resolverRef: React.Ref<((value: string) => void) | null>
|
||||||
handleDiagramExport: (data: any) => void;
|
drawioRef: React.Ref<DrawIoEmbedRef | null>
|
||||||
clearDiagram: () => void;
|
handleDiagramExport: (data: any) => void
|
||||||
|
clearDiagram: () => void
|
||||||
|
saveDiagramToFile: (
|
||||||
|
filename: string,
|
||||||
|
format: ExportFormat,
|
||||||
|
sessionId?: string,
|
||||||
|
) => void
|
||||||
|
saveDiagramToStorage: () => Promise<void>
|
||||||
|
isDrawioReady: boolean
|
||||||
|
onDrawioLoad: () => void
|
||||||
|
resetDrawioReady: () => void
|
||||||
|
showSaveDialog: boolean
|
||||||
|
setShowSaveDialog: (show: boolean) => 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 [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||||
const resolverRef = useRef<((value: string) => void) | null>(null);
|
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||||
|
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
||||||
|
const hasCalledOnLoadRef = useRef(false)
|
||||||
|
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
||||||
|
const resolverRef = useRef<((value: string) => void) | null>(null)
|
||||||
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
|
const expectHistoryExportRef = useRef<boolean>(false)
|
||||||
|
// Track if diagram has been restored from localStorage
|
||||||
|
const hasDiagramRestoredRef = useRef<boolean>(false)
|
||||||
|
|
||||||
|
const onDrawioLoad = () => {
|
||||||
|
// Only set ready state once to prevent infinite loops
|
||||||
|
if (hasCalledOnLoadRef.current) return
|
||||||
|
hasCalledOnLoadRef.current = true
|
||||||
|
// console.log("[DiagramContext] DrawIO loaded, setting ready state")
|
||||||
|
setIsDrawioReady(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetDrawioReady = () => {
|
||||||
|
// console.log("[DiagramContext] Resetting DrawIO ready state")
|
||||||
|
hasCalledOnLoadRef.current = false
|
||||||
|
setIsDrawioReady(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore diagram XML when DrawIO becomes ready
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- loadDiagram uses refs internally and is stable
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it)
|
||||||
|
if (!isDrawioReady) {
|
||||||
|
hasDiagramRestoredRef.current = false
|
||||||
|
setCanSaveDiagram(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (hasDiagramRestoredRef.current) return
|
||||||
|
hasDiagramRestoredRef.current = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedDiagramXml = localStorage.getItem(
|
||||||
|
STORAGE_DIAGRAM_XML_KEY,
|
||||||
|
)
|
||||||
|
if (savedDiagramXml) {
|
||||||
|
// Skip validation for trusted saved diagrams
|
||||||
|
loadDiagram(savedDiagramXml, true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restore diagram from localStorage:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow saving after restore is complete
|
||||||
|
setTimeout(() => {
|
||||||
|
setCanSaveDiagram(true)
|
||||||
|
}, 500)
|
||||||
|
}, [isDrawioReady])
|
||||||
|
|
||||||
|
// Save diagram XML to localStorage whenever it changes (debounced)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canSaveDiagram) return
|
||||||
|
if (!chartXML || chartXML.length <= 300) return
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId)
|
||||||
|
}, [chartXML, canSaveDiagram])
|
||||||
|
|
||||||
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
|
const saveResolverRef = useRef<{
|
||||||
|
resolver: ((data: string) => void) | null
|
||||||
|
format: ExportFormat | null
|
||||||
|
}>({ resolver: null, format: null })
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
|
// Mark that this export should be saved to history
|
||||||
|
expectHistoryExportRef.current = true
|
||||||
drawioRef.current.exportDiagram({
|
drawioRef.current.exportDiagram({
|
||||||
format: "xmlsvg",
|
format: "xmlsvg",
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const loadDiagram = (chart: string) => {
|
const handleExportWithoutHistory = () => {
|
||||||
|
if (drawioRef.current) {
|
||||||
|
// Export without saving to history (for edit_diagram fetching current state)
|
||||||
|
drawioRef.current.exportDiagram({
|
||||||
|
format: "xmlsvg",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current diagram to localStorage (used before theme/UI changes)
|
||||||
|
const saveDiagramToStorage = async (): Promise<void> => {
|
||||||
|
if (!drawioRef.current) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentXml = await Promise.race([
|
||||||
|
new Promise<string>((resolve) => {
|
||||||
|
resolverRef.current = resolve
|
||||||
|
drawioRef.current?.exportDiagram({ format: "xmlsvg" })
|
||||||
|
}),
|
||||||
|
new Promise<string>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("Export timeout")), 2000),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Only save if diagram has meaningful content (not empty template)
|
||||||
|
if (currentXml && currentXml.length > 300) {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, currentXml)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save diagram to storage:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDiagram = (
|
||||||
|
chart: string,
|
||||||
|
skipValidation?: boolean,
|
||||||
|
): string | null => {
|
||||||
|
let xmlToLoad = chart
|
||||||
|
|
||||||
|
// Validate XML structure before loading (unless skipped for internal use)
|
||||||
|
if (!skipValidation) {
|
||||||
|
const validation = validateAndFixXml(chart)
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.warn(
|
||||||
|
"[loadDiagram] Validation error:",
|
||||||
|
validation.error,
|
||||||
|
)
|
||||||
|
return validation.error
|
||||||
|
}
|
||||||
|
// Use fixed XML if auto-fix was applied
|
||||||
|
if (validation.fixed) {
|
||||||
|
console.log(
|
||||||
|
"[loadDiagram] Auto-fixed XML issues:",
|
||||||
|
validation.fixes,
|
||||||
|
)
|
||||||
|
xmlToLoad = validation.fixed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
|
||||||
|
setChartXML(xmlToLoad)
|
||||||
|
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: chart,
|
xml: xmlToLoad,
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleDiagramExport = (data: any) => {
|
const handleDiagramExport = (data: any) => {
|
||||||
const extractedXML = extractDiagramXML(data.data);
|
// Handle save to file if requested (process raw data before extraction)
|
||||||
setChartXML(extractedXML);
|
if (saveResolverRef.current.resolver) {
|
||||||
setLatestSvg(data.data);
|
const format = saveResolverRef.current.format
|
||||||
setDiagramHistory((prev) => [
|
saveResolverRef.current.resolver(data.data)
|
||||||
|
saveResolverRef.current = { resolver: null, format: null }
|
||||||
|
// For non-xmlsvg formats, skip XML extraction as it will fail
|
||||||
|
// Only drawio (which uses xmlsvg internally) has the content attribute
|
||||||
|
if (format === "png" || format === "svg") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedXML = extractDiagramXML(data.data)
|
||||||
|
setChartXML(extractedXML)
|
||||||
|
setLatestSvg(data.data)
|
||||||
|
|
||||||
|
// Only add to history if this was a user-initiated export
|
||||||
|
// Limit to 20 entries to prevent memory leaks during long sessions
|
||||||
|
const MAX_HISTORY_SIZE = 20
|
||||||
|
if (expectHistoryExportRef.current) {
|
||||||
|
setDiagramHistory((prev) => {
|
||||||
|
const newHistory = [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
svg: data.data,
|
svg: data.data,
|
||||||
xml: extractedXML,
|
xml: extractedXML,
|
||||||
},
|
},
|
||||||
]);
|
]
|
||||||
if (resolverRef.current) {
|
// Keep only the last MAX_HISTORY_SIZE entries (circular buffer)
|
||||||
resolverRef.current(extractedXML);
|
return newHistory.slice(-MAX_HISTORY_SIZE)
|
||||||
resolverRef.current = null;
|
})
|
||||||
|
expectHistoryExportRef.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolverRef.current) {
|
||||||
|
resolverRef.current(extractedXML)
|
||||||
|
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);
|
// Skip validation for trusted internal template (loadDiagram also sets chartXML)
|
||||||
setChartXML(emptyDiagram);
|
loadDiagram(emptyDiagram, true)
|
||||||
setLatestSvg("");
|
setLatestSvg("")
|
||||||
setDiagramHistory([]);
|
setDiagramHistory([])
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const saveDiagramToFile = (
|
||||||
|
filename: string,
|
||||||
|
format: ExportFormat,
|
||||||
|
sessionId?: string,
|
||||||
|
) => {
|
||||||
|
if (!drawioRef.current) {
|
||||||
|
console.warn("Draw.io editor not ready")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map format to draw.io export format
|
||||||
|
const drawioFormat = format === "drawio" ? "xmlsvg" : format
|
||||||
|
|
||||||
|
// Set up the resolver before triggering export
|
||||||
|
saveResolverRef.current = {
|
||||||
|
resolver: (exportData: string) => {
|
||||||
|
let fileContent: string | Blob
|
||||||
|
let mimeType: string
|
||||||
|
let extension: string
|
||||||
|
|
||||||
|
if (format === "drawio") {
|
||||||
|
// Extract XML from SVG for .drawio format
|
||||||
|
const xml = extractDiagramXML(exportData)
|
||||||
|
let xmlContent = xml
|
||||||
|
if (!xml.includes("<mxfile")) {
|
||||||
|
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`
|
||||||
|
}
|
||||||
|
fileContent = xmlContent
|
||||||
|
mimeType = "application/xml"
|
||||||
|
extension = ".drawio"
|
||||||
|
|
||||||
|
// Save to localStorage when user manually saves
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xmlContent)
|
||||||
|
} else if (format === "png") {
|
||||||
|
// PNG data comes as base64 data URL
|
||||||
|
fileContent = exportData
|
||||||
|
mimeType = "image/png"
|
||||||
|
extension = ".png"
|
||||||
|
} else {
|
||||||
|
// SVG format
|
||||||
|
fileContent = exportData
|
||||||
|
mimeType = "image/svg+xml"
|
||||||
|
extension = ".svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log save event to Langfuse (flags the trace)
|
||||||
|
logSaveToLangfuse(filename, format, sessionId)
|
||||||
|
|
||||||
|
// Handle download
|
||||||
|
let url: string
|
||||||
|
if (
|
||||||
|
typeof fileContent === "string" &&
|
||||||
|
fileContent.startsWith("data:")
|
||||||
|
) {
|
||||||
|
// Already a data URL (PNG)
|
||||||
|
url = fileContent
|
||||||
|
} else {
|
||||||
|
const blob = new Blob([fileContent], { type: mimeType })
|
||||||
|
url = URL.createObjectURL(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = `${filename}${extension}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
|
||||||
|
// Delay URL revocation to ensure download completes
|
||||||
|
if (!url.startsWith("data:")) {
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export diagram - callback will be handled in handleDiagramExport
|
||||||
|
drawioRef.current.exportDiagram({ format: drawioFormat })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log save event to Langfuse (just flags the trace, doesn't send content)
|
||||||
|
const logSaveToLangfuse = async (
|
||||||
|
filename: string,
|
||||||
|
format: string,
|
||||||
|
sessionId?: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await fetch(getApiEndpoint("/api/log-save"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ filename, format, sessionId }),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to log save to Langfuse:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiagramContext.Provider
|
<DiagramContext.Provider
|
||||||
@@ -76,21 +348,29 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
diagramHistory,
|
diagramHistory,
|
||||||
loadDiagram,
|
loadDiagram,
|
||||||
handleExport,
|
handleExport,
|
||||||
|
handleExportWithoutHistory,
|
||||||
resolverRef,
|
resolverRef,
|
||||||
drawioRef,
|
drawioRef,
|
||||||
handleDiagramExport,
|
handleDiagramExport,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
|
saveDiagramToFile,
|
||||||
|
saveDiagramToStorage,
|
||||||
|
isDrawioReady,
|
||||||
|
onDrawioLoad,
|
||||||
|
resetDrawioReady,
|
||||||
|
showSaveDialog,
|
||||||
|
setShowSaveDialog,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{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
|
||||||
}
|
}
|
||||||
|
|||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
drawio:
|
||||||
|
image: jgraph/drawio:latest
|
||||||
|
ports: ["8080:8080"]
|
||||||
|
next-ai-draw-io:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
||||||
|
# Uncomment below for subdirectory deployment
|
||||||
|
# - NEXT_PUBLIC_BASE_PATH=/nextaidrawio
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
# For subdirectory deployment, uncomment and set your path:
|
||||||
|
# NEXT_PUBLIC_BASE_PATH: /nextaidrawio
|
||||||
|
depends_on: [drawio]
|
||||||
244
docs/cn/README_CN.md
Normal file
244
docs/cn/README_CN.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Next AI Draw.io
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**AI驱动的图表创建工具 - 对话、绘制、可视化**
|
||||||
|
|
||||||
|
[English](../../README.md) | 中文 | [日本語](../ja/README_JA.md)
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://react.dev/)
|
||||||
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
|
||||||
|
|
||||||
|
> 注:感谢 <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 的赞助支持,本项目的 Demo 现已接入强大的 K2-thinking 模型!
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
- [Next AI Draw.io](#next-ai-drawio)
|
||||||
|
- [目录](#目录)
|
||||||
|
- [示例](#示例)
|
||||||
|
- [功能特性](#功能特性)
|
||||||
|
- [MCP服务器(预览)](#mcp服务器预览)
|
||||||
|
- [Claude Code CLI](#claude-code-cli)
|
||||||
|
- [快速开始](#快速开始)
|
||||||
|
- [在线试用](#在线试用)
|
||||||
|
- [桌面应用](#桌面应用)
|
||||||
|
- [使用Docker运行](#使用docker运行)
|
||||||
|
- [安装](#安装)
|
||||||
|
- [部署](#部署)
|
||||||
|
- [部署到腾讯云EdgeOne Pages](#部署到腾讯云edgeone-pages)
|
||||||
|
- [部署到Vercel(推荐)](#部署到vercel推荐)
|
||||||
|
- [部署到Cloudflare Workers](#部署到cloudflare-workers)
|
||||||
|
- [多提供商支持](#多提供商支持)
|
||||||
|
- [工作原理](#工作原理)
|
||||||
|
- [支持与联系](#支持与联系)
|
||||||
|
- [Star历史](#star历史)
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
以下是一些示例提示词及其生成的图表:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top" align="center">
|
||||||
|
<strong>动画Transformer连接器</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
|
||||||
|
<img src="../../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>GCP架构图</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
|
<img src="../../public/gcp_demo.svg" alt="GCP架构图" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>AWS架构图</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
|
<img src="../../public/aws_demo.svg" alt="AWS架构图" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>Azure架构图</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中,用户连接到托管在实例上的前端。</p>
|
||||||
|
<img src="../../public/azure_demo.svg" alt="Azure架构图" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>猫咪素描</strong><br />
|
||||||
|
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
|
||||||
|
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **LLM驱动的图表创建**:利用大语言模型通过自然语言命令直接创建和操作draw.io图表
|
||||||
|
- **基于图像的图表复制**:上传现有图表或图像,让AI自动复制和增强
|
||||||
|
- **PDF和文本文件上传**:上传PDF文档和文本文件,提取内容并从现有文档生成图表
|
||||||
|
- **AI推理过程显示**:查看支持模型的AI思考过程(OpenAI o1/o3、Gemini、Claude等)
|
||||||
|
- **图表历史记录**:全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本
|
||||||
|
- **交互式聊天界面**:与AI实时对话来完善您的图表
|
||||||
|
- **云架构图支持**:专门支持生成云架构图(AWS、GCP、Azure)
|
||||||
|
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
|
||||||
|
|
||||||
|
## MCP服务器(预览)
|
||||||
|
|
||||||
|
> **预览功能**:此功能为实验性功能,可能不稳定。
|
||||||
|
|
||||||
|
通过MCP(模型上下文协议)在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
然后让Claude创建图表:
|
||||||
|
> "创建一个展示用户认证流程的流程图,包含登录、MFA和会话管理"
|
||||||
|
|
||||||
|
图表会实时显示在浏览器中!
|
||||||
|
|
||||||
|
详情请参阅[MCP服务器README](../../packages/mcp-server/README.md),了解VS Code、Cursor等客户端配置。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 在线试用
|
||||||
|
|
||||||
|
无需安装!直接在我们的演示站点试用:
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
> **使用自己的 API Key**:您可以使用自己的 API Key 来绕过演示站点的用量限制。点击聊天面板中的设置图标即可配置您的 Provider 和 API Key。您的 Key 仅保存在浏览器本地,不会被存储在服务器上。
|
||||||
|
|
||||||
|
### 桌面应用
|
||||||
|
|
||||||
|
从 [Releases 页面](https://github.com/DayuanJiang/next-ai-draw-io/releases) 下载适用于您平台的原生桌面应用:
|
||||||
|
|
||||||
|
支持的平台:Windows、macOS、Linux。
|
||||||
|
|
||||||
|
### 使用Docker运行
|
||||||
|
|
||||||
|
[查看 Docker 指南](./docker.md)
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
1. 克隆仓库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DayuanJiang/next-ai-draw-io
|
||||||
|
cd next-ai-draw-io
|
||||||
|
npm install
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
详细设置说明请参阅[提供商配置指南](./ai-providers.md)。
|
||||||
|
|
||||||
|
2. 运行开发服务器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在浏览器中打开 [http://localhost:6002](http://localhost:6002) 查看应用。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### 部署到腾讯云EdgeOne Pages
|
||||||
|
|
||||||
|
您可以通过[腾讯云EdgeOne Pages](https://pages.edgeone.ai/zh)一键部署。
|
||||||
|
|
||||||
|
直接点击此按钮一键部署:
|
||||||
|
[](https://console.cloud.tencent.com/edgeone/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
查看[腾讯云EdgeOne Pages文档](https://pages.edgeone.ai/zh/document/product-introduction)了解更多详情。
|
||||||
|
|
||||||
|
同时,通过腾讯云EdgeOne Pages部署,也会获得[每日免费的DeepSeek模型额度](https://edgeone.cloud.tencent.com/pages/document/169925463311781888)。
|
||||||
|
|
||||||
|
### 部署到Vercel(推荐)
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。请确保在Vercel控制台中**设置环境变量**,就像您在本地 `.env.local` 文件中所做的那样。
|
||||||
|
|
||||||
|
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
|
||||||
|
|
||||||
|
### 部署到Cloudflare Workers
|
||||||
|
|
||||||
|
[查看 Cloudflare 部署指南](./cloudflare-deploy.md)
|
||||||
|
|
||||||
|
|
||||||
|
## 多提供商支持
|
||||||
|
|
||||||
|
- [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||||
|
- AWS Bedrock(默认)
|
||||||
|
- OpenAI
|
||||||
|
- Anthropic
|
||||||
|
- Google AI
|
||||||
|
- Azure OpenAI
|
||||||
|
- Ollama
|
||||||
|
- OpenRouter
|
||||||
|
- DeepSeek
|
||||||
|
- SiliconFlow
|
||||||
|
- SGLang
|
||||||
|
- Vercel AI Gateway
|
||||||
|
|
||||||
|
除AWS Bedrock和OpenRouter外,所有提供商都支持自定义端点。
|
||||||
|
|
||||||
|
📖 **[详细的提供商配置指南](./ai-providers.md)** - 查看各提供商的设置说明。
|
||||||
|
|
||||||
|
**模型要求**:此任务需要强大的模型能力,因为它涉及生成具有严格格式约束的长文本(draw.io XML)。推荐使用 Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro 和 DeepSeek V3.2/R1。
|
||||||
|
|
||||||
|
注意:`claude` 系列已在带有 AWS、Azure、GCP 等云架构 Logo 的 draw.io 图表上进行训练,因此如果您想创建云架构图,这是最佳选择。
|
||||||
|
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
本应用使用以下技术:
|
||||||
|
|
||||||
|
- **Next.js**:用于前端框架和路由
|
||||||
|
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):用于流式AI响应和多提供商支持
|
||||||
|
- **react-drawio**:用于图表表示和操作
|
||||||
|
|
||||||
|
图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
|
||||||
|
|
||||||
|
|
||||||
|
## 支持与联系
|
||||||
|
|
||||||
|
**特别感谢[字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)赞助演示站点的 API Token 使用!** 注册火山引擎 ARK 平台即可获得50万免费Token!
|
||||||
|
|
||||||
|
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
|
||||||
|
|
||||||
|
如需支持或咨询,请在GitHub仓库上提交issue或联系维护者:
|
||||||
|
|
||||||
|
- 邮箱:me[at]jiang.jp
|
||||||
|
|
||||||
|
## Star历史
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
|
||||||
|
|
||||||
|
---
|
||||||
236
docs/cn/ai-providers.md
Normal file
236
docs/cn/ai-providers.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# AI 提供商配置
|
||||||
|
|
||||||
|
本指南介绍如何为 next-ai-draw-io 配置不同的 AI 模型提供商。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
1. 将 `.env.example` 复制为 `.env.local`
|
||||||
|
2. 设置所选提供商的 API 密钥
|
||||||
|
3. 将 `AI_MODEL` 设置为所需的模型
|
||||||
|
4. 运行 `npm run dev`
|
||||||
|
|
||||||
|
## 支持的提供商
|
||||||
|
|
||||||
|
### 豆包 (字节跳动火山引擎)
|
||||||
|
|
||||||
|
> **免费 Token**:在 [火山引擎 ARK 平台](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 注册,即可获得所有模型 50 万免费 Token!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOUBAO_API_KEY=your_api_key
|
||||||
|
AI_MODEL=doubao-seed-1-8-251215 # 或其他豆包模型
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google Gemini
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
|
||||||
|
AI_MODEL=gemini-2.0-flash
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENAI_API_KEY=your_api_key
|
||||||
|
AI_MODEL=gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点(用于 OpenAI 兼容服务):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anthropic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_API_KEY=your_api_key
|
||||||
|
AI_MODEL=claude-sonnet-4-5-20250514
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeepSeek
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEEPSEEK_API_KEY=your_api_key
|
||||||
|
AI_MODEL=deepseek-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEEPSEEK_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### SiliconFlow (OpenAI 兼容)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SILICONFLOW_API_KEY=your_api_key
|
||||||
|
AI_MODEL=deepseek-ai/DeepSeek-V3 # 示例;使用任何 SiliconFlow 模型 ID
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点(默认为推荐域名):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # 或 https://api.siliconflow.cn/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### SGLang
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SGLANG_API_KEY=your_api_key
|
||||||
|
AI_MODEL=your_model_id
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SGLANG_BASE_URL=https://your-custom-endpoint/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Azure OpenAI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_RESOURCE_NAME=your-resource-name # 必填:您的 Azure 资源名称
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用自定义端点代替资源名称:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_BASE_URL=https://your-resource.openai.azure.com # AZURE_RESOURCE_NAME 的替代方案
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的推理配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_REASONING_EFFORT=low # 可选:low, medium, high
|
||||||
|
AZURE_REASONING_SUMMARY=detailed # 可选:none, brief, detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS Bedrock
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AWS_REGION=us-west-2
|
||||||
|
AWS_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_secret_access_key
|
||||||
|
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:在 AWS 环境(Lambda、带有 IAM 角色的 EC2)中,凭证会自动从 IAM 角色获取。
|
||||||
|
|
||||||
|
### OpenRouter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENROUTER_API_KEY=your_api_key
|
||||||
|
AI_MODEL=anthropic/claude-sonnet-4
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义端点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENROUTER_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ollama (本地)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=ollama
|
||||||
|
AI_MODEL=llama3.2
|
||||||
|
```
|
||||||
|
|
||||||
|
可选的自定义 URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vercel AI Gateway
|
||||||
|
|
||||||
|
Vercel AI Gateway 通过单个 API 密钥提供对多个 AI 提供商的统一访问。这简化了身份验证,让您无需管理多个 API 密钥即可在不同提供商之间切换。
|
||||||
|
|
||||||
|
**基本用法(Vercel 托管网关):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_gateway_api_key
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
**自定义网关 URL(用于本地开发或自托管网关):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_custom_api_key
|
||||||
|
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
模型格式使用 `provider/model` 语法:
|
||||||
|
|
||||||
|
- `openai/gpt-4o` - OpenAI GPT-4o
|
||||||
|
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
|
||||||
|
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
|
||||||
|
|
||||||
|
**配置说明:**
|
||||||
|
|
||||||
|
- 如果未设置 `AI_GATEWAY_BASE_URL`,则使用默认的 Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`)
|
||||||
|
- 自定义基础 URL 适用于:
|
||||||
|
- 使用自定义网关实例进行本地开发
|
||||||
|
- 自托管 AI Gateway 部署
|
||||||
|
- 企业代理配置
|
||||||
|
- 当使用自定义基础 URL 时,必须同时提供 `AI_GATEWAY_API_KEY`
|
||||||
|
|
||||||
|
从 [Vercel AI Gateway 仪表板](https://vercel.com/ai-gateway) 获取您的 API 密钥。
|
||||||
|
|
||||||
|
## 自动检测
|
||||||
|
|
||||||
|
如果您只配置了**一个**提供商的 API 密钥,系统将自动检测并使用该提供商。无需设置 `AI_PROVIDER`。
|
||||||
|
|
||||||
|
如果您配置了**多个** API 密钥,则必须显式设置 `AI_PROVIDER`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=google # 或:openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模型能力要求
|
||||||
|
|
||||||
|
此任务对模型能力要求极高,因为它涉及生成具有严格格式约束(draw.io XML)的长文本。
|
||||||
|
|
||||||
|
**推荐模型**:
|
||||||
|
|
||||||
|
- Claude Sonnet 4.5 / Opus 4.5
|
||||||
|
|
||||||
|
**关于 Ollama 的说明**:虽然支持将 Ollama 作为提供商,但除非您在本地运行像 DeepSeek R1 或 Qwen3-235B 这样的高性能模型,否则对于此用例通常不太实用。
|
||||||
|
|
||||||
|
## 温度设置 (Temperature)
|
||||||
|
|
||||||
|
您可以通过环境变量选择性地配置温度:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TEMPERATURE=0 # 输出更具确定性(推荐用于图表)
|
||||||
|
```
|
||||||
|
|
||||||
|
**重要提示**:对于不支持温度设置的模型(例如以下模型),请勿设置 `TEMPERATURE`:
|
||||||
|
- GPT-5.1 和其他推理模型
|
||||||
|
- 某些专用模型
|
||||||
|
|
||||||
|
未设置时,模型将使用其默认行为。
|
||||||
|
|
||||||
|
## 推荐
|
||||||
|
|
||||||
|
- **最佳体验**:使用支持视觉的模型(GPT-4o, Claude, Gemini)以获得图像转图表功能
|
||||||
|
- **经济实惠**:DeepSeek 提供具有竞争力的价格
|
||||||
|
- **隐私保护**:使用 Ollama 进行完全本地、离线的操作(需要强大的硬件支持)
|
||||||
|
- **灵活性**:OpenRouter 通过单一 API 提供对众多模型的访问
|
||||||
267
docs/cn/cloudflare-deploy.md
Normal file
267
docs/cn/cloudflare-deploy.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# 部署到 Cloudflare Workers
|
||||||
|
|
||||||
|
本项目可以通过 **OpenNext 适配器** 部署为 **Cloudflare Worker**,为您提供:
|
||||||
|
|
||||||
|
- 全球边缘部署
|
||||||
|
- 极低延迟
|
||||||
|
- 免费的 `workers.dev` 域名托管
|
||||||
|
- 通过 R2 实现完整的 Next.js ISR 支持(可选)
|
||||||
|
|
||||||
|
> **Windows 用户重要提示:** OpenNext 和 Wrangler 在 **原生 Windows 环境下并不完全可靠**。建议方案:
|
||||||
|
>
|
||||||
|
> - 使用 **GitHub Codespaces**(完美运行)
|
||||||
|
> - 或者使用 **WSL (Linux)**
|
||||||
|
>
|
||||||
|
> 纯 Windows 构建可能会因为 WASM 文件路径问题而失败。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
1. 一个 **Cloudflare 账户**(免费版即可满足基本部署需求)
|
||||||
|
2. **Node.js 18+**
|
||||||
|
3. 安装 **Wrangler CLI**(作为开发依赖安装即可):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 登录 Cloudflare:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler login
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意:** 只有在启用 R2 进行 ISR 缓存时才需要绑定支付方式。基本的 Workers 部署是免费的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一步 — 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第二步 — 配置环境变量
|
||||||
|
|
||||||
|
Cloudflare 在本地测试时使用不同的文件。
|
||||||
|
|
||||||
|
### 1) 创建 `.dev.vars`(用于 Cloudflare 本地调试 + 部署)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .dev.vars
|
||||||
|
```
|
||||||
|
|
||||||
|
填入您的 API 密钥和配置信息。
|
||||||
|
|
||||||
|
### 2) 确保 `.env.local` 也存在(用于常规 Next.js 开发)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
在此处填入相同的值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第三步 — 选择部署类型
|
||||||
|
|
||||||
|
### 选项 A:不使用 R2 部署(简单,免费)
|
||||||
|
|
||||||
|
如果您不需要 ISR 缓存,可以选择不使用 R2 进行部署:
|
||||||
|
|
||||||
|
**1. 使用简单的 `open-next.config.ts`:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 使用简单的 `wrangler.jsonc`(不包含 r2_buckets):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
直接跳至 **第四步**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 选项 B:使用 R2 部署(完整的 ISR 支持)
|
||||||
|
|
||||||
|
R2 开启了 **增量静态再生 (ISR)** 缓存功能。需要在您的 Cloudflare 账户中绑定支付方式。
|
||||||
|
|
||||||
|
**1. 在 Cloudflare 控制台中创建 R2 存储桶:**
|
||||||
|
|
||||||
|
- 进入 **Storage & Databases → R2**
|
||||||
|
- 点击 **Create bucket**
|
||||||
|
- 命名为:`next-inc-cache`
|
||||||
|
|
||||||
|
**2. 配置 `open-next.config.ts`:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
incrementalCache: r2IncrementalCache,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 配置 `wrangler.jsonc`(包含 R2):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "next-inc-cache"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重要提示:** `bucket_name` 必须与您在 Cloudflare 控制台中创建的名称完全一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第四步 — 注册 workers.dev 子域名(仅首次需要)
|
||||||
|
|
||||||
|
在首次部署之前,您需要一个 workers.dev 子域名。
|
||||||
|
|
||||||
|
**选项 1:通过 Cloudflare 控制台(推荐)**
|
||||||
|
|
||||||
|
访问:https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
|
||||||
|
|
||||||
|
**选项 2:在部署过程中**
|
||||||
|
|
||||||
|
运行 `npm run deploy` 时,Wrangler 可能会提示:
|
||||||
|
|
||||||
|
```
|
||||||
|
Would you like to register a workers.dev subdomain? (Y/n)
|
||||||
|
```
|
||||||
|
|
||||||
|
输入 `Y` 并选择一个子域名。
|
||||||
|
|
||||||
|
> **注意:** 在 CI/CD 或非交互式环境中,该提示不会出现。请先通过控制台进行注册。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第五步 — 部署到 Cloudflare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
该脚本执行的操作:
|
||||||
|
|
||||||
|
- 构建 Next.js 应用
|
||||||
|
- 通过 OpenNext 将其转换为 Cloudflare Worker
|
||||||
|
- 上传静态资源
|
||||||
|
- 发布 Worker
|
||||||
|
|
||||||
|
您的应用将可通过以下地址访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<worker-name>.<your-subdomain>.workers.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题与修复
|
||||||
|
|
||||||
|
### `You need to register a workers.dev subdomain`
|
||||||
|
|
||||||
|
**原因:** 您的账户尚未注册 workers.dev 子域名。
|
||||||
|
|
||||||
|
**修复:** 前往 https://dash.cloudflare.com → Workers & Pages → Set up a subdomain。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Please enable R2 through the Cloudflare Dashboard`
|
||||||
|
|
||||||
|
**原因:** wrangler.jsonc 中配置了 R2,但您的账户尚未启用该功能。
|
||||||
|
|
||||||
|
**修复:** 启用 R2(需要支付方式)或使用选项 A(不使用 R2 部署)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
|
||||||
|
|
||||||
|
**原因:** `wrangler.jsonc` 中缺少 `r2_buckets` 配置。
|
||||||
|
|
||||||
|
**修复:** 添加 `r2_buckets` 部分或切换到选项 A(不使用 R2)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Can't set compatibility date in the future`
|
||||||
|
|
||||||
|
**原因:** wrangler 配置中的 `compatibility_date` 设置为了未来的日期。
|
||||||
|
|
||||||
|
**修复:** 将 `compatibility_date` 修改为今天或更早的日期。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Windows 错误:`resvg.wasm?module` (ENOENT)
|
||||||
|
|
||||||
|
**原因:** Windows 文件名不能包含 `?`,但某个 wasm 资源文件名中使用了 `?module`。
|
||||||
|
|
||||||
|
**修复:** 在 Linux 环境(WSL、Codespaces 或 CI)上进行构建/部署。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 可选:本地预览
|
||||||
|
|
||||||
|
部署前在本地预览 Worker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
| 功能 | 不使用 R2 | 使用 R2 |
|
||||||
|
|---------|------------|---------|
|
||||||
|
| 成本 | 免费 | 需要绑定支付方式 |
|
||||||
|
| ISR 缓存 | 无 | 有 |
|
||||||
|
| 静态页面 | 支持 | 支持 |
|
||||||
|
| API 路由 | 支持 | 支持 |
|
||||||
|
| 配置复杂度 | 简单 | 中等 |
|
||||||
|
|
||||||
|
测试或简单应用请选择 **不使用 R2**。需要 ISR 缓存的生产环境应用请选择 **使用 R2**。
|
||||||
29
docs/cn/docker.md
Normal file
29
docs/cn/docker.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 使用 Docker 运行
|
||||||
|
|
||||||
|
如果您只是想在本地运行,最好的方式是使用 Docker。
|
||||||
|
|
||||||
|
首先,如果您尚未安装 Docker,请先安装:[获取 Docker](https://docs.docker.com/get-docker/)
|
||||||
|
|
||||||
|
然后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e AI_PROVIDER=openai \
|
||||||
|
-e AI_MODEL=gpt-4o \
|
||||||
|
-e OPENAI_API_KEY=your_api_key \
|
||||||
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用环境变量文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
# 编辑 .env 文件并填入您的配置
|
||||||
|
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
|
||||||
|
|
||||||
|
请将环境变量替换为您首选的 AI 提供商配置。查看 [AI 提供商](./ai-providers.md) 了解可用选项。
|
||||||
|
|
||||||
|
> **离线部署:** 如果无法访问 `embed.diagrams.net`,请参阅 [离线部署](./offline-deployment.md) 了解配置选项。
|
||||||
38
docs/cn/offline-deployment.md
Normal file
38
docs/cn/offline-deployment.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 离线部署
|
||||||
|
|
||||||
|
通过自托管 draw.io 来替代 `embed.diagrams.net`,从而离线部署 Next AI Draw.io。
|
||||||
|
|
||||||
|
**注意:** `NEXT_PUBLIC_DRAWIO_BASE_URL` 是一个**构建时**变量。修改它需要重新构建 Docker 镜像。
|
||||||
|
|
||||||
|
## Docker Compose 设置
|
||||||
|
|
||||||
|
1. 克隆仓库并在 `.env` 文件中定义 API 密钥。
|
||||||
|
2. 创建 `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
drawio:
|
||||||
|
image: jgraph/drawio:latest
|
||||||
|
ports: ["8080:8080"]
|
||||||
|
next-ai-draw-io:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
env_file: .env
|
||||||
|
depends_on: [drawio]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 运行 `docker compose up -d` 并打开 `http://localhost:3000`。
|
||||||
|
|
||||||
|
## 配置与重要警告
|
||||||
|
|
||||||
|
**`NEXT_PUBLIC_DRAWIO_BASE_URL` 必须是用户浏览器可访问的地址。**
|
||||||
|
|
||||||
|
| 场景 | URL 值 |
|
||||||
|
|----------|-----------|
|
||||||
|
| 本地主机 (Localhost) | `http://localhost:8080` |
|
||||||
|
| 远程/服务器 | `http://YOUR_SERVER_IP:8080` |
|
||||||
|
|
||||||
|
**切勿使用** Docker 内部别名(如 `http://drawio:8080`),因为浏览器无法解析它们。
|
||||||
236
docs/en/ai-providers.md
Normal file
236
docs/en/ai-providers.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# AI Provider Configuration
|
||||||
|
|
||||||
|
This guide explains how to configure different AI model providers for next-ai-draw-io.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Copy `.env.example` to `.env.local`
|
||||||
|
2. Set your API key for your chosen provider
|
||||||
|
3. Set `AI_MODEL` to your desired model
|
||||||
|
4. Run `npm run dev`
|
||||||
|
|
||||||
|
## Supported Providers
|
||||||
|
|
||||||
|
### Doubao (ByteDance Volcengine)
|
||||||
|
|
||||||
|
> **Free tokens**: Register on the [Volcengine ARK platform](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) to get 500K free tokens for all models!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOUBAO_API_KEY=your_api_key
|
||||||
|
AI_MODEL=doubao-seed-1-8-251215 # or other Doubao model
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google Gemini
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
|
||||||
|
AI_MODEL=gemini-2.0-flash
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENAI_API_KEY=your_api_key
|
||||||
|
AI_MODEL=gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint (for OpenAI-compatible services):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anthropic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_API_KEY=your_api_key
|
||||||
|
AI_MODEL=claude-sonnet-4-5-20250514
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeepSeek
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEEPSEEK_API_KEY=your_api_key
|
||||||
|
AI_MODEL=deepseek-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEEPSEEK_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### SiliconFlow (OpenAI-compatible)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SILICONFLOW_API_KEY=your_api_key
|
||||||
|
AI_MODEL=deepseek-ai/DeepSeek-V3 # example; use any SiliconFlow model id
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint (defaults to the recommended domain):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflow.cn/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### SGLang
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SGLANG_API_KEY=your_api_key
|
||||||
|
AI_MODEL=your_model_id
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SGLANG_BASE_URL=https://your-custom-endpoint/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Azure OpenAI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_RESOURCE_NAME=your-resource-name # Required: your Azure resource name
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a custom endpoint instead of resource name:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_BASE_URL=https://your-resource.openai.azure.com # Alternative to AZURE_RESOURCE_NAME
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional reasoning configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_REASONING_EFFORT=low # Optional: low, medium, high
|
||||||
|
AZURE_REASONING_SUMMARY=detailed # Optional: none, brief, detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS Bedrock
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AWS_REGION=us-west-2
|
||||||
|
AWS_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_secret_access_key
|
||||||
|
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: On AWS (Lambda, EC2 with IAM role), credentials are automatically obtained from the IAM role.
|
||||||
|
|
||||||
|
### OpenRouter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENROUTER_API_KEY=your_api_key
|
||||||
|
AI_MODEL=anthropic/claude-sonnet-4
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENROUTER_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ollama (Local)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=ollama
|
||||||
|
AI_MODEL=llama3.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional custom URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vercel AI Gateway
|
||||||
|
|
||||||
|
Vercel AI Gateway provides unified access to multiple AI providers through a single API key. This simplifies authentication and allows you to switch between providers without managing multiple API keys.
|
||||||
|
|
||||||
|
**Basic Usage (Vercel-hosted Gateway):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_gateway_api_key
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom Gateway URL (for local development or self-hosted Gateway):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_custom_api_key
|
||||||
|
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
Model format uses `provider/model` syntax:
|
||||||
|
|
||||||
|
- `openai/gpt-4o` - OpenAI GPT-4o
|
||||||
|
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
|
||||||
|
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
|
||||||
|
|
||||||
|
**Configuration notes:**
|
||||||
|
|
||||||
|
- If `AI_GATEWAY_BASE_URL` is not set, the default Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`) is used
|
||||||
|
- Custom base URL is useful for:
|
||||||
|
- Local development with a custom Gateway instance
|
||||||
|
- Self-hosted AI Gateway deployments
|
||||||
|
- Enterprise proxy configurations
|
||||||
|
- When using a custom base URL, you must also provide `AI_GATEWAY_API_KEY`
|
||||||
|
|
||||||
|
Get your API key from the [Vercel AI Gateway dashboard](https://vercel.com/ai-gateway).
|
||||||
|
|
||||||
|
## Auto-Detection
|
||||||
|
|
||||||
|
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
|
||||||
|
|
||||||
|
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Capability Requirements
|
||||||
|
|
||||||
|
This task requires exceptionally strong model capabilities, as it involves generating long-form text with strict formatting constraints (draw.io XML).
|
||||||
|
|
||||||
|
**Recommended models**:
|
||||||
|
|
||||||
|
- Claude Sonnet 4.5 / Opus 4.5
|
||||||
|
|
||||||
|
**Note on Ollama**: While Ollama is supported as a provider, it's generally not practical for this use case unless you're running high-capability models like DeepSeek R1 or Qwen3-235B locally.
|
||||||
|
|
||||||
|
## Temperature Setting
|
||||||
|
|
||||||
|
You can optionally configure the temperature via environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TEMPERATURE=0 # More deterministic output (recommended for diagrams)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Leave `TEMPERATURE` unset for models that don't support temperature settings, such as:
|
||||||
|
- GPT-5.1 and other reasoning models
|
||||||
|
- Some specialized models
|
||||||
|
|
||||||
|
When unset, the model uses its default behavior.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
- **Best experience**: Use models with vision support (GPT-4o, Claude, Gemini) for image-to-diagram features
|
||||||
|
- **Budget-friendly**: DeepSeek offers competitive pricing
|
||||||
|
- **Privacy**: Use Ollama for fully local, offline operation (requires powerful hardware)
|
||||||
|
- **Flexibility**: OpenRouter provides access to many models through a single API
|
||||||
267
docs/en/cloudflare-deploy.md
Normal file
267
docs/en/cloudflare-deploy.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Deploy on Cloudflare Workers
|
||||||
|
|
||||||
|
This project can be deployed as a **Cloudflare Worker** using the **OpenNext adapter**, giving you:
|
||||||
|
|
||||||
|
- Global edge deployment
|
||||||
|
- Very low latency
|
||||||
|
- Free `workers.dev` hosting
|
||||||
|
- Full Next.js ISR support via R2 (optional)
|
||||||
|
|
||||||
|
> **Important Windows Note:** OpenNext and Wrangler are **not fully reliable on native Windows**. Recommended options:
|
||||||
|
>
|
||||||
|
> - Use **GitHub Codespaces** (works perfectly)
|
||||||
|
> - OR use **WSL (Linux)**
|
||||||
|
>
|
||||||
|
> Pure Windows builds may fail due to WASM file path issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A **Cloudflare account** (free tier works for basic deployment)
|
||||||
|
2. **Node.js 18+**
|
||||||
|
3. **Wrangler CLI** installed (dev dependency is fine):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Cloudflare login:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler login
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** A payment method is only required if you want to enable R2 for ISR caching. Basic Workers deployment is free.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Configure environment variables
|
||||||
|
|
||||||
|
Cloudflare uses a different file for local testing.
|
||||||
|
|
||||||
|
### 1) Create `.dev.vars` (for Cloudflare local + deploy)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .dev.vars
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in your API keys and configuration.
|
||||||
|
|
||||||
|
### 2) Make sure `.env.local` also exists (for regular Next.js dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in the same values there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Choose your deployment type
|
||||||
|
|
||||||
|
### Option A: Deploy WITHOUT R2 (Simple, Free)
|
||||||
|
|
||||||
|
If you don't need ISR caching, you can deploy without R2:
|
||||||
|
|
||||||
|
**1. Use simple `open-next.config.ts`:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Use simple `wrangler.jsonc` (without r2_buckets):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip to **Step 4**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option B: Deploy WITH R2 (Full ISR Support)
|
||||||
|
|
||||||
|
R2 enables **Incremental Static Regeneration (ISR)** caching. Requires a payment method on your Cloudflare account.
|
||||||
|
|
||||||
|
**1. Create an R2 bucket** in the Cloudflare Dashboard:
|
||||||
|
|
||||||
|
- Go to **Storage & Databases → R2**
|
||||||
|
- Click **Create bucket**
|
||||||
|
- Name it: `next-inc-cache`
|
||||||
|
|
||||||
|
**2. Configure `open-next.config.ts`:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
incrementalCache: r2IncrementalCache,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Configure `wrangler.jsonc` (with R2):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "next-inc-cache"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** The `bucket_name` must exactly match the name you created in the Cloudflare dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Register a workers.dev subdomain (first-time only)
|
||||||
|
|
||||||
|
Before your first deployment, you need a workers.dev subdomain.
|
||||||
|
|
||||||
|
**Option 1: Via Cloudflare Dashboard (Recommended)**
|
||||||
|
|
||||||
|
Visit: https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
|
||||||
|
|
||||||
|
**Option 2: During deploy**
|
||||||
|
|
||||||
|
When you run `npm run deploy`, Wrangler may prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
Would you like to register a workers.dev subdomain? (Y/n)
|
||||||
|
```
|
||||||
|
|
||||||
|
Type `Y` and choose a subdomain name.
|
||||||
|
|
||||||
|
> **Note:** In CI/CD or non-interactive environments, the prompt won't appear. Register via the dashboard first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Deploy to Cloudflare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
What the script does:
|
||||||
|
|
||||||
|
- Builds the Next.js app
|
||||||
|
- Converts it to a Cloudflare Worker via OpenNext
|
||||||
|
- Uploads static assets
|
||||||
|
- Publishes the Worker
|
||||||
|
|
||||||
|
Your app will be available at:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<worker-name>.<your-subdomain>.workers.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common issues & fixes
|
||||||
|
|
||||||
|
### `You need to register a workers.dev subdomain`
|
||||||
|
|
||||||
|
**Cause:** No workers.dev subdomain registered for your account.
|
||||||
|
|
||||||
|
**Fix:** Go to https://dash.cloudflare.com → Workers & Pages → Set up a subdomain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Please enable R2 through the Cloudflare Dashboard`
|
||||||
|
|
||||||
|
**Cause:** R2 is configured in wrangler.jsonc but not enabled on your account.
|
||||||
|
|
||||||
|
**Fix:** Either enable R2 (requires payment method) or use Option A (deploy without R2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
|
||||||
|
|
||||||
|
**Cause:** `r2_buckets` is missing from `wrangler.jsonc`.
|
||||||
|
|
||||||
|
**Fix:** Add the `r2_buckets` section or switch to Option A (without R2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Can't set compatibility date in the future`
|
||||||
|
|
||||||
|
**Cause:** `compatibility_date` in wrangler config is set to a future date.
|
||||||
|
|
||||||
|
**Fix:** Change `compatibility_date` to today or an earlier date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Windows error: `resvg.wasm?module` (ENOENT)
|
||||||
|
|
||||||
|
**Cause:** Windows filenames cannot include `?`, but a wasm asset uses `?module` in its filename.
|
||||||
|
|
||||||
|
**Fix:** Build/deploy on Linux (WSL, Codespaces, or CI).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional: Preview locally
|
||||||
|
|
||||||
|
Preview the Worker locally before deploying:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Feature | Without R2 | With R2 |
|
||||||
|
|---------|------------|---------|
|
||||||
|
| Cost | Free | Requires payment method |
|
||||||
|
| ISR Caching | No | Yes |
|
||||||
|
| Static Pages | Yes | Yes |
|
||||||
|
| API Routes | Yes | Yes |
|
||||||
|
| Setup Complexity | Simple | Moderate |
|
||||||
|
|
||||||
|
Choose **without R2** for testing or simple apps. Choose **with R2** for production apps that need ISR caching.
|
||||||
29
docs/en/docker.md
Normal file
29
docs/en/docker.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Run with Docker
|
||||||
|
|
||||||
|
If you just want to run it locally, the best way is to use Docker.
|
||||||
|
|
||||||
|
First, install Docker if you haven't already: [Get Docker](https://docs.docker.com/get-docker/)
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e AI_PROVIDER=openai \
|
||||||
|
-e AI_MODEL=gpt-4o \
|
||||||
|
-e OPENAI_API_KEY=your_api_key \
|
||||||
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use an env file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||||
|
|
||||||
|
Replace the environment variables with your preferred AI provider configuration. See [AI Providers](./ai-providers.md) for available options.
|
||||||
|
|
||||||
|
> **Offline Deployment:** If `embed.diagrams.net` is blocked, see [Offline Deployment](./offline-deployment.md) for configuration options.
|
||||||
39
docs/en/offline-deployment.md
Normal file
39
docs/en/offline-deployment.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Offline Deployment
|
||||||
|
|
||||||
|
Deploy Next AI Draw.io offline by self-hosting draw.io to replace `embed.diagrams.net`.
|
||||||
|
|
||||||
|
**Note:** `NEXT_PUBLIC_DRAWIO_BASE_URL` is a **build-time** variable. Changing it requires rebuilding the Docker image.
|
||||||
|
|
||||||
|
## Docker Compose Setup
|
||||||
|
|
||||||
|
1. Clone the repository and define API keys in `.env`.
|
||||||
|
2. Create `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
drawio:
|
||||||
|
image: jgraph/drawio:latest
|
||||||
|
ports: ["8080:8080"]
|
||||||
|
next-ai-draw-io:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
env_file: .env
|
||||||
|
depends_on: [drawio]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run `docker compose up -d` and open `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Configuration & Critical Warning
|
||||||
|
|
||||||
|
**The `NEXT_PUBLIC_DRAWIO_BASE_URL` must be accessible from the user's browser.**
|
||||||
|
|
||||||
|
| Scenario | URL Value |
|
||||||
|
|----------|-----------|
|
||||||
|
| Localhost | `http://localhost:8080` |
|
||||||
|
| Remote/Server | `http://YOUR_SERVER_IP:8080` |
|
||||||
|
|
||||||
|
**Do NOT use** internal Docker aliases like `http://drawio:8080`; the browser cannot resolve them.
|
||||||
|
|
||||||
245
docs/ja/README_JA.md
Normal file
245
docs/ja/README_JA.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
# Next AI Draw.io
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**
|
||||||
|
|
||||||
|
[English](../../README.md) | [中文](../cn/README_CN.md) | 日本語
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://react.dev/)
|
||||||
|
[](https://github.com/sponsors/DayuanJiang)
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
|
||||||
|
|
||||||
|
> 注:<img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) のご支援により、デモサイトに強力な K2-thinking モデルを導入しました!
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
|
||||||
|
|
||||||
|
## 目次
|
||||||
|
- [Next AI Draw.io](#next-ai-drawio)
|
||||||
|
- [目次](#目次)
|
||||||
|
- [例](#例)
|
||||||
|
- [機能](#機能)
|
||||||
|
- [MCPサーバー(プレビュー)](#mcpサーバープレビュー)
|
||||||
|
- [Claude Code CLI](#claude-code-cli)
|
||||||
|
- [はじめに](#はじめに)
|
||||||
|
- [オンラインで試す](#オンラインで試す)
|
||||||
|
- [デスクトップアプリケーション](#デスクトップアプリケーション)
|
||||||
|
- [Dockerで実行](#dockerで実行)
|
||||||
|
- [インストール](#インストール)
|
||||||
|
- [デプロイ](#デプロイ)
|
||||||
|
- [EdgeOne Pagesへのデプロイ](#edgeone-pagesへのデプロイ)
|
||||||
|
- [Vercelへのデプロイ(推奨)](#vercelへのデプロイ推奨)
|
||||||
|
- [Cloudflare Workersへのデプロイ](#cloudflare-workersへのデプロイ)
|
||||||
|
- [マルチプロバイダーサポート](#マルチプロバイダーサポート)
|
||||||
|
- [仕組み](#仕組み)
|
||||||
|
- [サポート&お問い合わせ](#サポートお問い合わせ)
|
||||||
|
- [スター履歴](#スター履歴)
|
||||||
|
|
||||||
|
## 例
|
||||||
|
|
||||||
|
以下はいくつかのプロンプト例と生成されたダイアグラムです:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top" align="center">
|
||||||
|
<strong>アニメーションTransformerコネクタ</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
|
||||||
|
<img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>GCPアーキテクチャ図</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
|
<img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>AWSアーキテクチャ図</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
|
<img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>Azureアーキテクチャ図</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
|
||||||
|
<img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
<strong>猫のスケッチ</strong><br />
|
||||||
|
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
|
||||||
|
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
|
||||||
|
- **LLM搭載のダイアグラム作成**:大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
|
||||||
|
- **画像ベースのダイアグラム複製**:既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
|
||||||
|
- **PDFとテキストファイルのアップロード**:PDFドキュメントやテキストファイルをアップロードして、既存のドキュメントからコンテンツを抽出し、ダイアグラムを生成
|
||||||
|
- **AI推論プロセス表示**:サポートされているモデル(OpenAI o1/o3、Gemini、Claudeなど)のAIの思考プロセスを表示
|
||||||
|
- **ダイアグラム履歴**:すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
|
||||||
|
- **インタラクティブなチャットインターフェース**:AIとリアルタイムでコミュニケーションしてダイアグラムを改善
|
||||||
|
- **クラウドアーキテクチャダイアグラムサポート**:クラウドアーキテクチャダイアグラムの生成を専門的にサポート(AWS、GCP、Azure)
|
||||||
|
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
|
||||||
|
|
||||||
|
## MCPサーバー(プレビュー)
|
||||||
|
|
||||||
|
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
|
||||||
|
|
||||||
|
MCP(Model Context Protocol)を介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"drawio": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["@next-ai-drawio/mcp-server@latest"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Claudeにダイアグラムの作成を依頼:
|
||||||
|
> 「ログイン、MFA、セッション管理を含むユーザー認証のフローチャートを作成してください」
|
||||||
|
|
||||||
|
ダイアグラムがリアルタイムでブラウザに表示されます!
|
||||||
|
|
||||||
|
詳細は[MCPサーバーREADME](../../packages/mcp-server/README.md)をご覧ください(VS Code、Cursorなどのクライアント設定も含む)。
|
||||||
|
|
||||||
|
## はじめに
|
||||||
|
|
||||||
|
### オンラインで試す
|
||||||
|
|
||||||
|
インストール不要!デモサイトで直接お試しください:
|
||||||
|
|
||||||
|
[](https://next-ai-drawio.jiang.jp/)
|
||||||
|
|
||||||
|
> **自分のAPIキーを使用**:自分のAPIキーを使用することで、デモサイトの利用制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。キーはブラウザのローカルに保存され、サーバーには保存されません。
|
||||||
|
|
||||||
|
### デスクトップアプリケーション
|
||||||
|
|
||||||
|
[Releases ページ](https://github.com/DayuanJiang/next-ai-draw-io/releases)からお使いのプラットフォーム用のネイティブデスクトップアプリをダウンロードしてください:
|
||||||
|
|
||||||
|
対応プラットフォーム:Windows、macOS、Linux。
|
||||||
|
|
||||||
|
### Dockerで実行
|
||||||
|
|
||||||
|
[Docker ガイドを参照](./docker.md)
|
||||||
|
|
||||||
|
### インストール
|
||||||
|
|
||||||
|
1. リポジトリをクローン:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DayuanJiang/next-ai-draw-io
|
||||||
|
cd next-ai-draw-io
|
||||||
|
npm install
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
詳細な設定手順については[プロバイダー設定ガイド](./ai-providers.md)を参照してください。
|
||||||
|
|
||||||
|
2. 開発サーバーを起動:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. ブラウザで[http://localhost:6002](http://localhost:6002)を開いてアプリケーションを確認。
|
||||||
|
|
||||||
|
## デプロイ
|
||||||
|
|
||||||
|
### EdgeOne Pagesへのデプロイ
|
||||||
|
|
||||||
|
[Tencent EdgeOne Pages](https://pages.edgeone.ai/)を使用してワンクリックでデプロイできます。
|
||||||
|
|
||||||
|
このボタンでデプロイ:
|
||||||
|
|
||||||
|
[](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
詳細は[Tencent EdgeOne Pagesドキュメント](https://pages.edgeone.ai/document/deployment-overview)をご覧ください。
|
||||||
|
|
||||||
|
また、Tencent EdgeOne Pagesでデプロイすると、[DeepSeekモデルの毎日の無料クォータ](https://pages.edgeone.ai/document/edge-ai)が付与されます。
|
||||||
|
|
||||||
|
### Vercelへのデプロイ(推奨)
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
|
||||||
|
|
||||||
|
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。ローカルの`.env.local`ファイルと同様に、Vercelダッシュボードで**環境変数を設定**してください。
|
||||||
|
|
||||||
|
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
|
||||||
|
|
||||||
|
### Cloudflare Workersへのデプロイ
|
||||||
|
|
||||||
|
[Cloudflare デプロイガイドを参照](./cloudflare-deploy.md)
|
||||||
|
|
||||||
|
|
||||||
|
## マルチプロバイダーサポート
|
||||||
|
|
||||||
|
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
|
||||||
|
- AWS Bedrock(デフォルト)
|
||||||
|
- OpenAI
|
||||||
|
- Anthropic
|
||||||
|
- Google AI
|
||||||
|
- Azure OpenAI
|
||||||
|
- Ollama
|
||||||
|
- OpenRouter
|
||||||
|
- DeepSeek
|
||||||
|
- SiliconFlow
|
||||||
|
- SGLang
|
||||||
|
- Vercel AI Gateway
|
||||||
|
|
||||||
|
AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタムエンドポイントをサポートしています。
|
||||||
|
|
||||||
|
📖 **[詳細なプロバイダー設定ガイド](./ai-providers.md)** - 各プロバイダーの設定手順をご覧ください。
|
||||||
|
|
||||||
|
**モデル要件**:このタスクは厳密なフォーマット制約(draw.io XML)を持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro、DeepSeek V3.2/R1を推奨します。
|
||||||
|
|
||||||
|
注:`claude`シリーズはAWS、Azure、GCPなどのクラウドアーキテクチャロゴ付きのdraw.ioダイアグラムで学習されているため、クラウドアーキテクチャダイアグラムを作成したい場合は最適な選択です。
|
||||||
|
|
||||||
|
|
||||||
|
## 仕組み
|
||||||
|
|
||||||
|
本アプリケーションは以下の技術を使用しています:
|
||||||
|
|
||||||
|
- **Next.js**:フロントエンドフレームワークとルーティング
|
||||||
|
- **Vercel AI SDK**(`ai` + `@ai-sdk/*`):ストリーミングAIレスポンスとマルチプロバイダーサポート
|
||||||
|
- **react-drawio**:ダイアグラムの表現と操作
|
||||||
|
|
||||||
|
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
|
||||||
|
|
||||||
|
|
||||||
|
## サポート&お問い合わせ
|
||||||
|
|
||||||
|
**デモサイトのAPIトークン使用を支援してくださった[ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に特別な感謝を申し上げます!** ARKプラットフォームに登録すると、50万トークンが無料でもらえます!
|
||||||
|
|
||||||
|
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
|
||||||
|
|
||||||
|
サポートやお問い合わせについては、GitHubリポジトリでissueを開くか、メンテナーにご連絡ください:
|
||||||
|
|
||||||
|
- メール:me[at]jiang.jp
|
||||||
|
|
||||||
|
## スター履歴
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
|
||||||
|
|
||||||
|
---
|
||||||
236
docs/ja/ai-providers.md
Normal file
236
docs/ja/ai-providers.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# AIプロバイダーの設定
|
||||||
|
|
||||||
|
このガイドでは、next-ai-draw-io でさまざまな AI モデルプロバイダーを設定する方法について説明します。
|
||||||
|
|
||||||
|
## クイックスタート
|
||||||
|
|
||||||
|
1. `.env.example` を `.env.local` にコピーします
|
||||||
|
2. 選択したプロバイダーの API キーを設定します
|
||||||
|
3. `AI_MODEL` を希望のモデルに設定します
|
||||||
|
4. `npm run dev` を実行します
|
||||||
|
|
||||||
|
## 対応プロバイダー
|
||||||
|
|
||||||
|
### Doubao (ByteDance Volcengine)
|
||||||
|
|
||||||
|
> **無料トークン**: [Volcengine ARK プラットフォーム](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に登録すると、すべてのモデルで使える50万トークンが無料で入手できます!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOUBAO_API_KEY=your_api_key
|
||||||
|
AI_MODEL=doubao-seed-1-8-251215 # または他の Doubao モデル
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google Gemini
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
|
||||||
|
AI_MODEL=gemini-2.0-flash
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOGLE_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENAI_API_KEY=your_api_key
|
||||||
|
AI_MODEL=gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント(OpenAI 互換サービス用):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anthropic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_API_KEY=your_api_key
|
||||||
|
AI_MODEL=claude-sonnet-4-5-20250514
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeepSeek
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEEPSEEK_API_KEY=your_api_key
|
||||||
|
AI_MODEL=deepseek-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEEPSEEK_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### SiliconFlow (OpenAI 互換)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SILICONFLOW_API_KEY=your_api_key
|
||||||
|
AI_MODEL=deepseek-ai/DeepSeek-V3 # 例; 任意の SiliconFlow モデル ID を使用
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント(デフォルトは推奨ドメイン):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # または https://api.siliconflow.cn/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### SGLang
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SGLANG_API_KEY=your_api_key
|
||||||
|
AI_MODEL=your_model_id
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SGLANG_BASE_URL=https://your-custom-endpoint/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Azure OpenAI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_RESOURCE_NAME=your-resource-name # 必須: Azure リソース名
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
またはリソース名の代わりにカスタムエンドポイントを使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_API_KEY=your_api_key
|
||||||
|
AZURE_BASE_URL=https://your-resource.openai.azure.com # AZURE_RESOURCE_NAME の代替
|
||||||
|
AI_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
任意の推論設定:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AZURE_REASONING_EFFORT=low # 任意: low, medium, high
|
||||||
|
AZURE_REASONING_SUMMARY=detailed # 任意: none, brief, detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS Bedrock
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AWS_REGION=us-west-2
|
||||||
|
AWS_ACCESS_KEY_ID=your_access_key_id
|
||||||
|
AWS_SECRET_ACCESS_KEY=your_secret_access_key
|
||||||
|
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
|
||||||
|
```
|
||||||
|
|
||||||
|
注: AWS 上(IAM ロールを持つ Lambda や EC2)では、認証情報は IAM ロールから自動的に取得されます。
|
||||||
|
|
||||||
|
### OpenRouter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENROUTER_API_KEY=your_api_key
|
||||||
|
AI_MODEL=anthropic/claude-sonnet-4
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタムエンドポイント:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENROUTER_BASE_URL=https://your-custom-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ollama (ローカル)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=ollama
|
||||||
|
AI_MODEL=llama3.2
|
||||||
|
```
|
||||||
|
|
||||||
|
任意のカスタム URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vercel AI Gateway
|
||||||
|
|
||||||
|
Vercel AI Gateway は、単一の API キーで複数の AI プロバイダーへの統合アクセスを提供します。これにより認証が簡素化され、複数の API キーを管理することなくプロバイダーを切り替えることができます。
|
||||||
|
|
||||||
|
**基本的な使用法 (Vercel ホストの Gateway):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_gateway_api_key
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
**カスタム Gateway URL (ローカル開発またはセルフホスト Gateway 用):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_GATEWAY_API_KEY=your_custom_api_key
|
||||||
|
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
|
||||||
|
AI_MODEL=openai/gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
モデル形式は `provider/model` 構文を使用します:
|
||||||
|
|
||||||
|
- `openai/gpt-4o` - OpenAI GPT-4o
|
||||||
|
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
|
||||||
|
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
|
||||||
|
|
||||||
|
**設定に関する注意点:**
|
||||||
|
|
||||||
|
- `AI_GATEWAY_BASE_URL` が設定されていない場合、デフォルトの Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`) が使用されます
|
||||||
|
- カスタムベース URL は以下の場合に便利です:
|
||||||
|
- カスタム Gateway インスタンスを使用したローカル開発
|
||||||
|
- セルフホスト AI Gateway デプロイメント
|
||||||
|
- エンタープライズプロキシ設定
|
||||||
|
- カスタムベース URL を使用する場合、`AI_GATEWAY_API_KEY` も指定する必要があります
|
||||||
|
|
||||||
|
[Vercel AI Gateway ダッシュボード](https://vercel.com/ai-gateway)から API キーを取得してください。
|
||||||
|
|
||||||
|
## 自動検出
|
||||||
|
|
||||||
|
**1つ**のプロバイダーの API キーのみを設定した場合、システムはそのプロバイダーを自動的に検出して使用します。`AI_PROVIDER` を設定する必要はありません。
|
||||||
|
|
||||||
|
**複数**の API キーを設定する場合は、`AI_PROVIDER` を明示的に設定する必要があります:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=google # または: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
|
||||||
|
```
|
||||||
|
|
||||||
|
## モデル性能要件
|
||||||
|
|
||||||
|
このタスクは、厳密なフォーマット制約(draw.io XML)を伴う長文テキストの生成を含むため、非常に強力なモデル性能が必要です。
|
||||||
|
|
||||||
|
**推奨モデル**:
|
||||||
|
|
||||||
|
- Claude Sonnet 4.5 / Opus 4.5
|
||||||
|
|
||||||
|
**Ollama に関する注意**: Ollama はプロバイダーとしてサポートされていますが、DeepSeek R1 や Qwen3-235B のような高性能モデルをローカルで実行していない限り、このユースケースでは一般的に実用的ではありません。
|
||||||
|
|
||||||
|
## Temperature(温度)設定
|
||||||
|
|
||||||
|
環境変数で Temperature を任意に設定できます:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TEMPERATURE=0 # より決定論的な出力(ダイアグラムに推奨)
|
||||||
|
```
|
||||||
|
|
||||||
|
**重要**: 以下の Temperature 設定をサポートしていないモデルでは、`TEMPERATURE` を未設定のままにしてください:
|
||||||
|
- GPT-5.1 およびその他の推論モデル
|
||||||
|
- 一部の特殊なモデル
|
||||||
|
|
||||||
|
未設定の場合、モデルはデフォルトの挙動を使用します。
|
||||||
|
|
||||||
|
## 推奨事項
|
||||||
|
|
||||||
|
- **最高の体験**: 画像からダイアグラムを生成する機能には、ビジョン(画像認識)をサポートするモデル(GPT-4o, Claude, Gemini)を使用してください
|
||||||
|
- **低コスト**: DeepSeek は競争力のある価格を提供しています
|
||||||
|
- **プライバシー**: 完全にローカルなオフライン操作には Ollama を使用してください(強力なハードウェアが必要です)
|
||||||
|
- **柔軟性**: OpenRouter は単一の API で多数のモデルへのアクセスを提供します
|
||||||
267
docs/ja/cloudflare-deploy.md
Normal file
267
docs/ja/cloudflare-deploy.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Cloudflare Workers へのデプロイ
|
||||||
|
|
||||||
|
このプロジェクトは **OpenNext アダプター** を使用して **Cloudflare Worker** としてデプロイすることができ、以下のメリットがあります:
|
||||||
|
|
||||||
|
- グローバルエッジへのデプロイ
|
||||||
|
- 超低レイテンシー
|
||||||
|
- 無料の `workers.dev` ホスティング
|
||||||
|
- R2 を介した完全な Next.js ISR サポート(オプション)
|
||||||
|
|
||||||
|
> **Windows ユーザー向けの重要な注意:** OpenNext と Wrangler は、**ネイティブ Windows 環境では完全には信頼できません**。以下の方法を推奨します:
|
||||||
|
>
|
||||||
|
> - **GitHub Codespaces** を使用する(完全に動作します)
|
||||||
|
> - または **WSL (Linux)** を使用する
|
||||||
|
>
|
||||||
|
> 純粋な Windows 環境でのビルドは、WASM ファイルパスの問題により失敗する可能性があります。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
1. **Cloudflare アカウント**(基本的なデプロイには無料プランで十分です)
|
||||||
|
2. **Node.js 18以上**
|
||||||
|
3. **Wrangler CLI** のインストール(開発依存関係で問題ありません):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Cloudflare へのログイン:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler login
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意:** 支払い方法の登録が必要なのは、ISR キャッシュのために R2 を有効にする場合のみです。基本的な Workers へのデプロイは無料です。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ステップ 1 — 依存関係のインストール
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ステップ 2 — 環境変数の設定
|
||||||
|
|
||||||
|
Cloudflare はローカルテスト用に別のファイルを使用します。
|
||||||
|
|
||||||
|
### 1) `.dev.vars` の作成(Cloudflare ローカルおよびデプロイ用)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .dev.vars
|
||||||
|
```
|
||||||
|
|
||||||
|
API キーと設定を入力してください。
|
||||||
|
|
||||||
|
### 2) `.env.local` も存在することを確認(通常の Next.js 開発用)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
同じ値を入力してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ステップ 3 — デプロイタイプの選択
|
||||||
|
|
||||||
|
### オプション A: R2 なしでのデプロイ(シンプル、無料)
|
||||||
|
|
||||||
|
ISR キャッシュが不要な場合は、R2 なしでデプロイできます:
|
||||||
|
|
||||||
|
**1. シンプルな `open-next.config.ts` を使用:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. シンプルな `wrangler.jsonc` を使用(r2_buckets なし):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ステップ 4** へ進んでください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### オプション B: R2 ありでのデプロイ(完全な ISR サポート)
|
||||||
|
|
||||||
|
R2 を使用すると **Incremental Static Regeneration (ISR)** キャッシュが有効になります。これには Cloudflare アカウントに支払い方法の登録が必要です。
|
||||||
|
|
||||||
|
**1. R2 バケットの作成**(Cloudflare ダッシュボードにて):
|
||||||
|
|
||||||
|
- **Storage & Databases → R2** へ移動
|
||||||
|
- **Create bucket** をクリック
|
||||||
|
- 名前を入力: `next-inc-cache`
|
||||||
|
|
||||||
|
**2. `open-next.config.ts` の設定:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
|
||||||
|
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
|
||||||
|
|
||||||
|
export default defineCloudflareConfig({
|
||||||
|
incrementalCache: r2IncrementalCache,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. `wrangler.jsonc` の設定(R2 あり):**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"main": ".open-next/worker.js",
|
||||||
|
"name": "next-ai-draw-io-worker",
|
||||||
|
"compatibility_date": "2025-12-08",
|
||||||
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
||||||
|
"assets": {
|
||||||
|
"directory": ".open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "next-inc-cache"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "next-ai-draw-io-worker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重要:** `bucket_name` は Cloudflare ダッシュボードで作成した名前と完全に一致させる必要があります。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ステップ 4 — workers.dev サブドメインの登録(初回のみ)
|
||||||
|
|
||||||
|
初回デプロイの前に、workers.dev サブドメインが必要です。
|
||||||
|
|
||||||
|
**オプション 1: Cloudflare ダッシュボード経由(推奨)**
|
||||||
|
|
||||||
|
アクセス先: https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
|
||||||
|
|
||||||
|
**オプション 2: デプロイ時**
|
||||||
|
|
||||||
|
`npm run deploy` を実行した際、Wrangler が以下のように尋ねてくる場合があります:
|
||||||
|
|
||||||
|
```
|
||||||
|
Would you like to register a workers.dev subdomain? (Y/n)
|
||||||
|
```
|
||||||
|
|
||||||
|
`Y` を入力し、サブドメイン名を選択してください。
|
||||||
|
|
||||||
|
> **注意:** CI/CD や非対話型環境では、このプロンプトは表示されません。事前にダッシュボードで登録してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ステップ 5 — Cloudflare へのデプロイ
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
スクリプトの処理内容:
|
||||||
|
|
||||||
|
- Next.js アプリのビルド
|
||||||
|
- OpenNext を介した Cloudflare Worker への変換
|
||||||
|
- 静的アセットのアップロード
|
||||||
|
- Worker の公開
|
||||||
|
|
||||||
|
アプリは以下の URL で利用可能になります:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<worker-name>.<your-subdomain>.workers.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## よくある問題と解決策
|
||||||
|
|
||||||
|
### `You need to register a workers.dev subdomain`
|
||||||
|
|
||||||
|
**原因:** アカウントに workers.dev サブドメインが登録されていません。
|
||||||
|
|
||||||
|
**解決策:** https://dash.cloudflare.com → Workers & Pages → Set up a subdomain から登録してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Please enable R2 through the Cloudflare Dashboard`
|
||||||
|
|
||||||
|
**原因:** `wrangler.jsonc` で R2 が設定されていますが、アカウントで R2 が有効になっていません。
|
||||||
|
|
||||||
|
**解決策:** R2 を有効にする(支払い方法が必要)か、オプション A(R2 なしでデプロイ)を使用してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
|
||||||
|
|
||||||
|
**原因:** `wrangler.jsonc` に `r2_buckets` がありません。
|
||||||
|
|
||||||
|
**解決策:** `r2_buckets` セクションを追加するか、オプション A(R2 なし)に切り替えてください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Can't set compatibility date in the future`
|
||||||
|
|
||||||
|
**原因:** wrangler 設定の `compatibility_date` が未来の日付に設定されています。
|
||||||
|
|
||||||
|
**解決策:** `compatibility_date` を今日またはそれ以前の日付に変更してください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Windows エラー: `resvg.wasm?module` (ENOENT)
|
||||||
|
|
||||||
|
**原因:** Windows のファイル名には `?` を含めることができませんが、wasm アセットのファイル名に `?module` が使用されているためです。
|
||||||
|
|
||||||
|
**解決策:** Linux 環境(WSL、Codespaces、または CI)でビルド/デプロイしてください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## オプション: ローカルでのプレビュー
|
||||||
|
|
||||||
|
デプロイ前に Worker をローカルでプレビューできます:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## まとめ
|
||||||
|
|
||||||
|
| 機能 | R2 なし | R2 あり |
|
||||||
|
|---------|------------|---------|
|
||||||
|
| コスト | 無料 | 支払い方法が必要 |
|
||||||
|
| ISR キャッシュ | なし | あり |
|
||||||
|
| 静的ページ | あり | あり |
|
||||||
|
| API ルート | あり | あり |
|
||||||
|
| 設定の複雑さ | シンプル | 普通 |
|
||||||
|
|
||||||
|
テストやシンプルなアプリには **R2 なし** を選んでください。ISR キャッシュが必要な本番アプリには **R2 あり** を選んでください。
|
||||||
29
docs/ja/docker.md
Normal file
29
docs/ja/docker.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Dockerで実行する
|
||||||
|
|
||||||
|
ローカルで実行したいだけであれば、Dockerを使用するのが最も良い方法です。
|
||||||
|
|
||||||
|
まず、Dockerがまだインストールされていない場合はインストールしてください: [Dockerを入手](https://docs.docker.com/get-docker/)
|
||||||
|
|
||||||
|
次に、以下を実行します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e AI_PROVIDER=openai \
|
||||||
|
-e AI_MODEL=gpt-4o \
|
||||||
|
-e OPENAI_API_KEY=your_api_key \
|
||||||
|
ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
または、envファイルを使用します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example .env
|
||||||
|
# .envを構成に合わせて編集します
|
||||||
|
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
ブラウザで[http://localhost:3000](http://localhost:3000)を開きます。
|
||||||
|
|
||||||
|
環境変数は、お好みのAIプロバイダー設定に置き換えてください。利用可能なオプションについては、[AIプロバイダー](./ai-providers.md)を参照してください。
|
||||||
|
|
||||||
|
> **オフラインデプロイ:** `embed.diagrams.net`がブロックされている場合は、構成オプションについて[オフラインデプロイ](./offline-deployment.md)を参照してください。
|
||||||
38
docs/ja/offline-deployment.md
Normal file
38
docs/ja/offline-deployment.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# オフラインデプロイ
|
||||||
|
|
||||||
|
`embed.diagrams.net` の代わりに draw.io をセルフホストすることで、Next AI Draw.io をオフライン環境にデプロイできます。
|
||||||
|
|
||||||
|
**注:** `NEXT_PUBLIC_DRAWIO_BASE_URL` は**ビルド時**の変数です。これを変更する場合は、Docker イメージの再ビルドが必要です。
|
||||||
|
|
||||||
|
## Docker Compose のセットアップ
|
||||||
|
|
||||||
|
1. リポジトリをクローンし、`.env` ファイルに API キーを定義します。
|
||||||
|
2. `docker-compose.yml` を作成します。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
drawio:
|
||||||
|
image: jgraph/drawio:latest
|
||||||
|
ports: ["8080:8080"]
|
||||||
|
next-ai-draw-io:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
env_file: .env
|
||||||
|
depends_on: [drawio]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. `docker compose up -d` を実行し、`http://localhost:3000` にアクセスします。
|
||||||
|
|
||||||
|
## 設定と重要な警告
|
||||||
|
|
||||||
|
**`NEXT_PUBLIC_DRAWIO_BASE_URL` は、ユーザーのブラウザからアクセスできる必要があります。**
|
||||||
|
|
||||||
|
| シナリオ | URL の値 |
|
||||||
|
|----------|-----------|
|
||||||
|
| ローカルホスト | `http://localhost:8080` |
|
||||||
|
| リモート/サーバー | `http://YOUR_SERVER_IP:8080` |
|
||||||
|
|
||||||
|
**`http://drawio:8080` のような Docker 内部のエイリアスは絶対に使用しないでください。** ブラウザはこれらを名前解決できません。
|
||||||
78
docs/shape-libraries/README.md
Normal file
78
docs/shape-libraries/README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Draw.io Shape Libraries
|
||||||
|
|
||||||
|
Reference: `style="shape=mxgraph.<library>.<shape_name>"`
|
||||||
|
|
||||||
|
## Cloud Providers
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| aws4 | 1031 | `mxgraph.aws4` | Amazon Web Services (2025) - EC2, S3, Lambda, RDS, etc. | [aws4.md](./aws4.md) |
|
||||||
|
| azure2 | 608 | `img/lib/azure2/` | Microsoft Azure (2024) - VMs, Storage, AI, Networking, etc. | [azure2.md](./azure2.md) |
|
||||||
|
| gcp2 | 297 | `mxgraph.gcp2` | Google Cloud Platform - Compute Engine, BigQuery, GKE, etc. | [gcp2.md](./gcp2.md) |
|
||||||
|
| alibaba_cloud | 273 | `mxgraph.alibaba_cloud` | Alibaba Cloud - ECS, OSS, RDS, SLB, VPC, etc. | [alibaba_cloud.md](./alibaba_cloud.md) |
|
||||||
|
| openstack | 18 | `mxgraph.openstack` | OpenStack cloud platform icons | [openstack.md](./openstack.md) |
|
||||||
|
| digitalocean | 74 | `mxgraph.digitalocean` | DigitalOcean - Droplets, Spaces, Kubernetes, etc. | [digitalocean.md](./digitalocean.md) |
|
||||||
|
| salesforce | 96 | `mxgraph.salesforce` | Salesforce platform icons | [salesforce.md](./salesforce.md) |
|
||||||
|
|
||||||
|
## Networking & Infrastructure
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| cisco19 | 232 | `mxgraph.cisco19` | Cisco network equipment - routers, switches, firewalls | [cisco19.md](./cisco19.md) |
|
||||||
|
| network | 58 | `mxgraph.networks` | General network diagram symbols | [network.md](./network.md) |
|
||||||
|
| arista | 45 | `mxgraph.arista` | Arista network switches and equipment | [arista.md](./arista.md) |
|
||||||
|
| kubernetes | 40 | `mxgraph.kubernetes` | Kubernetes - pods, services, deployments, nodes | [kubernetes.md](./kubernetes.md) |
|
||||||
|
| vvd | 93 | `mxgraph.vvd` | VMware Validated Design icons | [vvd.md](./vvd.md) |
|
||||||
|
| rack | 11 | `mxgraph.rack` | Server rack and data center equipment | [rack.md](./rack.md) |
|
||||||
|
|
||||||
|
## Business Process
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| bpmn | 39 | `mxgraph.bpmn` | Business Process Model and Notation - events, gateways, tasks | [bpmn.md](./bpmn.md) |
|
||||||
|
| eip | 36 | `mxgraph.eip` | Enterprise Integration Patterns - messaging, routing | [eip.md](./eip.md) |
|
||||||
|
| lean_mapping | 13 | `mxgraph.lean_mapping` | Lean/Value Stream Mapping symbols | [lean_mapping.md](./lean_mapping.md) |
|
||||||
|
|
||||||
|
## General Diagrams
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| flowchart | 34 | `mxgraph.flowchart` | Standard flowchart symbols - process, decision, data | [flowchart.md](./flowchart.md) |
|
||||||
|
| basic | 30 | `mxgraph.basic` | Basic shapes - stars, banners, callouts, hearts | [basic.md](./basic.md) |
|
||||||
|
| arrows2 | 34 | `mxgraph.arrows2` | Arrow shapes and connectors | [arrows2.md](./arrows2.md) |
|
||||||
|
| infographic | 29 | `mxgraph.infographic` | Infographic elements - charts, icons, badges | [infographic.md](./infographic.md) |
|
||||||
|
| sitemap | 50 | `mxgraph.sitemap` | Website sitemap icons - pages, forms, navigation | [sitemap.md](./sitemap.md) |
|
||||||
|
|
||||||
|
## UI/Mockups
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| android | 17 | `mxgraph.android` | Android UI mockup components | [android.md](./android.md) |
|
||||||
|
|
||||||
|
## Enterprise Software
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| citrix | 97 | `mxgraph.citrix` | Citrix virtualization - XenApp, XenDesktop, NetScaler | [citrix.md](./citrix.md) |
|
||||||
|
| sap | 98 | `mxgraph.sap` | SAP enterprise software icons | [sap.md](./sap.md) |
|
||||||
|
| mscae | 73 | `mxgraph.mscae` | Microsoft Cloud and Enterprise symbols | [mscae.md](./mscae.md) |
|
||||||
|
| atlassian | 26 | `mxgraph.atlassian` | Atlassian - Jira, Confluence issue types | [atlassian.md](./atlassian.md) |
|
||||||
|
|
||||||
|
## Engineering
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| fluidpower | 246 | `mxgraph.fluid_power` | Hydraulic/pneumatic engineering symbols | [fluidpower.md](./fluidpower.md) |
|
||||||
|
| electrical | 50 | `mxgraph.electrical` | Electrical circuit symbols - resistors, capacitors | [electrical.md](./electrical.md) |
|
||||||
|
| pid | 18 | `mxgraph.pid2` | Piping and Instrumentation Diagram symbols | [pid.md](./pid.md) |
|
||||||
|
| cabinets | 53 | `mxgraph.cabinets` | Electrical cabinet components - breakers, terminals | [cabinets.md](./cabinets.md) |
|
||||||
|
| floorplan | 44 | `mxgraph.floorplan` | Floor plan furniture and fixtures | [floorplan.md](./floorplan.md) |
|
||||||
|
|
||||||
|
## Icons & Graphics
|
||||||
|
|
||||||
|
| Library | Shapes | Prefix | Description | File |
|
||||||
|
|---------|--------|--------|-------------|------|
|
||||||
|
| webicons | 176 | `mxgraph.webicons` | Web/social media logos - GitHub, Twitter, AWS, etc. | [webicons.md](./webicons.md) |
|
||||||
|
| un-ocha-icons | 242 | `mxgraph.un-ocha-icons` | UN OCHA humanitarian icons | [un-ocha-icons.md](./un-ocha-icons.md) |
|
||||||
|
|
||||||
|
**Total: 33 libraries, 4,281 shapes**
|
||||||
328
docs/shape-libraries/alibaba_cloud.md
Normal file
328
docs/shape-libraries/alibaba_cloud.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# alibaba_cloud
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.alibaba_cloud`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.alibaba_cloud.{shape};fillColor=#FF6A00;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (311)
|
||||||
|
|
||||||
|
- `abap_business_application_platform`
|
||||||
|
- `acms_application_configuration_manangement`
|
||||||
|
- `acr_cloud_container_registry`
|
||||||
|
- `actiontrail`
|
||||||
|
- `adam_advanced_database_and_application_migration`
|
||||||
|
- `adb_analyticdb_for_mysql`
|
||||||
|
- `address_purification`
|
||||||
|
- `afs_fraud_service`
|
||||||
|
- `agw_aligateway`
|
||||||
|
- `ahas_application_high_availability_service`
|
||||||
|
- `airec_artificial_intelligence_recommendation`
|
||||||
|
- `alb_application_load_balancer_01`
|
||||||
|
- `alb_application_load_balancer_02`
|
||||||
|
- `alibaba_cloud_logo`
|
||||||
|
- `alibaba_cloud_logo_chinese`
|
||||||
|
- `alibaba_cloud_logo_english`
|
||||||
|
- `alimail`
|
||||||
|
- `alimt_machine_translation`
|
||||||
|
- `aliyun_linux`
|
||||||
|
- `amqp_advanced_message_queuing_protocol`
|
||||||
|
- `amscloudapp`
|
||||||
|
- `analyticdb_for_postgresql`
|
||||||
|
- `antibot`
|
||||||
|
- `apigateway`
|
||||||
|
- `apsara_file_storage_for_hdfs`
|
||||||
|
- `apsaravideo_vod`
|
||||||
|
- `arms_application_real-time_monitoring_service`
|
||||||
|
- `ask_ack_container_service_for_kubernetes`
|
||||||
|
- `asm_service_mesh`
|
||||||
|
- `assettech`
|
||||||
|
- `avds_vulnerability_db_scanning`
|
||||||
|
- `baas_blockchain_as_a_service`
|
||||||
|
- `bandwidth_bag`
|
||||||
|
- `bastionhost`
|
||||||
|
- `batchcompute`
|
||||||
|
- `bccluster`
|
||||||
|
- `beebot`
|
||||||
|
- `beian`
|
||||||
|
- `bizdevops`
|
||||||
|
- `bizworks`
|
||||||
|
- `bpstudio`
|
||||||
|
- `cas_ssl_central_authentication_service`
|
||||||
|
- `cassandra_wide-column_database_01`
|
||||||
|
- `cassandra_wide-column_database_02`
|
||||||
|
- `ccc_cloud_call_center`
|
||||||
|
- `ccn_cloud_connect_network`
|
||||||
|
- `ccs_customer_service_01`
|
||||||
|
- `ccs_customer_service_02`
|
||||||
|
- `cddc_cloud_database_dedicated_cluster`
|
||||||
|
- `cdn_content_distribution_network`
|
||||||
|
- `cdp_cloudera_cdp`
|
||||||
|
- `cdt_cloud_datatransfer`
|
||||||
|
- `cen_cloud_enterprise_network`
|
||||||
|
- `cfw_cloud_firewall`
|
||||||
|
- `cityvisual`
|
||||||
|
- `clb_classic_load_balancer_01`
|
||||||
|
- `clb_classic_load_balancer_02`
|
||||||
|
- `clickhouse`
|
||||||
|
- `cloud_auth`
|
||||||
|
- `cloud_config`
|
||||||
|
- `cloud_display`
|
||||||
|
- `cloud_governance_center`
|
||||||
|
- `cloud_security_center`
|
||||||
|
- `cloud_shield`
|
||||||
|
- `cloudap`
|
||||||
|
- `cloudbox`
|
||||||
|
- `clouddesktop`
|
||||||
|
- `clouddev`
|
||||||
|
- `cloudphoto`
|
||||||
|
- `cloudproc`
|
||||||
|
- `cloudshell`
|
||||||
|
- `cmn_cloud_managed_network`
|
||||||
|
- `cmp_cloud_mobile_push`
|
||||||
|
- `cms_cloud_monitor_service`
|
||||||
|
- `codepipeline`
|
||||||
|
- `codestore`
|
||||||
|
- `companyreg`
|
||||||
|
- `computenest`
|
||||||
|
- `content_security`
|
||||||
|
- `coo`
|
||||||
|
- `cpns_cell_phone_number_service`
|
||||||
|
- `csas_cloud_security_access_service`
|
||||||
|
- `cvc_cloud_video_conferencing`
|
||||||
|
- `cwh_cloud_web_hosting`
|
||||||
|
- `das_database_autonomy_service`
|
||||||
|
- `databot`
|
||||||
|
- `datahub`
|
||||||
|
- `dataphin`
|
||||||
|
- `dataquotient`
|
||||||
|
- `datav`
|
||||||
|
- `dataworks_dataide`
|
||||||
|
- `dbaudit`
|
||||||
|
- `dbes_database_expert_service`
|
||||||
|
- `dbfs_database_file_system`
|
||||||
|
- `dbs_database_backup`
|
||||||
|
- `dcdn_dynamic_route_for_cdn`
|
||||||
|
- `ddh_dedicated_host`
|
||||||
|
- `ddos-bgp`
|
||||||
|
- `ddos-dip`
|
||||||
|
- `ddos-pro`
|
||||||
|
- `ddos_protection`
|
||||||
|
- `devops`
|
||||||
|
- `dg_database_gateway`
|
||||||
|
- `directmail`
|
||||||
|
- `disk_block_storage`
|
||||||
|
- `dlf_data_lake_formation`
|
||||||
|
- `dms_data_management_service`
|
||||||
|
- `dns_domain_name_system`
|
||||||
|
- `dns_privatezone_01`
|
||||||
|
- `dns_privatezone_02`
|
||||||
|
- `domain`
|
||||||
|
- `domain_and_website`
|
||||||
|
- `drds_distribute_relational_database_service`
|
||||||
|
- `dsi_data_security_insurance`
|
||||||
|
- `dts_data_transmission_service`
|
||||||
|
- `e-mapreduce`
|
||||||
|
- `eais_elastic_accelerated_computing_instances`
|
||||||
|
- `eci_elastic_container_instance`
|
||||||
|
- `ecs_elastic_compute_service`
|
||||||
|
- `edas_enterprise_distributed_application_service`
|
||||||
|
- `ehpc_elastic_high_performance_computing`
|
||||||
|
- `eip_elastic_ip_address`
|
||||||
|
- `elastic_web_hosting`
|
||||||
|
- `elasticsearch`
|
||||||
|
- `emas_enterprise_mobile_application_studio`
|
||||||
|
- `energyexpert`
|
||||||
|
- `ens_edge_node_service`
|
||||||
|
- `enterprise_website`
|
||||||
|
- `eprofile`
|
||||||
|
- `esign`
|
||||||
|
- `ess_elastic_scaling_service`
|
||||||
|
- `eventbridge`
|
||||||
|
- `express_connect`
|
||||||
|
- `face_recognition`
|
||||||
|
- `fc_function_compute`
|
||||||
|
- `flow_service`
|
||||||
|
- `flowbag`
|
||||||
|
- `fnf_serverless_function_flow`
|
||||||
|
- `fpga_field_programmable_gate_array`
|
||||||
|
- `fraud_detection`
|
||||||
|
- `ga_global_accelerator`
|
||||||
|
- `gameshield`
|
||||||
|
- `gdb_graph_database`
|
||||||
|
- `graphanalytics`
|
||||||
|
- `graphcompute`
|
||||||
|
- `gtm_global_traffic_manager`
|
||||||
|
- `gts_global_transaction_service`
|
||||||
|
- `gws_graphic_workstation`
|
||||||
|
- `havip_high-availability_virtual_ip_address`
|
||||||
|
- `hbase`
|
||||||
|
- `hbr_hybrid_backup_recovery`
|
||||||
|
- `hcs-hgw_hybrid_cloud_storage_array`
|
||||||
|
- `hcs-mgw_hybrid_cloud_storage_datatransport`
|
||||||
|
- `hcs-sgw_hybrid_cloud_storage_gateway`
|
||||||
|
- `hdr_hybrid_disaster_recovery`
|
||||||
|
- `hologres`
|
||||||
|
- `holowatcher`
|
||||||
|
- `hsm_hardware_security_module`
|
||||||
|
- `httpdns`
|
||||||
|
- `idrsservice`
|
||||||
|
- `image_recognition`
|
||||||
|
- `imagesearch`
|
||||||
|
- `imarketing`
|
||||||
|
- `imm_intelligent_media_management`
|
||||||
|
- `imp_intelligent_media_production`
|
||||||
|
- `imp_low_code_video_factory`
|
||||||
|
- `indvi_industrial_visual_intelligence`
|
||||||
|
- `intelligent_advisor`
|
||||||
|
- `iot_internet_of_things_platform`
|
||||||
|
- `iot_wireless_connection_service`
|
||||||
|
- `iotid_identity`
|
||||||
|
- `iov_iot_vehicle_cloud`
|
||||||
|
- `ipv6_gateway`
|
||||||
|
- `isoc_iot_security_operations_center`
|
||||||
|
- `isu_intelligent_semantic_understanding`
|
||||||
|
- `ivision`
|
||||||
|
- `ivpd_intelligent_visual_production`
|
||||||
|
- `kafka`
|
||||||
|
- `linkedmall`
|
||||||
|
- `linkwan`
|
||||||
|
- `live`
|
||||||
|
- `livinglink`
|
||||||
|
- `log_streaming`
|
||||||
|
- `logic_composer`
|
||||||
|
- `machine_learning`
|
||||||
|
- `man_mobile_analytics`
|
||||||
|
- `mariadb`
|
||||||
|
- `mas_mobile_acceleration_service`
|
||||||
|
- `maxcompute`
|
||||||
|
- `memcache`
|
||||||
|
- `miniappdev`
|
||||||
|
- `mns_message_service`
|
||||||
|
- `mobile_hotfix`
|
||||||
|
- `mobsec`
|
||||||
|
- `mongodb`
|
||||||
|
- `mps-ai`
|
||||||
|
- `mps-censor`
|
||||||
|
- `mps-cover`
|
||||||
|
- `mps-dna`
|
||||||
|
- `mps-multimod`
|
||||||
|
- `mps-produce`
|
||||||
|
- `mps_apsaravideo_media_processing`
|
||||||
|
- `mq_message_queue`
|
||||||
|
- `mqc_mobile_quality_center`
|
||||||
|
- `mse_microservices_engine`
|
||||||
|
- `multi-cloud_finops`
|
||||||
|
- `multi-mode_database_lindorm`
|
||||||
|
- `multimediaai`
|
||||||
|
- `mxgraph.alibaba_cloud`
|
||||||
|
- `mysql`
|
||||||
|
- `nas_network_attached_storage`
|
||||||
|
- `nat_gateway`
|
||||||
|
- `network_acl_access_control_list`
|
||||||
|
- `nlb_network_load_balancer_01`
|
||||||
|
- `nlb_network_load_balancer_02`
|
||||||
|
- `nlp-address`
|
||||||
|
- `nlp-automl`
|
||||||
|
- `nlp-ie_text_information_extraction`
|
||||||
|
- `nlp-ke_keyword_extraction`
|
||||||
|
- `nlp-ner_named_entity_recognition`
|
||||||
|
- `nlp-pos_part-of-speech_tagging`
|
||||||
|
- `nlp-ra_reflexive_anaphora`
|
||||||
|
- `nlp-sa_sentiment_analysis`
|
||||||
|
- `nlp-tc_text_categorization`
|
||||||
|
- `nlp-ws_word_segmentation`
|
||||||
|
- `nlp_natural_language_processing`
|
||||||
|
- `nls`
|
||||||
|
- `nls-asrbag`
|
||||||
|
- `nls-asrcustommodel`
|
||||||
|
- `nls-filebag`
|
||||||
|
- `nls-service`
|
||||||
|
- `nls-shortasrbag`
|
||||||
|
- `nls-ttsbag`
|
||||||
|
- `nodejs_performance_platform`
|
||||||
|
- `oceanbase`
|
||||||
|
- `ocr_optical_character_recognition`
|
||||||
|
- `onsmqtt_micro_message_queuing_telemetry_transport`
|
||||||
|
- `oos_operation_orchestration_service`
|
||||||
|
- `openanalytics`
|
||||||
|
- `openapi_explorer`
|
||||||
|
- `opensearch`
|
||||||
|
- `oss_object_storage_service`
|
||||||
|
- `ots_tablestore`
|
||||||
|
- `outboundbot`
|
||||||
|
- `pcdn_p2p_cdn`
|
||||||
|
- `petadata_hybriddb_for_mysql`
|
||||||
|
- `physical_connection`
|
||||||
|
- `pnvs_phone_number_verification_service`
|
||||||
|
- `polardb`
|
||||||
|
- `porana_portrait_analysis`
|
||||||
|
- `postgresql`
|
||||||
|
- `ppas_pay-as-you-go_database`
|
||||||
|
- `privatelink`
|
||||||
|
- `prometheus`
|
||||||
|
- `prophet`
|
||||||
|
- `pts_performance_test_service`
|
||||||
|
- `quickbi`
|
||||||
|
- `ram_resource_access_management`
|
||||||
|
- `re_recommendation_engine`
|
||||||
|
- `realtime_compute`
|
||||||
|
- `redis_kvstore`
|
||||||
|
- `region`
|
||||||
|
- `retailir`
|
||||||
|
- `ros_resource_orchestration_service`
|
||||||
|
- `route_table`
|
||||||
|
- `router`
|
||||||
|
- `rsimganalys`
|
||||||
|
- `rtc_real-time_communication`
|
||||||
|
- `sae_serverless_app_engine`
|
||||||
|
- `sag_smart_access_gateway_01`
|
||||||
|
- `sag_smart_access_gateway_02`
|
||||||
|
- `sas_situational_awareness`
|
||||||
|
- `sca_smart_conversation_analysis_01`
|
||||||
|
- `sca_smart_conversation_analysis_02`
|
||||||
|
- `scc_super_computing_cluster`
|
||||||
|
- `scdn_secure_cdn`
|
||||||
|
- `scu_storage_capacity_unit`
|
||||||
|
- `sddp_sensitive_data_protection`
|
||||||
|
- `shared_bandwidth`
|
||||||
|
- `shared_flow_bag`
|
||||||
|
- `shc_shield_hybrid_cloud`
|
||||||
|
- `slb_server_load_balancer_01`
|
||||||
|
- `slb_server_load_balancer_02`
|
||||||
|
- `slb_server_load_balancer_03`
|
||||||
|
- `sls_simple_log_service`
|
||||||
|
- `smc_server_migration_center`
|
||||||
|
- `sms_short_message_service`
|
||||||
|
- `sos`
|
||||||
|
- `spark_data_insights`
|
||||||
|
- `sppc`
|
||||||
|
- `sqlserver`
|
||||||
|
- `swas_simple_application_server`
|
||||||
|
- `tr_transit_router`
|
||||||
|
- `trademark_service`
|
||||||
|
- `uis_ultimate_internet_service`
|
||||||
|
- `user`
|
||||||
|
- `user_feedback_01`
|
||||||
|
- `user_feedback_02`
|
||||||
|
- `vbr_virtual_border_router`
|
||||||
|
- `vcs_visual_computing_service`
|
||||||
|
- `vms_voice_messaging_service`
|
||||||
|
- `voicebot_intelligent_voice_navigation`
|
||||||
|
- `vpc_virtual_private_cloud`
|
||||||
|
- `vpn_gateway`
|
||||||
|
- `vs_video_surveillance`
|
||||||
|
- `vswitch`
|
||||||
|
- `waf_web_application_firewall`
|
||||||
|
- `webplus_web_app_service`
|
||||||
|
- `xdragon_bare_metal_server`
|
||||||
|
- `xtrace`
|
||||||
|
- `yida`
|
||||||
62
docs/shape-libraries/android.md
Normal file
62
docs/shape-libraries/android.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# android
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.android`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.android.phone2;strokeColor=#c0c0c0;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="200" height="390" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (47)
|
||||||
|
|
||||||
|
- `action_bar`
|
||||||
|
- `action_bar_landscape`
|
||||||
|
- `anchor`
|
||||||
|
- `checkbox`
|
||||||
|
- `contact_badge_focused`
|
||||||
|
- `contextual_action_bar`
|
||||||
|
- `contextual_action_bar_landscape`
|
||||||
|
- `contextual_split_action_bar`
|
||||||
|
- `contextual_split_action_bar_landscape`
|
||||||
|
- `contextual_split_action_bar_landscape_white`
|
||||||
|
- `indeterminateSpinner`
|
||||||
|
- `indeterminate_progress_bar`
|
||||||
|
- `keyboard`
|
||||||
|
- `navigation_bar_1`
|
||||||
|
- `navigation_bar_1_landscape`
|
||||||
|
- `navigation_bar_1_vertical`
|
||||||
|
- `navigation_bar_2`
|
||||||
|
- `navigation_bar_3`
|
||||||
|
- `navigation_bar_3_landscape`
|
||||||
|
- `navigation_bar_4`
|
||||||
|
- `navigation_bar_5`
|
||||||
|
- `navigation_bar_5_vertical`
|
||||||
|
- `navigation_bar_6`
|
||||||
|
- `phone2`
|
||||||
|
- `progressBar`
|
||||||
|
- `progressScrubberDisabled`
|
||||||
|
- `progressScrubberFocused`
|
||||||
|
- `progressScrubberPressed`
|
||||||
|
- `quick_contact`
|
||||||
|
- `quickscroll2`
|
||||||
|
- `quickscroll3`
|
||||||
|
- `rect`
|
||||||
|
- `rrect`
|
||||||
|
- `scrollbars2`
|
||||||
|
- `spinner2`
|
||||||
|
- `split_action_bar`
|
||||||
|
- `split_action_bar_landscape`
|
||||||
|
- `statusBar`
|
||||||
|
- `switch_off`
|
||||||
|
- `switch_on`
|
||||||
|
- `tab2`
|
||||||
|
- `textSelHandles`
|
||||||
|
- `text_insertion_point`
|
||||||
|
- `textfield`
|
||||||
|
- `time_picker`
|
||||||
|
- `time_picker_dark`
|
||||||
|
- `transparent`
|
||||||
33
docs/shape-libraries/arrows2.md
Normal file
33
docs/shape-libraries/arrows2.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# arrows2
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.arrows2`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.arrows2.arrow;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="100" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (18)
|
||||||
|
|
||||||
|
- `arrow`
|
||||||
|
- `bendArrow`
|
||||||
|
- `bendDoubleArrow`
|
||||||
|
- `calloutArrow`
|
||||||
|
- `calloutDouble90Arrow`
|
||||||
|
- `calloutDoubleArrow`
|
||||||
|
- `calloutQuadArrow`
|
||||||
|
- `jumpInArrow`
|
||||||
|
- `quadArrow`
|
||||||
|
- `sharpArrow`
|
||||||
|
- `sharpArrow2`
|
||||||
|
- `stripedArrow`
|
||||||
|
- `stylisedArrow`
|
||||||
|
- `tailedArrow`
|
||||||
|
- `tailedNotchedArrow`
|
||||||
|
- `triadArrow`
|
||||||
|
- `twoWayArrow`
|
||||||
|
- `uTurnArrow`
|
||||||
32
docs/shape-libraries/atlassian.md
Normal file
32
docs/shape-libraries/atlassian.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# atlassian
|
||||||
|
|
||||||
|
**Type:** SVG images
|
||||||
|
**Path:** `img/lib/atlassian/`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="image;aspect=fixed;image=img/lib/atlassian/Jira_Logo.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (17)
|
||||||
|
|
||||||
|
- `Atlassian_Logo`
|
||||||
|
- `Bamboo_Logo`
|
||||||
|
- `Bitbucket_Logo`
|
||||||
|
- `Clover_Logo`
|
||||||
|
- `Confluence_Logo`
|
||||||
|
- `Crowd_Logo`
|
||||||
|
- `Crucible_Logo`
|
||||||
|
- `Fisheye_Logo`
|
||||||
|
- `Hipchat_Logo`
|
||||||
|
- `Jira_Core_Logo`
|
||||||
|
- `Jira_Logo`
|
||||||
|
- `Jira_Service_Desk_Logo`
|
||||||
|
- `Jira_Software_Logo`
|
||||||
|
- `Sourcetree_Logo`
|
||||||
|
- `Statuspage_Logo`
|
||||||
|
- `Stride_Logo`
|
||||||
|
- `Trello_Logo`
|
||||||
1049
docs/shape-libraries/aws4.md
Normal file
1049
docs/shape-libraries/aws4.md
Normal file
File diff suppressed because it is too large
Load Diff
431
docs/shape-libraries/azure2.md
Normal file
431
docs/shape-libraries/azure2.md
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
# azure2
|
||||||
|
|
||||||
|
**Type:** SVG images
|
||||||
|
**Path:** `img/lib/azure2/`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="image;aspect=fixed;image=img/lib/azure2/compute/Virtual_Machine.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes (648)
|
||||||
|
|
||||||
|
Shapes are organized by category: `azure2/{category}/{shape}.svg`
|
||||||
|
|
||||||
|
### ai_machine_learning (30)
|
||||||
|
|
||||||
|
- `AI_Studio`
|
||||||
|
- `Anomaly_Detector`
|
||||||
|
- `Azure_Applied_AI`
|
||||||
|
- `Azure_Experimentation_Studio`
|
||||||
|
- `Azure_Object_Understanding`
|
||||||
|
- `Azure_OpenAI`
|
||||||
|
- `Batch_AI`
|
||||||
|
- `Bonsai`
|
||||||
|
- `Bot_Services`
|
||||||
|
- `Cognitive_Services`
|
||||||
|
- `Cognitive_Services_Decisions`
|
||||||
|
- `Computer_Vision`
|
||||||
|
- `Content_Moderators`
|
||||||
|
- `Content_Safety`
|
||||||
|
- `Custom_Vision`
|
||||||
|
- `Face_APIs`
|
||||||
|
- `Form_Recognizers`
|
||||||
|
- `Genomics`
|
||||||
|
- `Immersive_Readers`
|
||||||
|
- `Language_Services`
|
||||||
|
- `Language_Understanding`
|
||||||
|
- `Machine_Learning`
|
||||||
|
- `Machine_Learning_Studio_Classic_Web_Services`
|
||||||
|
- `Machine_Learning_Studio_Web_Service_Plans`
|
||||||
|
- `Machine_Learning_Studio_Workspaces`
|
||||||
|
- `Personalizers`
|
||||||
|
- `QnA_Makers`
|
||||||
|
- `Serverless_Search`
|
||||||
|
- `Speech_Services`
|
||||||
|
- `Translator_Text`
|
||||||
|
|
||||||
|
### analytics (14)
|
||||||
|
|
||||||
|
- `Analysis_Services`
|
||||||
|
- `Azure_Databricks`
|
||||||
|
- `Azure_Synapse_Analytics`
|
||||||
|
- `Azure_Workbooks`
|
||||||
|
- `Data_Lake_Analytics`
|
||||||
|
- `Data_Lake_Store_Gen1`
|
||||||
|
- `Endpoint_Analytics`
|
||||||
|
- `Event_Hub_Clusters`
|
||||||
|
- `Event_Hubs`
|
||||||
|
- `HD_Insight_Clusters`
|
||||||
|
- `Log_Analytics_Workspaces`
|
||||||
|
- `Power_BI_Embedded`
|
||||||
|
- `Power_Platform`
|
||||||
|
- `Stream_Analytics_Jobs`
|
||||||
|
|
||||||
|
### app_services (9)
|
||||||
|
|
||||||
|
- `API_Management_Services`
|
||||||
|
- `App_Service_Certificates`
|
||||||
|
- `App_Service_Domains`
|
||||||
|
- `App_Service_Environments`
|
||||||
|
- `App_Service_Plans`
|
||||||
|
- `App_Services`
|
||||||
|
- `CDN_Profiles`
|
||||||
|
- `Notification_Hubs`
|
||||||
|
- `Search_Services`
|
||||||
|
|
||||||
|
### compute (38)
|
||||||
|
|
||||||
|
- `App_Services`
|
||||||
|
- `Application_Group`
|
||||||
|
- `Automanaged_VM`
|
||||||
|
- `Availability_Sets`
|
||||||
|
- `Azure_Compute_Galleries`
|
||||||
|
- `Azure_Spring_Cloud`
|
||||||
|
- `Batch_Accounts`
|
||||||
|
- `Cloud_Services_Classic`
|
||||||
|
- `Container_Instances`
|
||||||
|
- `Container_Services_Deprecated`
|
||||||
|
- `Disk_Encryption_Sets`
|
||||||
|
- `Disks`
|
||||||
|
- `Disks_Classic`
|
||||||
|
- `Disks_Snapshots`
|
||||||
|
- `Function_Apps`
|
||||||
|
- `Host_Groups`
|
||||||
|
- `Host_Pools`
|
||||||
|
- `Hosts`
|
||||||
|
- `Image_Definitions`
|
||||||
|
- `Image_Templates`
|
||||||
|
- `Image_Versions`
|
||||||
|
- `Images`
|
||||||
|
- `Kubernetes_Services`
|
||||||
|
- `Maintenance_Configuration`
|
||||||
|
- `Managed_Service_Fabric`
|
||||||
|
- `Mesh_Applications`
|
||||||
|
- `Metrics_Advisor`
|
||||||
|
- `OS_Images_Classic`
|
||||||
|
- `Restore_Points`
|
||||||
|
- `Restore_Points_Collections`
|
||||||
|
- `Service_Fabric_Clusters`
|
||||||
|
- `Shared_Image_Galleries`
|
||||||
|
- `VM_Images_Classic`
|
||||||
|
- `VM_Scale_Sets`
|
||||||
|
- `Virtual_Machine`
|
||||||
|
- `Virtual_Machines_Classic`
|
||||||
|
- `Workspaces`
|
||||||
|
- `Workspaces2`
|
||||||
|
|
||||||
|
### containers (7)
|
||||||
|
|
||||||
|
- `App_Services`
|
||||||
|
- `Azure_Red_Hat_OpenShift`
|
||||||
|
- `Batch_Accounts`
|
||||||
|
- `Container_Instances`
|
||||||
|
- `Container_Registries`
|
||||||
|
- `Kubernetes_Services`
|
||||||
|
- `Service_Fabric_Clusters`
|
||||||
|
|
||||||
|
### databases (27)
|
||||||
|
|
||||||
|
- `Azure_Cosmos_DB`
|
||||||
|
- `Azure_Data_Explorer_Clusters`
|
||||||
|
- `Azure_Database_MariaDB_Server`
|
||||||
|
- `Azure_Database_Migration_Services`
|
||||||
|
- `Azure_Database_MySQL_Server`
|
||||||
|
- `Azure_Database_PostgreSQL_Server`
|
||||||
|
- `Azure_Database_PostgreSQL_Server_Group`
|
||||||
|
- `Azure_Purview_Accounts`
|
||||||
|
- `Azure_SQL`
|
||||||
|
- `Azure_SQL_Edge`
|
||||||
|
- `Azure_SQL_Server_Stretch_Databases`
|
||||||
|
- `Azure_SQL_VM`
|
||||||
|
- `Azure_Synapse_Analytics`
|
||||||
|
- `Cache_Redis`
|
||||||
|
- `Data_Factory`
|
||||||
|
- `Elastic_Job_Agents`
|
||||||
|
- `Instance_Pools`
|
||||||
|
- `Managed_Database`
|
||||||
|
- `Oracle_Database`
|
||||||
|
- `SQL_Data_Warehouses`
|
||||||
|
- `SQL_Database`
|
||||||
|
- `SQL_Elastic_Pools`
|
||||||
|
- `SQL_Managed_Instance`
|
||||||
|
- `SQL_Server`
|
||||||
|
- `SQL_Server_Registries`
|
||||||
|
- `SSIS_Lift_And_Shift_IR`
|
||||||
|
- `Virtual_Clusters`
|
||||||
|
|
||||||
|
### identity (35)
|
||||||
|
|
||||||
|
- `AAD_Licenses`
|
||||||
|
- `Active_Directory_Connect_Health`
|
||||||
|
- `Active_Directory_Connect_Health2`
|
||||||
|
- `Administrative_Units`
|
||||||
|
- `App_Registrations`
|
||||||
|
- `Azure_AD_B2C`
|
||||||
|
- `Azure_AD_B2C2`
|
||||||
|
- `Azure_AD_Domain_Services`
|
||||||
|
- `Azure_AD_Identity_Protection`
|
||||||
|
- `Azure_AD_Privilege_Identity_Management`
|
||||||
|
- `Azure_Active_Directory`
|
||||||
|
- `Azure_Information_Protection`
|
||||||
|
- `Custom_Azure_AD_Roles`
|
||||||
|
- `Enterprise_Applications`
|
||||||
|
- `Entra_Connect`
|
||||||
|
- `Entra_Domain_Services`
|
||||||
|
- `Entra_Global_Secure_Access`
|
||||||
|
- `Entra_ID_Protection`
|
||||||
|
- `Entra_Internet_Access`
|
||||||
|
- `Entra_Managed_Identities`
|
||||||
|
- `Entra_Private_Access`
|
||||||
|
- `Entra_Privileged_Identity_Management`
|
||||||
|
- `Entra_Verified_ID`
|
||||||
|
- `External_Identities`
|
||||||
|
- `Groups`
|
||||||
|
- `Identity_Governance`
|
||||||
|
- `Managed_Identities`
|
||||||
|
- `Multi_Factor_Authentication`
|
||||||
|
- `PIM`
|
||||||
|
- `Security`
|
||||||
|
- `Tenant_Properties`
|
||||||
|
- `User_Settings`
|
||||||
|
- `Users`
|
||||||
|
- `Verifiable_Credentials`
|
||||||
|
- `Verification_As_A_Service`
|
||||||
|
|
||||||
|
### networking (51)
|
||||||
|
|
||||||
|
- `ATM_Multistack`
|
||||||
|
- `Application_Gateway_Containers`
|
||||||
|
- `Application_Gateways`
|
||||||
|
- `Azure_Communications_Gateway`
|
||||||
|
- `Azure_Firewall_Manager`
|
||||||
|
- `Azure_Firewall_Policy`
|
||||||
|
- `Bastions`
|
||||||
|
- `CDN_Profiles`
|
||||||
|
- `Connections`
|
||||||
|
- `DDoS_Protection_Plans`
|
||||||
|
- `DNS_Multistack`
|
||||||
|
- `DNS_Private_Resolver`
|
||||||
|
- `DNS_Security_Policy`
|
||||||
|
- `DNS_Zones`
|
||||||
|
- `ExpressRoute_Circuits`
|
||||||
|
- `Firewalls`
|
||||||
|
- `Front_Doors`
|
||||||
|
- `IP_Address_manager`
|
||||||
|
- `IP_Groups`
|
||||||
|
- `Load_Balancer_Hub`
|
||||||
|
- `Load_Balancers`
|
||||||
|
- `Local_Network_Gateways`
|
||||||
|
- `NAT`
|
||||||
|
- `Network_Interfaces`
|
||||||
|
- `Network_Security_Groups`
|
||||||
|
- `Network_Watcher`
|
||||||
|
- `On_Premises_Data_Gateways`
|
||||||
|
- `Private_Endpoint`
|
||||||
|
- `Private_Link`
|
||||||
|
- `Private_Link_Hub`
|
||||||
|
- `Private_Link_Service`
|
||||||
|
- `Proximity_Placement_Groups`
|
||||||
|
- `Public_IP_Addresses`
|
||||||
|
- `Public_IP_Addresses_Classic`
|
||||||
|
- `Public_IP_Prefixes`
|
||||||
|
- `Reserved_IP_Addresses_Classic`
|
||||||
|
- `Resource_Management_Private_Link`
|
||||||
|
- `Route_Filters`
|
||||||
|
- `Route_Tables`
|
||||||
|
- `Service_Endpoint_Policies`
|
||||||
|
- `Spot_VM`
|
||||||
|
- `Spot_VMSS`
|
||||||
|
- `Subnet`
|
||||||
|
- `Traffic_Manager_Profiles`
|
||||||
|
- `Virtual_Network_Gateways`
|
||||||
|
- `Virtual_Networks`
|
||||||
|
- `Virtual_Networks_Classic`
|
||||||
|
- `Virtual_Router`
|
||||||
|
- `Virtual_WAN_Hub`
|
||||||
|
- `Virtual_WANs`
|
||||||
|
- `Web_Application_Firewall_Policies_WAF`
|
||||||
|
|
||||||
|
### security (14)
|
||||||
|
|
||||||
|
- `Application_Security_Groups`
|
||||||
|
- `Azure_AD_Risky_Signins`
|
||||||
|
- `Azure_AD_Risky_Users`
|
||||||
|
- `Azure_Defender`
|
||||||
|
- `Azure_Sentinel`
|
||||||
|
- `Conditional_Access`
|
||||||
|
- `Detonation`
|
||||||
|
- `ExtendedSecurityUpdates`
|
||||||
|
- `Identity_Secure_Score`
|
||||||
|
- `Key_Vaults`
|
||||||
|
- `Keys`
|
||||||
|
- `MS_Defender_EASM`
|
||||||
|
- `Multifactor_Authentication`
|
||||||
|
- `Security_Center`
|
||||||
|
|
||||||
|
### storage (17)
|
||||||
|
|
||||||
|
- `Azure_Fileshare`
|
||||||
|
- `Azure_HCP_Cache`
|
||||||
|
- `Azure_NetApp_Files`
|
||||||
|
- `Azure_Stack_Edge`
|
||||||
|
- `Data_Box`
|
||||||
|
- `Data_Box_Edge`
|
||||||
|
- `Data_Lake_Storage_Gen1`
|
||||||
|
- `Data_Share_Invitations`
|
||||||
|
- `Data_Shares`
|
||||||
|
- `Import_Export_Jobs`
|
||||||
|
- `Recovery_Services_Vaults`
|
||||||
|
- `StorSimple_Data_Managers`
|
||||||
|
- `StorSimple_Device_Managers`
|
||||||
|
- `Storage_Accounts`
|
||||||
|
- `Storage_Accounts_Classic`
|
||||||
|
- `Storage_Explorer`
|
||||||
|
- `Storage_Sync_Services`
|
||||||
|
|
||||||
|
### general (98)
|
||||||
|
|
||||||
|
- `All_Resources`
|
||||||
|
- `Backlog`
|
||||||
|
- `Biz_Talk`
|
||||||
|
- `Blob_Block`
|
||||||
|
- `Blob_Page`
|
||||||
|
- `Branch`
|
||||||
|
- `Browser`
|
||||||
|
- `Bug`
|
||||||
|
- `Builds`
|
||||||
|
- `Cache`
|
||||||
|
- `Code`
|
||||||
|
- `Commit`
|
||||||
|
- `Controls`
|
||||||
|
- `Controls_Horizontal`
|
||||||
|
- `Cost_Alerts`
|
||||||
|
- `Cost_Analysis`
|
||||||
|
- `Cost_Budgets`
|
||||||
|
- `Cost_Management`
|
||||||
|
- `Cost_Management_and_Billing`
|
||||||
|
- `Counter`
|
||||||
|
- `Cubes`
|
||||||
|
- `Dashboard`
|
||||||
|
- `Dashboard2`
|
||||||
|
- `Dev_Console`
|
||||||
|
- `Download`
|
||||||
|
- `Error`
|
||||||
|
- `Extensions`
|
||||||
|
- `FTP`
|
||||||
|
- `File`
|
||||||
|
- `Files`
|
||||||
|
- `Folder_Blank`
|
||||||
|
- `Folder_Website`
|
||||||
|
- `Free_Services`
|
||||||
|
- `Gear`
|
||||||
|
- `Globe`
|
||||||
|
- `Globe_Error`
|
||||||
|
- `Globe_Success`
|
||||||
|
- `Globe_Warning`
|
||||||
|
- `Guide`
|
||||||
|
- `Heart`
|
||||||
|
- `Help_and_Support`
|
||||||
|
- `Image`
|
||||||
|
- `Information`
|
||||||
|
- `Input_Output`
|
||||||
|
- `Journey_Hub`
|
||||||
|
- `Launch_Portal`
|
||||||
|
- `Learn`
|
||||||
|
- `Load_Test`
|
||||||
|
- `Location`
|
||||||
|
- `Log_Streaming`
|
||||||
|
- `Management_Groups`
|
||||||
|
- `Management_Portal`
|
||||||
|
- `Marketplace`
|
||||||
|
- `Media`
|
||||||
|
- `Media_File`
|
||||||
|
- `Mobile`
|
||||||
|
- `Mobile_Engagement`
|
||||||
|
- `Module`
|
||||||
|
- `Power`
|
||||||
|
- `Power_Up`
|
||||||
|
- `Powershell`
|
||||||
|
- `Preview`
|
||||||
|
- `Preview_Features`
|
||||||
|
- `Process_Explorer`
|
||||||
|
- `Production_Ready_Database`
|
||||||
|
- `Quickstart_Center`
|
||||||
|
- `Recent`
|
||||||
|
- `Reservations`
|
||||||
|
- `Resource_Explorer`
|
||||||
|
- `Resource_Group_List`
|
||||||
|
- `Resource_Groups`
|
||||||
|
- `Resource_Linked`
|
||||||
|
- `SSD`
|
||||||
|
- `Scale`
|
||||||
|
- `Scheduler`
|
||||||
|
- `Search`
|
||||||
|
- `Search_Grid`
|
||||||
|
- `Server_Farm`
|
||||||
|
- `Service_Bus`
|
||||||
|
- `Service_Health`
|
||||||
|
- `Storage_Azure_Files`
|
||||||
|
- `Storage_Container`
|
||||||
|
- `Storage_Queue`
|
||||||
|
- `Subscriptions`
|
||||||
|
- `TFS_VC_Repository`
|
||||||
|
- `Table`
|
||||||
|
- `Tag`
|
||||||
|
- `Tags`
|
||||||
|
- `Templates`
|
||||||
|
- `Toolbox`
|
||||||
|
- `Troubleshoot`
|
||||||
|
- `Versions`
|
||||||
|
- `Web_Slots`
|
||||||
|
- `Web_Test`
|
||||||
|
- `Website_Power`
|
||||||
|
- `Website_Staging`
|
||||||
|
- `Workbooks`
|
||||||
|
- `Workflow`
|
||||||
|
|
||||||
|
### other (149)
|
||||||
|
|
||||||
|
(See draw.io for complete list of 149 shapes in the "other" category)
|
||||||
|
|
||||||
|
Selected shapes:
|
||||||
|
- `Azure_Backup_Center`
|
||||||
|
- `Azure_Chaos_Studio`
|
||||||
|
- `Azure_Cloud_Shell`
|
||||||
|
- `Azure_Communication_Services`
|
||||||
|
- `Azure_Deployment_Environments`
|
||||||
|
- `Azure_Load_Testing`
|
||||||
|
- `Azure_Monitor_Dashboard`
|
||||||
|
- `Azure_Network_Manager`
|
||||||
|
- `Azure_Orbital`
|
||||||
|
- `Azure_Sphere`
|
||||||
|
- `Azure_Storage_Mover`
|
||||||
|
- `Grafana`
|
||||||
|
- `Kubernetes_Fleet_Manager`
|
||||||
|
- `SSH_Keys`
|
||||||
|
|
||||||
|
### Additional Categories
|
||||||
|
|
||||||
|
- **azure_ecosystem** (3): Applens, Azure_Hybrid_Center, Collaborative_Service
|
||||||
|
- **azure_stack** (8): Azure_Stack, Capacity, Infrastructure_Backup, Multi_Tenancy, Offers, Plans, Updates, User_Subscriptions
|
||||||
|
- **azure_vmware_solution** (1): AVS
|
||||||
|
- **blockchain** (6): ABS_Member, Azure_Blockchain_Service, Azure_Token_Service, Blockchain_Applications, Consortium, Outbound_Connection
|
||||||
|
- **cxp** (2): Elixir, Elixir_Purple
|
||||||
|
- **devops** (10): API_Connections, Application_Insights, Azure_DevOps, Change_Analysis, CloudTest, Code_Optimization, DevOps_Starter, DevTest_Labs, Lab_Accounts, Lab_Services
|
||||||
|
- **hybrid_multicloud** (5): Azure_Operator_5G_Core, Azure_Operator_Insights, Azure_Operator_Nexus, Azure_Operator_Service_Manager, Azure_Programmable_Connectivity
|
||||||
|
- **integration** (21): API_Management_Services, App_Configuration, Azure_API_for_FHIR, Azure_Data_Catalog, Event_Grid_Domains, Event_Grid_Subscriptions, Event_Grid_Topics, Integration_Accounts, Integration_Environments, Integration_Service_Environments, Logic_Apps, Logic_Apps_Custom_Connector, Partner_Namespace, Partner_Registration, Partner_Topic, Relays, SQL_Data_Warehouses, SendGrid_Accounts, Service_Bus, Software_as_a_Service, System_Topic
|
||||||
|
- **internet_of_things** (3): Digital_Twins, Logic_Apps, Time_Series_Insights_Access_Policies
|
||||||
|
- **intune** (17): Azure_AD_Roles_and_Administrators, Client_Apps, Device_Compliance, Device_Configuration, Device_Enrollment, Device_Security_Apple, Device_Security_Google, Device_Security_Windows, Devices, Exchange_Access, Intune, Intune_For_Education, Mindaro, Security_Baselines, Software_Updates, Tenant_Status, eBooks
|
||||||
|
- **iot** (19): Azure_IoT_Operations, Azure_Maps_Accounts, Azure_Stack_HCI_Sizer, Device_Provisioning_Services, Digital_Twins, Event_Hubs, Function_Apps, Industrial_IoT, IoT_Central_Applications, IoT_Edge, IoT_Hub, Logic_Apps, Notification_Hubs, Stack_HCI_Premium, Stream_Analytics_Jobs, Time_Series_Data_Sets, Time_Series_Insights_Environments, Time_Series_Insights_Event_Sources, Windows10_Core_Services
|
||||||
|
- **management_governance** (32): Activity_Log, Advisor, Alerts, Application_Insights, Arc_Machines, Automation_Accounts, Azure_Arc, Azure_Lighthouse, Blueprints, Compliance, Cost_Management_and_Billing, Customer_Lockbox_for_MS_Azure, Diagnostics_Settings, Education, Log_Analytics_Workspaces, MachinesAzureArc, Managed_Applications_Center, Managed_Desktop, Metrics, Monitor, My_Customers, Operation_Log_Classic, Policy, Recovery_Services_Vaults, Resource_Graph_Explorer, Resources_Provider, Scheduler_Job_Collections, Service_Catalog_MAD, Service_Providers, Solutions, Universal_Print, User_Privacy
|
||||||
|
- **menu** (1): Keys
|
||||||
|
- **migrate** (5): Azure_Migrate, Cost_Management_and_Billing, Data_Box, Data_Box_Edge, Recovery_Services_Vaults
|
||||||
|
- **mixed_reality** (2): Remote_Rendering, Spatial_Anchor_Accounts
|
||||||
|
- **monitor** (1): SAP_Azure_Monitor
|
||||||
|
- **power_platform** (9): AIBuilder, CopilotStudio, Dataverse, PowerApps, PowerAutomate, PowerBI, PowerFx, PowerPages, PowerPlatform
|
||||||
|
- **preview** (9): Azure_Cloud_Shell, Azure_Sphere, Azure_Workbooks, IoT_Edge, Private_Link_Hub, RTOS, Static_Apps, Time_Series_Data_Sets, Web_Environment
|
||||||
|
- **web** (5): API_Center, App_Space, Azure_Media_Service, Notification_Hub_Namespaces, SignalR
|
||||||
48
docs/shape-libraries/basic.md
Normal file
48
docs/shape-libraries/basic.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# basic
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.basic`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.basic.{shape};fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (31)
|
||||||
|
|
||||||
|
- `4_point_star`
|
||||||
|
- `6_point_star`
|
||||||
|
- `8_point_star`
|
||||||
|
- `banner`
|
||||||
|
- `cloud_callout`
|
||||||
|
- `cloud_rect`
|
||||||
|
- `cone`
|
||||||
|
- `cross`
|
||||||
|
- `document`
|
||||||
|
- `flash`
|
||||||
|
- `half_circle`
|
||||||
|
- `heart`
|
||||||
|
- `loud_callout`
|
||||||
|
- `moon`
|
||||||
|
- `mxgraph.basic`
|
||||||
|
- `no_symbol`
|
||||||
|
- `octagon`
|
||||||
|
- `orthogonal_triangle`
|
||||||
|
- `oval_callout`
|
||||||
|
- `parallelepiped`
|
||||||
|
- `pentagon`
|
||||||
|
- `pointed_oval`
|
||||||
|
- `rectangular_callout`
|
||||||
|
- `rounded_rectangular_callout`
|
||||||
|
- `smiley`
|
||||||
|
- `star`
|
||||||
|
- `sun`
|
||||||
|
- `tick`
|
||||||
|
- `trapezoid`
|
||||||
|
- `wave`
|
||||||
|
- `x`
|
||||||
60
docs/shape-libraries/bpmn.md
Normal file
60
docs/shape-libraries/bpmn.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# bpmn
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.bpmn`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.bpmn.shape;symbol=message;outline=throwing;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `outline` - Event type: `start`, `end`, `catching`, `throwing`, `none`
|
||||||
|
- `symbol` - Icon inside: `message`, `timer`, `error`, `cancel`, `compensation`, `link`, `terminate`, `general`, `multiple`, `rule`
|
||||||
|
|
||||||
|
## Shapes (40)
|
||||||
|
|
||||||
|
- `ad_hoc`
|
||||||
|
- `business_rule_task`
|
||||||
|
- `cancel_end`
|
||||||
|
- `cancel_intermediate`
|
||||||
|
- `compensation`
|
||||||
|
- `compensation_end`
|
||||||
|
- `compensation_intermediate`
|
||||||
|
- `error_end`
|
||||||
|
- `error_intermediate`
|
||||||
|
- `gateway`
|
||||||
|
- `gateway_and`
|
||||||
|
- `gateway_complex`
|
||||||
|
- `gateway_or`
|
||||||
|
- `gateway_xor_(data)`
|
||||||
|
- `gateway_xor_(event)`
|
||||||
|
- `general_end`
|
||||||
|
- `general_intermediate`
|
||||||
|
- `general_start`
|
||||||
|
- `link_end`
|
||||||
|
- `link_intermediate`
|
||||||
|
- `link_start`
|
||||||
|
- `loop`
|
||||||
|
- `loop_marker`
|
||||||
|
- `manual_task`
|
||||||
|
- `message_end`
|
||||||
|
- `message_intermediate`
|
||||||
|
- `message_start`
|
||||||
|
- `multiple_end`
|
||||||
|
- `multiple_instances`
|
||||||
|
- `multiple_intermediate`
|
||||||
|
- `multiple_start`
|
||||||
|
- `mxgraph.bpmn`
|
||||||
|
- `rule_intermediate`
|
||||||
|
- `rule_start`
|
||||||
|
- `script_task`
|
||||||
|
- `service_task`
|
||||||
|
- `terminate`
|
||||||
|
- `timer_intermediate`
|
||||||
|
- `timer_start`
|
||||||
|
- `user_task`
|
||||||
71
docs/shape-libraries/cabinets.md
Normal file
71
docs/shape-libraries/cabinets.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# cabinets
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.cabinets`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.cabinets.{shape};" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (54)
|
||||||
|
|
||||||
|
- `auxiliary_contact_contactor_1_32a`
|
||||||
|
- `auxiliary_contact_contactor_32_125a`
|
||||||
|
- `cb_1p`
|
||||||
|
- `cb_1p_x10`
|
||||||
|
- `cb_2p`
|
||||||
|
- `cb_2p_x10`
|
||||||
|
- `cb_3p`
|
||||||
|
- `cb_3p_x5`
|
||||||
|
- `cb_4p`
|
||||||
|
- `cb_4p_x5`
|
||||||
|
- `cb_auxiliary_contact`
|
||||||
|
- `contactor_125_400a`
|
||||||
|
- `contactor_1_32a`
|
||||||
|
- `contactor_32_125a`
|
||||||
|
- `din_rail`
|
||||||
|
- `distribution_block_4p_125a_11_connections`
|
||||||
|
- `distribution_block_4p_125a_11_connections_2`
|
||||||
|
- `mccb_25_63a_3p`
|
||||||
|
- `mccb_25_63a_4p`
|
||||||
|
- `mccb_63_250a_3p`
|
||||||
|
- `mccb_63_250a_4p`
|
||||||
|
- `motor_cb_125_400a`
|
||||||
|
- `motor_cb_1_32a`
|
||||||
|
- `motor_cb_32_125a`
|
||||||
|
- `motor_protection_cb`
|
||||||
|
- `motor_starter_125_400a`
|
||||||
|
- `motor_starter_1_32a`
|
||||||
|
- `motor_starter_32_125a`
|
||||||
|
- `motorized_switch_3p`
|
||||||
|
- `motorized_switch_4p`
|
||||||
|
- `mxgraph.cabinets`
|
||||||
|
- `overcurrent_relay_125_400a`
|
||||||
|
- `overcurrent_relay_1_32a`
|
||||||
|
- `overcurrent_relay_32_125a`
|
||||||
|
- `plugin_relay_1`
|
||||||
|
- `plugin_relay_2`
|
||||||
|
- `residual_current_device_2p`
|
||||||
|
- `residual_current_device_4p`
|
||||||
|
- `surge_protection_1p`
|
||||||
|
- `surge_protection_2p`
|
||||||
|
- `surge_protection_3p`
|
||||||
|
- `surge_protection_4p`
|
||||||
|
- `terminal_40mm2`
|
||||||
|
- `terminal_40mm2_x10`
|
||||||
|
- `terminal_4_6mm2`
|
||||||
|
- `terminal_4_6mm2_x10`
|
||||||
|
- `terminal_4mm2`
|
||||||
|
- `terminal_4mm2_x10`
|
||||||
|
- `terminal_50mm2`
|
||||||
|
- `terminal_50mm2_x10`
|
||||||
|
- `terminal_6_25mm2`
|
||||||
|
- `terminal_6_25mm2_x10`
|
||||||
|
- `terminal_75mm2`
|
||||||
|
- `terminal_75mm2_x10`
|
||||||
250
docs/shape-libraries/cisco19.md
Normal file
250
docs/shape-libraries/cisco19.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# cisco19
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.cisco19`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.cisco19.rect;prIcon={shape};fillColor=#00bceb;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (233)
|
||||||
|
|
||||||
|
- `3g_4g_indicator`
|
||||||
|
- `6500_vss`
|
||||||
|
- `6500_vss2`
|
||||||
|
- `access_control_and_trustsec`
|
||||||
|
- `aci`
|
||||||
|
- `aci2`
|
||||||
|
- `acibg`
|
||||||
|
- `acs`
|
||||||
|
- `ad_decoder`
|
||||||
|
- `ad_encoder`
|
||||||
|
- `analysis_correlation`
|
||||||
|
- `anomaly_detection`
|
||||||
|
- `anti_malware`
|
||||||
|
- `anti_malware2`
|
||||||
|
- `appnav`
|
||||||
|
- `asa_5500`
|
||||||
|
- `asr_1000`
|
||||||
|
- `asr_9000`
|
||||||
|
- `avc_application_visibility_control`
|
||||||
|
- `avc_application_visibility_control2`
|
||||||
|
- `bg1`
|
||||||
|
- `bg10`
|
||||||
|
- `bg2`
|
||||||
|
- `bg3`
|
||||||
|
- `bg4`
|
||||||
|
- `bg5`
|
||||||
|
- `bg6`
|
||||||
|
- `bg7`
|
||||||
|
- `bg8`
|
||||||
|
- `bg9`
|
||||||
|
- `blade_server`
|
||||||
|
- `branch`
|
||||||
|
- `branch2`
|
||||||
|
- `camera`
|
||||||
|
- `camera2`
|
||||||
|
- `cell_phone`
|
||||||
|
- `cell_phone2`
|
||||||
|
- `cisco_15800`
|
||||||
|
- `cisco_dna`
|
||||||
|
- `cisco_dna_center`
|
||||||
|
- `cisco_meetingplace_express`
|
||||||
|
- `cisco_security_manager`
|
||||||
|
- `cisco_unified_contact_center_enterprise_and_hosted`
|
||||||
|
- `cisco_unified_presence_service`
|
||||||
|
- `clock`
|
||||||
|
- `cloud`
|
||||||
|
- `cloud2`
|
||||||
|
- `cognitive`
|
||||||
|
- `collab1`
|
||||||
|
- `collab2`
|
||||||
|
- `collab3`
|
||||||
|
- `collab4`
|
||||||
|
- `communications_manager`
|
||||||
|
- `contact_center_express`
|
||||||
|
- `content_recording_streaming_server`
|
||||||
|
- `content_router`
|
||||||
|
- `csr_1000v`
|
||||||
|
- `da_decoder`
|
||||||
|
- `da_encoder`
|
||||||
|
- `data_center`
|
||||||
|
- `data_center2`
|
||||||
|
- `database_relational`
|
||||||
|
- `dns_server`
|
||||||
|
- `dns_server2`
|
||||||
|
- `dual_mode_access_point`
|
||||||
|
- `email_security`
|
||||||
|
- `fabric_interconnect`
|
||||||
|
- `fibre_channel_director_mds_9000`
|
||||||
|
- `fibre_channel_fabric_switch`
|
||||||
|
- `firewall`
|
||||||
|
- `flow_analytics`
|
||||||
|
- `flow_analytics2`
|
||||||
|
- `flow_collector`
|
||||||
|
- `h323`
|
||||||
|
- `handheld`
|
||||||
|
- `handheld2`
|
||||||
|
- `hdtv`
|
||||||
|
- `hdtv2`
|
||||||
|
- `home_office`
|
||||||
|
- `home_office2`
|
||||||
|
- `host_based_security`
|
||||||
|
- `hypervisor`
|
||||||
|
- `immersive_telepresence_endpoint`
|
||||||
|
- `ip_ip_gateway`
|
||||||
|
- `ip_phone`
|
||||||
|
- `ip_phone2`
|
||||||
|
- `ip_telephone_router`
|
||||||
|
- `ips_ids`
|
||||||
|
- `ironport`
|
||||||
|
- `ise`
|
||||||
|
- `joystick_keyboard`
|
||||||
|
- `joystick_keyboard2`
|
||||||
|
- `key`
|
||||||
|
- `key2`
|
||||||
|
- `l2_modular`
|
||||||
|
- `l2_modular2`
|
||||||
|
- `l2_switch`
|
||||||
|
- `l2_switch_with_dual_supervisor`
|
||||||
|
- `l3_modular`
|
||||||
|
- `l3_modular2`
|
||||||
|
- `l3_modular3`
|
||||||
|
- `l3_switch`
|
||||||
|
- `l3_switch_with_dual_supervisor`
|
||||||
|
- `laptop`
|
||||||
|
- `laptop2`
|
||||||
|
- `laptop_video_client`
|
||||||
|
- `laptop_video_client2`
|
||||||
|
- `layer3_nexus_5k_switch`
|
||||||
|
- `ldap`
|
||||||
|
- `ldap2`
|
||||||
|
- `load_balancer`
|
||||||
|
- `lock`
|
||||||
|
- `lock2`
|
||||||
|
- `media_server`
|
||||||
|
- `meeting_scheduling_and_management_server`
|
||||||
|
- `mesh_access_point`
|
||||||
|
- `monitor`
|
||||||
|
- `monitoring`
|
||||||
|
- `multipoint_meeting_server`
|
||||||
|
- `mxgraph.cisco19`
|
||||||
|
- `nac_appliance`
|
||||||
|
- `nam_virtual_service_blade`
|
||||||
|
- `net_mgmt_appliance`
|
||||||
|
- `netflow_router`
|
||||||
|
- `netflow_router2`
|
||||||
|
- `netflow_router3`
|
||||||
|
- `next_generation_intrusion_prevention_system`
|
||||||
|
- `nexus_1010`
|
||||||
|
- `nexus_1k`
|
||||||
|
- `nexus_1kv_vsm`
|
||||||
|
- `nexus_2000_10ge`
|
||||||
|
- `nexus_2k`
|
||||||
|
- `nexus_3k`
|
||||||
|
- `nexus_4k`
|
||||||
|
- `nexus_5k`
|
||||||
|
- `nexus_5k_with_integrated_vsm`
|
||||||
|
- `nexus_7k`
|
||||||
|
- `nexus_9300`
|
||||||
|
- `nexus_9500`
|
||||||
|
- `operations_manager`
|
||||||
|
- `phone_polycom`
|
||||||
|
- `phone_polycom2`
|
||||||
|
- `policy_configuration`
|
||||||
|
- `pos`
|
||||||
|
- `pos2`
|
||||||
|
- `posture_assessment`
|
||||||
|
- `primary_codec`
|
||||||
|
- `printer`
|
||||||
|
- `printer2`
|
||||||
|
- `router`
|
||||||
|
- `router_with_firewall`
|
||||||
|
- `router_with_firewall2`
|
||||||
|
- `router_with_voice`
|
||||||
|
- `rps`
|
||||||
|
- `secondary_codec`
|
||||||
|
- `secure_catalyst_switch_color`
|
||||||
|
- `secure_catalyst_switch_color2`
|
||||||
|
- `secure_catalyst_switch_color3`
|
||||||
|
- `secure_catalyst_switch_subdued`
|
||||||
|
- `secure_catalyst_switch_subdued2`
|
||||||
|
- `secure_endpoint_pc`
|
||||||
|
- `secure_endpoint_pc2`
|
||||||
|
- `secure_endpoints`
|
||||||
|
- `secure_endpoints2`
|
||||||
|
- `secure_router`
|
||||||
|
- `secure_server`
|
||||||
|
- `secure_server2`
|
||||||
|
- `secure_switch`
|
||||||
|
- `security_management`
|
||||||
|
- `server`
|
||||||
|
- `server2`
|
||||||
|
- `service_ready_engine`
|
||||||
|
- `set_top`
|
||||||
|
- `set_top2`
|
||||||
|
- `shield`
|
||||||
|
- `ssl_terminator`
|
||||||
|
- `stealthwatch_management_console_smc`
|
||||||
|
- `stealthwatch_management_console_smc2`
|
||||||
|
- `storage`
|
||||||
|
- `surveillance_camera`
|
||||||
|
- `surveillance_camera2`
|
||||||
|
- `tablet`
|
||||||
|
- `tablet2`
|
||||||
|
- `telepresence_endpoint`
|
||||||
|
- `telepresence_endpoint_twin_data_display`
|
||||||
|
- `telepresence_exchange`
|
||||||
|
- `threat_intelligence`
|
||||||
|
- `transcoder`
|
||||||
|
- `ucs_5108_blade_chassis`
|
||||||
|
- `ucs_c_series_server`
|
||||||
|
- `ucs_express`
|
||||||
|
- `unity`
|
||||||
|
- `upc_unified_personal_communicator`
|
||||||
|
- `upc_unified_personal_communicator2`
|
||||||
|
- `ups`
|
||||||
|
- `user`
|
||||||
|
- `user2`
|
||||||
|
- `vbond`
|
||||||
|
- `video_analytics`
|
||||||
|
- `video_call_server`
|
||||||
|
- `video_gateway`
|
||||||
|
- `virtual_desktop_service`
|
||||||
|
- `virtual_matrix_switch`
|
||||||
|
- `virtual_private_network`
|
||||||
|
- `virtual_private_network2`
|
||||||
|
- `virtual_private_network_connector`
|
||||||
|
- `vmanage`
|
||||||
|
- `vpn_concentrator`
|
||||||
|
- `vsmart`
|
||||||
|
- `vts`
|
||||||
|
- `vts2`
|
||||||
|
- `web_application_firewall`
|
||||||
|
- `web_reputation_filtering`
|
||||||
|
- `web_reputation_filtering_2`
|
||||||
|
- `web_security`
|
||||||
|
- `web_security_services`
|
||||||
|
- `web_security_services2`
|
||||||
|
- `webex`
|
||||||
|
- `wifi_indicator`
|
||||||
|
- `wireless_access_point`
|
||||||
|
- `wireless_access_point2`
|
||||||
|
- `wireless_bridge`
|
||||||
|
- `wireless_bridge2`
|
||||||
|
- `wireless_connector`
|
||||||
|
- `wireless_intrusion_prevention`
|
||||||
|
- `wireless_lan_controller`
|
||||||
|
- `wireless_location_appliance`
|
||||||
|
- `wireless_router`
|
||||||
|
- `workgroup_switch`
|
||||||
|
- `workstation`
|
||||||
|
- `workstation2`
|
||||||
|
- `x509_certificate`
|
||||||
|
- `x509_certificate2`
|
||||||
115
docs/shape-libraries/citrix.md
Normal file
115
docs/shape-libraries/citrix.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# citrix
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.citrix`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.citrix.{shape};fillColor=#00A4E4;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (98)
|
||||||
|
|
||||||
|
- `1u_2u_server`
|
||||||
|
- `access_card`
|
||||||
|
- `branch_repeater`
|
||||||
|
- `browser`
|
||||||
|
- `cache_server`
|
||||||
|
- `calendar`
|
||||||
|
- `cell_phone`
|
||||||
|
- `chassis`
|
||||||
|
- `citrix_hdx`
|
||||||
|
- `citrix_logo`
|
||||||
|
- `cloud`
|
||||||
|
- `command_center`
|
||||||
|
- `database`
|
||||||
|
- `database_server`
|
||||||
|
- `datacenter`
|
||||||
|
- `desktop`
|
||||||
|
- `desktop_web`
|
||||||
|
- `dhcp_server`
|
||||||
|
- `directory_server`
|
||||||
|
- `dns_server`
|
||||||
|
- `document`
|
||||||
|
- `edgesight_server`
|
||||||
|
- `file_server`
|
||||||
|
- `firewall`
|
||||||
|
- `ftp_server`
|
||||||
|
- `geolocation_database`
|
||||||
|
- `globe`
|
||||||
|
- `goto_meeting`
|
||||||
|
- `government`
|
||||||
|
- `home_office`
|
||||||
|
- `hq_enterprise`
|
||||||
|
- `inspection`
|
||||||
|
- `ip_phone`
|
||||||
|
- `kiosk`
|
||||||
|
- `laptop_1`
|
||||||
|
- `laptop_2`
|
||||||
|
- `license_server`
|
||||||
|
- `merchandising_server`
|
||||||
|
- `middleware`
|
||||||
|
- `mxgraph.citrix`
|
||||||
|
- `netscaler_gateway`
|
||||||
|
- `netscaler_mpx`
|
||||||
|
- `netscaler_sdx`
|
||||||
|
- `netscaler_vpx`
|
||||||
|
- `pbx_server`
|
||||||
|
- `pda`
|
||||||
|
- `podio`
|
||||||
|
- `printer`
|
||||||
|
- `process`
|
||||||
|
- `provisioning_server`
|
||||||
|
- `proxy_server`
|
||||||
|
- `radius_server`
|
||||||
|
- `remote_office`
|
||||||
|
- `reporting`
|
||||||
|
- `role_appcontroller`
|
||||||
|
- `role_applications`
|
||||||
|
- `role_cloudbridge`
|
||||||
|
- `role_desktops`
|
||||||
|
- `role_load_testing_controller`
|
||||||
|
- `role_load_testing_launcher`
|
||||||
|
- `role_receiver`
|
||||||
|
- `role_repeater`
|
||||||
|
- `role_secure_access`
|
||||||
|
- `role_security`
|
||||||
|
- `role_services`
|
||||||
|
- `role_storefront`
|
||||||
|
- `role_storefront_services`
|
||||||
|
- `role_synchronizer`
|
||||||
|
- `role_xenmobile`
|
||||||
|
- `role_xenmobile_device_manager`
|
||||||
|
- `router`
|
||||||
|
- `security`
|
||||||
|
- `sharefile`
|
||||||
|
- `site`
|
||||||
|
- `smtp_server`
|
||||||
|
- `storefront_services`
|
||||||
|
- `switch`
|
||||||
|
- `tablet_1`
|
||||||
|
- `tablet_2`
|
||||||
|
- `thin_client`
|
||||||
|
- `tower_server`
|
||||||
|
- `user_control`
|
||||||
|
- `users`
|
||||||
|
- `web_server`
|
||||||
|
- `web_service`
|
||||||
|
- `worxenroll`
|
||||||
|
- `worxhome`
|
||||||
|
- `worxmail`
|
||||||
|
- `worxweb`
|
||||||
|
- `xenapp_server`
|
||||||
|
- `xenapp_services`
|
||||||
|
- `xenapp_web`
|
||||||
|
- `xencenter`
|
||||||
|
- `xenclient`
|
||||||
|
- `xenclient_synchronizer`
|
||||||
|
- `xendesktop_server`
|
||||||
|
- `xenmobile`
|
||||||
|
- `xenserver`
|
||||||
50
docs/shape-libraries/electrical.md
Normal file
50
docs/shape-libraries/electrical.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# electrical
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.electrical`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.electrical.resistors.resistor_1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
Shapes are organized by category: `mxgraph.electrical.{category}.{shape}`
|
||||||
|
|
||||||
|
## Categories
|
||||||
|
|
||||||
|
### resistors
|
||||||
|
- `resistor_1`
|
||||||
|
- `resistor_2`
|
||||||
|
|
||||||
|
### capacitors
|
||||||
|
- `capacitor_1`
|
||||||
|
- `capacitor_3`
|
||||||
|
|
||||||
|
### inductors
|
||||||
|
- `inductor_3`
|
||||||
|
- `transformer_1`
|
||||||
|
|
||||||
|
### diodes
|
||||||
|
- `diode`
|
||||||
|
- `zener_diode_1`
|
||||||
|
|
||||||
|
### transistors
|
||||||
|
- `npn_transistor_1`
|
||||||
|
- `pnp_transistor_1`
|
||||||
|
|
||||||
|
### mosfets1
|
||||||
|
- `n-channel_mosfet_1`
|
||||||
|
- `p-channel_mosfet_1`
|
||||||
|
|
||||||
|
### logic_gates
|
||||||
|
- `logic_gate`
|
||||||
|
- `dual_inline_ic`
|
||||||
|
|
||||||
|
### electro-mechanical
|
||||||
|
- `singleSwitch`
|
||||||
|
- `pushbutton`
|
||||||
|
|
||||||
|
(See draw.io Electrical shape library for complete list)
|
||||||
62
docs/shape-libraries/floorplan.md
Normal file
62
docs/shape-libraries/floorplan.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# floorplan
|
||||||
|
|
||||||
|
**Type:** mxgraph shapes
|
||||||
|
**Prefix:** `mxgraph.floorplan`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mxCell value="label" style="shape=mxgraph.floorplan.{shape};fillColor=#ffffff;strokeColor=#000000;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Shapes (45)
|
||||||
|
|
||||||
|
- `bathtub`
|
||||||
|
- `bathtub2`
|
||||||
|
- `bed_double`
|
||||||
|
- `bed_single`
|
||||||
|
- `bookcase`
|
||||||
|
- `chair`
|
||||||
|
- `copier`
|
||||||
|
- `couch`
|
||||||
|
- `crt_tv`
|
||||||
|
- `desk_corner`
|
||||||
|
- `desk_corner_2`
|
||||||
|
- `dresser`
|
||||||
|
- `drying_machine`
|
||||||
|
- `elevator`
|
||||||
|
- `fireplace`
|
||||||
|
- `flat_tv`
|
||||||
|
- `floor_lamp`
|
||||||
|
- `laptop`
|
||||||
|
- `mxgraph.floorplan`
|
||||||
|
- `office_chair`
|
||||||
|
- `piano`
|
||||||
|
- `plant`
|
||||||
|
- `printer`
|
||||||
|
- `range_1`
|
||||||
|
- `range_2`
|
||||||
|
- `refrigerator`
|
||||||
|
- `shower`
|
||||||
|
- `shower2`
|
||||||
|
- `sink_1`
|
||||||
|
- `sink_2`
|
||||||
|
- `sink_22`
|
||||||
|
- `sink_double`
|
||||||
|
- `sink_double2`
|
||||||
|
- `sofa`
|
||||||
|
- `spiral_stairs`
|
||||||
|
- `table`
|
||||||
|
- `table_1`
|
||||||
|
- `table_2`
|
||||||
|
- `table_3`
|
||||||
|
- `table_4`
|
||||||
|
- `table_5`
|
||||||
|
- `toilet`
|
||||||
|
- `washing_machine`
|
||||||
|
- `water_cooler`
|
||||||
|
- `workstation`
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user