Showing preview only (1,635K chars total). Download the full file or copy to clipboard to get everything.
Repository: DayuanJiang/next-ai-draw-io
Branch: main
Commit: 43cc4cb65761
Files: 242
Total size: 1.5 MB
Directory structure:
gitextract_moyqnob7/
├── .dockerignore
├── .eslintrc.json
├── .github/
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ ├── enhancement.md
│ │ └── feature_request.md
│ ├── renovate.json
│ └── workflows/
│ ├── auto-format.yml
│ ├── ci.yml
│ ├── docker-build.yml
│ ├── electron-release.yml
│ └── test.yml
├── .gitignore
├── .husky/
│ ├── pre-commit
│ └── pre-push
├── .vscode/
│ └── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── app/
│ ├── [lang]/
│ │ ├── about/
│ │ │ ├── cn/
│ │ │ │ └── page.tsx
│ │ │ ├── ja/
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/
│ │ ├── chat/
│ │ │ ├── route.ts
│ │ │ └── xml_guide.md
│ │ ├── config/
│ │ │ └── route.ts
│ │ ├── log-feedback/
│ │ │ └── route.ts
│ │ ├── log-save/
│ │ │ └── route.ts
│ │ ├── parse-url/
│ │ │ └── route.ts
│ │ ├── server-models/
│ │ │ └── route.ts
│ │ ├── validate-diagram/
│ │ │ └── route.ts
│ │ ├── validate-model/
│ │ │ └── route.ts
│ │ └── verify-access-code/
│ │ └── route.ts
│ ├── globals.css
│ ├── manifest.ts
│ ├── robots.ts
│ └── sitemap.ts
├── biome.json
├── components/
│ ├── ai-elements/
│ │ ├── model-selector.tsx
│ │ ├── reasoning.tsx
│ │ └── shimmer.tsx
│ ├── button-with-tooltip.tsx
│ ├── chat/
│ │ ├── ChatLobby.tsx
│ │ ├── ToolCallCard.tsx
│ │ ├── ValidationCard.tsx
│ │ └── types.ts
│ ├── chat-example-panel.tsx
│ ├── chat-input.tsx
│ ├── chat-message-display.tsx
│ ├── chat-panel.tsx
│ ├── code-block.tsx
│ ├── dev-xml-simulator.tsx
│ ├── error-toast.tsx
│ ├── file-preview-list.tsx
│ ├── history-dialog.tsx
│ ├── image-with-basepath.tsx
│ ├── model-config-dialog.tsx
│ ├── model-selector.tsx
│ ├── quota-limit-toast.tsx
│ ├── reset-warning-modal.tsx
│ ├── save-dialog.tsx
│ ├── settings-dialog.tsx
│ ├── ui/
│ │ ├── alert-dialog.tsx
│ │ ├── button.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── switch.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
│ └── url-input-dialog.tsx
├── components.json
├── contexts/
│ └── diagram-context.tsx
├── docker-compose.yml
├── docs/
│ ├── cn/
│ │ ├── FAQ.md
│ │ ├── README_CN.md
│ │ ├── ai-providers.md
│ │ ├── cloudflare-deploy.md
│ │ ├── docker.md
│ │ └── offline-deployment.md
│ ├── en/
│ │ ├── FAQ.md
│ │ ├── ai-providers.md
│ │ ├── cloudflare-deploy.md
│ │ ├── docker.md
│ │ └── offline-deployment.md
│ ├── ja/
│ │ ├── FAQ.md
│ │ ├── README_JA.md
│ │ ├── ai-providers.md
│ │ ├── cloudflare-deploy.md
│ │ ├── docker.md
│ │ └── offline-deployment.md
│ └── shape-libraries/
│ ├── README.md
│ ├── alibaba_cloud.md
│ ├── android.md
│ ├── arrows2.md
│ ├── atlassian.md
│ ├── aws4.md
│ ├── azure2.md
│ ├── basic.md
│ ├── bpmn.md
│ ├── cabinets.md
│ ├── cisco19.md
│ ├── citrix.md
│ ├── electrical.md
│ ├── floorplan.md
│ ├── flowchart.md
│ ├── fluidpower.md
│ ├── gcp2.md
│ ├── infographic.md
│ ├── kubernetes.md
│ ├── lean_mapping.md
│ ├── material_design.md
│ ├── mscae.md
│ ├── network.md
│ ├── openstack.md
│ ├── pid.md
│ ├── rack.md
│ ├── salesforce.md
│ ├── sap.md
│ ├── sitemap.md
│ ├── vvd.md
│ └── webicons.md
├── edge-functions/
│ └── api/
│ └── edgeai/
│ └── chat/
│ └── completions.ts
├── edgeone.json
├── electron/
│ ├── electron-builder.yml
│ ├── electron.d.ts
│ ├── main/
│ │ ├── app-menu.ts
│ │ ├── config-manager.ts
│ │ ├── env-loader.ts
│ │ ├── index.ts
│ │ ├── ipc-handlers.ts
│ │ ├── menu-i18n.ts
│ │ ├── next-server.ts
│ │ ├── port-manager.ts
│ │ ├── proxy-manager.ts
│ │ ├── settings-window.ts
│ │ └── window-manager.ts
│ ├── preload/
│ │ ├── index.ts
│ │ └── settings.ts
│ ├── settings/
│ │ ├── index.html
│ │ ├── settings.css
│ │ └── settings.js
│ └── tsconfig.json
├── env.example
├── hooks/
│ ├── use-diagram-tool-handlers.ts
│ ├── use-dictionary.ts
│ ├── use-model-config.ts
│ ├── use-session-manager.ts
│ └── use-validate-diagram.ts
├── instrumentation.ts
├── lib/
│ ├── ai-providers.ts
│ ├── base-path.ts
│ ├── cached-responses.ts
│ ├── chat-helpers.ts
│ ├── diagram-validator.ts
│ ├── dynamo-quota-manager.ts
│ ├── i18n/
│ │ ├── config.ts
│ │ ├── dictionaries/
│ │ │ ├── en.json
│ │ │ ├── ja.json
│ │ │ ├── zh-Hant.json
│ │ │ └── zh.json
│ │ ├── dictionaries.ts
│ │ └── utils.ts
│ ├── langfuse.ts
│ ├── pdf-utils.ts
│ ├── server-model-config.ts
│ ├── session-storage.ts
│ ├── ssrf-protection.ts
│ ├── storage.ts
│ ├── system-prompts.ts
│ ├── types/
│ │ └── model-config.ts
│ ├── url-utils.ts
│ ├── use-file-processor.tsx
│ ├── use-quota-manager.tsx
│ ├── user-id.ts
│ ├── utils.ts
│ ├── validation-prompts.ts
│ └── validation-schema.ts
├── next.config.ts
├── open-next.config.ts
├── package.json
├── packages/
│ ├── claude-plugin/
│ │ ├── .claude-plugin/
│ │ │ └── plugin.json
│ │ ├── .mcp.json
│ │ └── README.md
│ └── mcp-server/
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── diagram-operations.ts
│ │ ├── history.ts
│ │ ├── http-server.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ └── xml-validation.ts
│ └── tsconfig.json
├── playwright.config.ts
├── postcss.config.mjs
├── proxy.ts
├── public/
│ ├── _headers
│ └── chain-of-thought.txt
├── resources/
│ └── entitlements.mac.plist
├── scripts/
│ ├── afterPack.cjs
│ ├── electron-dev.mjs
│ ├── prepare-electron-build.mjs
│ └── test-diagram-operations.mjs
├── tests/
│ ├── e2e/
│ │ ├── chat.spec.ts
│ │ ├── copy-paste.spec.ts
│ │ ├── diagram-generation.spec.ts
│ │ ├── error-handling.spec.ts
│ │ ├── file-upload.spec.ts
│ │ ├── fixtures/
│ │ │ └── diagrams.ts
│ │ ├── history-restore.spec.ts
│ │ ├── history.spec.ts
│ │ ├── iframe-interaction.spec.ts
│ │ ├── keyboard.spec.ts
│ │ ├── language.spec.ts
│ │ ├── lib/
│ │ │ ├── fixtures.ts
│ │ │ └── helpers.ts
│ │ ├── model-config.spec.ts
│ │ ├── multi-turn.spec.ts
│ │ ├── save.spec.ts
│ │ ├── settings.spec.ts
│ │ ├── smoke.spec.ts
│ │ ├── theme.spec.ts
│ │ └── upload.spec.ts
│ └── unit/
│ ├── ai-providers.test.ts
│ ├── cached-responses.test.ts
│ ├── chat-helpers.test.ts
│ ├── diagram-validator.test.ts
│ ├── server-model-config.test.ts
│ └── utils.test.ts
├── tsconfig.json
├── vercel.json
├── vitest.config.mts
└── wrangler.jsonc
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# 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
================================================
FILE: .eslintrc.json
================================================
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
================================================
FILE: .github/CONTRIBUTING.md
================================================
# 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)
```
Git hooks via Husky run automatically:
- **Pre-commit**: Biome (format/lint) + TypeScript type check
- **Pre-push**: Unit tests
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.
## Testing
Run tests before submitting PRs:
```bash
npm run test # Unit tests (Vitest)
npm run test:e2e # E2E tests (Playwright)
```
E2E tests use mocked API responses - no AI provider needed. Tests are in `tests/e2e/`.
To run a specific test file:
```bash
npx playwright test tests/e2e/diagram-generation.spec.ts
```
To run tests with UI mode:
```bash
npx playwright test --ui
```
## Before You Start
For **significant changes** (new features, architecture changes, large refactors, etc.), please **open an issue first** to discuss your proposal before writing code. This helps avoid wasted effort and ensures alignment with the project direction. Small bug fixes and minor improvements can go straight to a PR.
## Pull Requests
1. Create a feature branch
2. Make changes (pre-commit runs lint + type check automatically)
3. Run E2E tests with `npm run test:e2e`
4. Push (pre-push runs unit tests automatically)
5. Submit PR against `main` with a clear description
CI will run the full test suite on your PR.
## Code Review
This project uses GitHub Copilot for automated code review. If you receive review comments from Copilot on your PR:
- **Valid suggestions**: Please address them in your code.
- **Invalid or irrelevant suggestions**: Feel free to click "Resolve" to dismiss them.
## Issues
Include steps to reproduce, expected vs actual behavior, and AI provider used.
================================================
FILE: .github/FUNDING.yml
================================================
# 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']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
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.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
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
================================================
FILE: .github/ISSUE_TEMPLATE/enhancement.md
================================================
---
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.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
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.
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["after 10am on the first day of the month"],
"timezone": "Asia/Tokyo",
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchPackagePatterns": ["*"],
"groupName": "minor and patch dependencies",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"matchPackagePatterns": ["*"],
"groupName": "major dependencies",
"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
}
}
================================================
FILE: .github/workflows/auto-format.yml
================================================
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 }}
================================================
FILE: .github/workflows/ci.yml
================================================
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
================================================
FILE: .github/workflows/docker-build.yml
================================================
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
================================================
FILE: .github/workflows/electron-release.yml
================================================
name: Electron Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version tag (e.g., v0.4.5)"
required: false
jobs:
# Mac and Linux: Build and publish directly (no signing needed)
build-mac-linux:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: mac
- 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: Download draw.io static files for offline use
run: |
rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF
rm -rf public/drawio/META-INF
- name: Install dependencies
run: npm install
- name: Build and publish
run: npm run dist:${{ matrix.platform }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Windows: Build, sign with SignPath, then publish
build-windows:
permissions:
contents: write
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: "npm"
- name: Download draw.io static files for offline use
shell: bash
run: |
rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF
rm -rf public/drawio/META-INF
- name: Install dependencies
run: npm install
# Build WITHOUT publishing
- name: Build Windows app
run: npm run dist:win:build
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload unsigned artifacts for signing
uses: actions/upload-artifact@v6
id: upload-unsigned
with:
name: windows-unsigned
path: release/*.exe
retention-days: 1
- name: Sign with SignPath
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: '880a211d-2cd3-4e7b-8d04-3d1f8eb39df5'
project-slug: 'next-ai-draw-io'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: 'windows-exe'
github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: release-signed
- name: Upload signed artifacts to release
uses: softprops/action-gh-release@v2
with:
files: release-signed/*.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint-and-unit:
name: Lint & Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run check
- name: Run unit tests
run: npm run test -- --run
e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v5
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium --with-deps
- name: Install Playwright deps (cached)
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium
- name: Build app
run: npm run build
- name: Run E2E tests
run: npm run test:e2e
env:
CI: true
- name: Upload test results
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
packages/*/node_modules
packages/*/dist
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
/playwright-report/
/test-results/
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
push-via-ec2.sh
.claude/
.playwright-mcp/
# Cloudflare
.dev.vars
.open-next/
.wrangler/
.env*.local
# Electron
/dist-electron/
/release/
/electron-standalone/
# Draw.io static files (downloaded during CI build)
public/drawio/
*.dmg
*.exe
*.AppImage
*.deb
*.rpm
*.snap
CLAUDE.md
.spec-workflow
# edgeone
.edgeone
opencode.json
ai-models.json
# local backups
*.bak
================================================
FILE: .husky/pre-commit
================================================
npx lint-staged
npx tsc --noEmit
================================================
FILE: .husky/pre-push
================================================
# Skip if node_modules not installed (e.g., on EC2 push server)
if [ -d "node_modules" ]; then
npm run test -- --run
fi
================================================
FILE: .vscode/settings.json
================================================
{
"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"
}
}
================================================
FILE: Dockerfile
================================================
# 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
ARG ELECTRON_SKIP_BINARY_DOWNLOAD=1
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-time argument for subdirectory deployment (e.g., /nextaidrawio)
ARG NEXT_PUBLIC_BASE_PATH=""
ENV NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH}
# Control sponsorship and self-hosting messaging in quota notifications.
# Set NEXT_PUBLIC_SELFHOSTED="true" in self-hosted deployments to hide sponsorship/self-host links and related text in quota popups.
ARG NEXT_PUBLIC_SELFHOSTED=""
ENV NEXT_PUBLIC_SELFHOSTED="${NEXT_PUBLIC_SELFHOSTED}"
# 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"]
================================================
FILE: LICENSE
================================================
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.
================================================
FILE: README.md
================================================
# Next AI Draw.io
<div align="center">
**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**
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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio) sponsorship, the demo site now uses the powerful glm-4.7 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](#deploy-on-vercel)
- [Deploy on Cloudflare Workers](#deploy-on-cloudflare-workers)
- [Multi-Provider Support](#multi-provider-support)
- [How It Works](#how-it-works)
- [Support \& Contact](#support--contact)
- [FAQ](#faq)
- [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
- **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
- **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.
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
## MCP Server (Preview)
> **Preview Feature**: This feature is experimental and may not be stable.
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).
```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
```
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
### 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
1. Clone the repository:
```bash
git clone https://github.com/DayuanJiang/next-ai-draw-io
cd next-ai-draw-io
npm install
cp env.example .env.local
```
See the [Provider Configuration Guide](./docs/en/ai-providers.md) for detailed setup instructions for each provider.
2. Run the development server:
```bash
npm run dev
```
3. Open [http://localhost:6002](http://localhost:6002) in your browser to see the application.
## Deployment
### Deploy to EdgeOne Pages
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
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
The easiest way to deploy is using [Vercel](https://vercel.com/new), the creators of Next.js. Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
### Deploy on Cloudflare Workers
[Go to Cloudflare Deploy Guide](./docs/en/cloudflare-deploy.md)
## Multi-Provider Support
- [ByteDance Doubao](https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio)
- AWS Bedrock (default)
- OpenAI
- Anthropic
- Google AI
- Google Vertex AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
- SiliconFlow
- ModelScope
- 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.
### Server-Side Multi-Model Configuration
Administrators can configure multiple server-side models that are available to all users without requiring personal API keys. Configure via `AI_MODELS_CONFIG` environment variable (JSON string) or `ai-models.json` file.
**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
**Special thanks to [ByteDance Doubao](https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio) 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:
- Email: me[at]jiang.jp
## FAQ
See [FAQ](./docs/en/FAQ.md) for common issues and solutions.
## Star History
[](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
---
================================================
FILE: app/[lang]/about/cn/page.tsx
================================================
import type { Metadata } from "next"
import Link from "next/link"
import { FaGithub } from "react-icons/fa"
import Image from "@/components/image-with-basepath"
export const metadata: Metadata = {
title: "关于 - Next AI Draw.io",
description:
"AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
}
export default function AboutCN() {
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-xl font-bold text-gray-900 hover:text-gray-700"
>
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
编辑器
</Link>
<Link
href="/about/cn"
className="text-blue-600 font-semibold"
>
关于
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="在GitHub上查看"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Next AI Draw.io
</h1>
<p className="text-xl text-gray-600 font-medium">
AI驱动的图表创建工具 - 对话、绘制、可视化
</p>
</div>
<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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-blue-600 hover:underline"
>
字节跳动豆包
</a>
的慷慨赞助,演示站点现已接入强大的{" "}
<span className="font-semibold text-amber-700">
glm-4.7
</span>{" "}
模型,图表生成效果更佳!点击链接注册即可领取{" "}
<span className="font-semibold text-amber-700">
50万免费Token
</span>
,适用于所有模型!
</p>
</div>
{/* Invite Poster */}
<div className="text-center mb-5">
<a
href="https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="/volcengine-invite.png"
alt="火山引擎方舟 Coding Plan"
width={300}
height={400}
className="mx-auto rounded-lg"
/>
</a>
</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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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>Google Vertex AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
<li>SiliconFlow</li>
<li>ModelScope</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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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>
)
}
================================================
FILE: app/[lang]/about/ja/page.tsx
================================================
import type { Metadata } from "next"
import Link from "next/link"
import { FaGithub } from "react-icons/fa"
import Image from "@/components/image-with-basepath"
export const metadata: Metadata = {
title: "概要 - Next AI Draw.io",
description:
"AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
keywords: [
"AIダイアグラム",
"draw.io",
"AWSアーキテクチャ",
"GCPダイアグラム",
"Azureダイアグラム",
"LLM",
],
}
export default function AboutJA() {
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-xl font-bold text-gray-900 hover:text-gray-700"
>
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
エディタ
</Link>
<Link
href="/about/ja"
className="text-blue-600 font-semibold"
>
概要
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="GitHubで見る"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Next AI Draw.io
</h1>
<p className="text-xl text-gray-600 font-medium">
AI搭載のダイアグラム作成ツール -
チャット、描画、可視化
</p>
</div>
<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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-blue-600 hover:underline"
>
ByteDance Doubao
</a>
様のご支援により、デモサイトでは強力な{" "}
<span className="font-semibold text-amber-700">
glm-4.7
</span>{" "}
モデルを利用できるようになり、より高品質なダイアグラム生成が可能になりました。リンクから登録すると、すべてのモデルで使える{" "}
<span className="font-semibold text-amber-700">
50万トークン
</span>
が無料でもらえます!
</p>
</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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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>Google Vertex AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
<li>SiliconFlow</li>
<li>ModelScope</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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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>
)
}
================================================
FILE: app/[lang]/about/page.tsx
================================================
import type { Metadata } from "next"
import Link from "next/link"
import { FaGithub } from "react-icons/fa"
import Image from "@/components/image-with-basepath"
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",
],
}
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-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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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">
glm-4.7
</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>
{/* 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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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>Google Vertex AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
<li>SiliconFlow</li>
<li>ModelScope</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://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=Z9Z3LDTJ&utm_campaign=drawio&utm_content=drawio&utm_medium=devrel&utm_source=OWO&utm_term=drawio"
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>
)
}
================================================
FILE: app/[lang]/layout.tsx
================================================
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, "zh-Hant": 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",
"zh-Hant": "Next AI Draw.io - AI 驅動的圖表產生器",
}
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.",
"zh-Hant":
"使用 AI 建立 AWS 架構圖、流程圖和技術圖表。免費線上工具整合 draw.io 與 AI 輔助,輕鬆建立專業圖表。",
}
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 === "zh-Hant"
? "zh_HK"
: 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",
"zh-Hant": "/zh-Hant",
},
},
}
}
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>
)
}
================================================
FILE: app/[lang]/page.tsx
================================================
"use client"
import { usePathname, useRouter } from "next/navigation"
import { Suspense, 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 {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context"
import { i18n, type Locale } from "@/lib/i18n/config"
export default function Home() {
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
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 [isDrawioReady, setIsDrawioReady] = useState(false)
const [isElectron, setIsElectron] = useState(false)
const [drawioBaseUrl, setDrawioBaseUrl] = useState(
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net",
)
const chatPanelRef = useRef<ImperativePanelHandle>(null)
const isMobileRef = useRef(false)
// 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)
}
// Detect Electron and use bundled draw.io files for offline use
// Note: react-drawio uses `new URL(baseUrl)` so we need absolute URL
// Include /index.html because Next.js doesn't auto-serve index.html for directories
const electronDetected =
!process.env.NEXT_PUBLIC_DRAWIO_BASE_URL &&
!!(window as unknown as { electronAPI?: unknown }).electronAPI
if (electronDetected) {
setIsElectron(true)
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
}
setIsLoaded(true)
}, [pathname, router])
const handleDrawioLoad = useCallback(() => {
setIsDrawioReady(true)
onDrawioLoad()
}, [onDrawioLoad])
const handleDarkModeChange = () => {
const newValue = !darkMode
setDarkMode(newValue)
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
document.documentElement.classList.toggle("dark", newValue)
setIsDrawioReady(false)
resetDrawioReady()
}
const handleDrawioUiChange = () => {
const newUi = drawioUi === "min" ? "sketch" : "min"
localStorage.setItem("drawio-theme", newUi)
setDrawioUi(newUi)
setIsDrawioReady(false)
resetDrawioReady()
}
// Check mobile - reset draw.io before crossing breakpoint
const isInitialRenderRef = useRef(true)
useEffect(() => {
const checkMobile = () => {
const newIsMobile = window.innerWidth < 768
if (
!isInitialRenderRef.current &&
newIsMobile !== isMobileRef.current
) {
setIsDrawioReady(false)
resetDrawioReady()
}
isMobileRef.current = newIsMobile
isInitialRenderRef.current = false
setIsMobile(newIsMobile)
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [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)
}, [])
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"
}`}
>
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 relative">
{isLoaded && (
<div
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
>
<DrawIoEmbed
key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
ref={drawioRef}
onExport={handleDiagramExport}
onLoad={handleDrawioLoad}
baseUrl={drawioBaseUrl}
urlParameters={{
ui: drawioUi,
spin: false,
libraries: false,
saveAndExit: false,
noSaveBtn: true,
noExitBtn: true,
dark: darkMode,
lang: currentLang,
// Enable offline mode in Electron to disable external service calls
...(isElectron && {
offline: true,
}),
}}
/>
</div>
)}
{(!isLoaded || !isDrawioReady) && (
<div className="h-full w-full bg-background flex items-center justify-center">
<span className="text-muted-foreground">
Draw.io panel is loading...
</span>
</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"}`}>
<Suspense
fallback={
<div className="h-full bg-card rounded-xl border border-border/30 flex items-center justify-center text-muted-foreground">
Loading chat...
</div>
}
>
<ChatPanel
isVisible={isChatVisible}
onToggleVisibility={toggleChatPanel}
drawioUi={drawioUi}
onToggleDrawioUi={handleDrawioUiChange}
darkMode={darkMode}
onToggleDarkMode={handleDarkModeChange}
isMobile={isMobile}
/>
</Suspense>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
)
}
================================================
FILE: app/api/chat/route.ts
================================================
import {
APICallError,
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
InvalidToolInputError,
LoadAPIKeyError,
stepCountIs,
streamText,
} from "ai"
import fs from "fs/promises"
import { jsonrepair } from "jsonrepair"
import path from "path"
import { z } from "zod"
import {
getAIModel,
SINGLE_SYSTEM_PROVIDERS,
supportsImageInput,
supportsPromptCaching,
} from "@/lib/ai-providers"
import { findCachedResponse } from "@/lib/cached-responses"
import {
isMinimalDiagram,
replaceHistoricalToolInputs,
validateFileParts,
} from "@/lib/chat-helpers"
import {
checkAndIncrementRequest,
isQuotaEnabled,
recordTokenUsage,
} from "@/lib/dynamo-quota-manager"
import {
getTelemetryConfig,
setTraceInput,
setTraceOutput,
wrapWithObserve,
} from "@/lib/langfuse"
import { findServerModelById } from "@/lib/server-model-config"
import { getSystemPrompt } from "@/lib/system-prompts"
import { getUserIdFromRequest } from "@/lib/user-id"
export const maxDuration = 120
// Helper function to create cached stream response
function createCachedStreamResponse(xml: string): Response {
const toolCallId = `cached-${Date.now()}`
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.write({ type: "start" })
writer.write({
type: "tool-input-start",
toolCallId,
toolName: "display_diagram",
})
writer.write({
type: "tool-input-delta",
toolCallId,
inputTextDelta: xml,
})
writer.write({
type: "tool-input-available",
toolCallId,
toolName: "display_diagram",
input: { xml },
})
writer.write({ type: "finish" })
},
})
return createUIMessageStreamResponse({ stream })
}
// Inner handler function
async function handleChatRequest(req: Request): Promise<Response> {
// Check for access code
const accessCodes =
process.env.ACCESS_CODE_LIST?.split(",")
.map((code) => code.trim())
.filter(Boolean) || []
if (accessCodes.length > 0) {
const accessCodeHeader = req.headers.get("x-access-code")
if (!accessCodeHeader || !accessCodes.includes(accessCodeHeader)) {
return Response.json(
{
error: "Invalid or missing access code. Please configure it in Settings.",
},
{ status: 401 },
)
}
}
const body = await req.json()
const { messages, xml, previousXml, sessionId } = body
const customSystemMessage =
typeof body.customSystemMessage === "string"
? body.customSystemMessage.slice(0, 5000)
: ""
// Get user ID for Langfuse tracking and quota
const userId = getUserIdFromRequest(req)
// Validate sessionId for Langfuse (must be string, max 200 chars)
const validSessionId =
sessionId && typeof sessionId === "string" && sessionId.length <= 200
? sessionId
: undefined
// Extract user input text for Langfuse trace
// Find the last USER message, not just the last message (which could be assistant in multi-step tool flows)
const lastUserMessage = [...messages]
.reverse()
.find((m: any) => m.role === "user")
const userInputText =
lastUserMessage?.parts?.find((p: any) => p.type === "text")?.text || ""
// Update Langfuse trace with input, session, and user
setTraceInput({
input: userInputText,
sessionId: validSessionId,
userId: userId,
})
// === SERVER-SIDE QUOTA CHECK START ===
// Quota is opt-in: only enabled when DYNAMODB_QUOTA_TABLE env var is set
const hasOwnApiKey = !!(
req.headers.get("x-ai-provider") &&
(req.headers.get("x-ai-api-key") ||
req.headers.get("x-aws-access-key-id") ||
req.headers.get("x-vertex-api-key"))
)
// Skip quota check if: quota disabled, user has own API key, or is anonymous
if (isQuotaEnabled() && !hasOwnApiKey && userId !== "anonymous") {
const quotaCheck = await checkAndIncrementRequest(userId, {
requests: Number(process.env.DAILY_REQUEST_LIMIT) || 10,
tokens: Number(process.env.DAILY_TOKEN_LIMIT) || 200000,
tpm: Number(process.env.TPM_LIMIT) || 20000,
})
if (!quotaCheck.allowed) {
return Response.json(
{
error: quotaCheck.error,
type: quotaCheck.type,
used: quotaCheck.used,
limit: quotaCheck.limit,
},
{ status: 429 },
)
}
}
// === SERVER-SIDE QUOTA CHECK END ===
// === FILE VALIDATION START ===
const fileValidation = validateFileParts(messages)
if (!fileValidation.valid) {
return Response.json({ error: fileValidation.error }, { status: 400 })
}
// === FILE VALIDATION END ===
// === CACHE CHECK START ===
const isFirstMessage = messages.length === 1
const isEmptyDiagram = !xml || xml.trim() === "" || isMinimalDiagram(xml)
if (isFirstMessage && isEmptyDiagram) {
const lastMessage = messages[0]
const textPart = lastMessage.parts?.find((p: any) => p.type === "text")
const filePart = lastMessage.parts?.find((p: any) => p.type === "file")
const cached = findCachedResponse(textPart?.text || "", !!filePart)
if (cached) {
return createCachedStreamResponse(cached.xml)
}
}
// === CACHE CHECK END ===
// Read client AI provider overrides from headers
const provider = req.headers.get("x-ai-provider")
let baseUrl = req.headers.get("x-ai-base-url")
const selectedModelId = req.headers.get("x-selected-model-id")
// For EdgeOne provider, construct full URL from request origin
// because createOpenAI needs absolute URL, not relative path
if (provider === "edgeone" && !baseUrl) {
const origin = req.headers.get("origin") || new URL(req.url).origin
baseUrl = `${origin}/api/edgeai`
}
// Get cookie header for EdgeOne authentication (eo_token, eo_time)
const cookieHeader = req.headers.get("cookie")
// Check if this is a server model with custom env var names
let serverModelConfig: {
apiKeyEnv?: string | string[]
baseUrlEnv?: string
provider?: string
} = {}
if (selectedModelId?.startsWith("server:")) {
const serverModel = await findServerModelById(selectedModelId)
console.log(
`[Server Model Lookup] ID: ${selectedModelId}, Found: ${!!serverModel}, Provider: ${serverModel?.provider}`,
)
if (serverModel) {
serverModelConfig = {
apiKeyEnv: serverModel.apiKeyEnv,
baseUrlEnv: serverModel.baseUrlEnv,
// Use actual provider from config (client header may have incorrect value due to ID format change)
provider: serverModel.provider,
}
}
}
const clientOverrides = {
// Server model provider takes precedence over client header
provider: serverModelConfig.provider || provider,
baseUrl,
apiKey: req.headers.get("x-ai-api-key"),
modelId: req.headers.get("x-ai-model"),
// AWS Bedrock credentials
awsAccessKeyId: req.headers.get("x-aws-access-key-id"),
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
awsRegion: req.headers.get("x-aws-region"),
awsSessionToken: req.headers.get("x-aws-session-token"),
// Server model custom env var names
...serverModelConfig,
// Vertex AI credentials (Express Mode)
vertexApiKey: req.headers.get("x-vertex-api-key"),
// Pass cookies for EdgeOne Pages authentication
...(provider === "edgeone" &&
cookieHeader && {
headers: { cookie: cookieHeader },
}),
}
// Read minimal style preference from header
const minimalStyle = req.headers.get("x-minimal-style") === "true"
console.log(
`[Client Overrides] provider: ${clientOverrides.provider}, modelId: ${clientOverrides.modelId}`,
)
// Get AI model with optional client overrides
const {
model,
providerOptions,
headers,
modelId,
provider: resolvedProvider,
} = getAIModel(clientOverrides)
// Check if model supports prompt caching
const shouldCache = supportsPromptCaching(modelId)
console.log(
`[Prompt Caching] ${shouldCache ? "ENABLED" : "DISABLED"} for model: ${modelId}`,
)
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
const systemMessage = getSystemPrompt(modelId, minimalStyle)
const finalSystemMessage = customSystemMessage
? `${systemMessage}\n\n## Custom Instructions\n${customSystemMessage}`
: systemMessage
// Extract file parts (images) from the last user message
const fileParts =
lastUserMessage?.parts?.filter((part: any) => part.type === "file") ||
[]
// Check if user is sending images to a model that doesn't support them
// AI SDK silently drops unsupported parts, so we need to catch this early
if (fileParts.length > 0 && !supportsImageInput(modelId)) {
return Response.json(
{
error: `The model "${modelId}" does not support image input. Please use a vision-capable model (e.g., GPT-4o, Claude, Gemini) or remove the image.`,
},
{ status: 400 },
)
}
// User input only - XML is now in a separate cached system message
const formattedUserInput = `User input:
"""md
${userInputText}
"""`
// Convert UIMessages to ModelMessages and add system message
const modelMessages = await convertToModelMessages(messages)
// DEBUG: Log incoming messages structure
console.log("[route.ts] Incoming messages count:", messages.length)
messages.forEach((msg: any, idx: number) => {
console.log(
`[route.ts] Message ${idx} role:`,
msg.role,
"parts count:",
msg.parts?.length,
)
if (msg.parts) {
msg.parts.forEach((part: any, partIdx: number) => {
if (
part.type === "tool-invocation" ||
part.type === "tool-result"
) {
console.log(`[route.ts] Part ${partIdx}:`, {
type: part.type,
toolName: part.toolName,
hasInput: !!part.input,
inputType: typeof part.input,
inputKeys:
part.input && typeof part.input === "object"
? Object.keys(part.input)
: null,
})
}
})
}
})
// Replace historical tool call XML with placeholders to reduce tokens
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
const enableHistoryReplace =
process.env.ENABLE_HISTORY_XML_REPLACE === "true"
const placeholderMessages = enableHistoryReplace
? replaceHistoricalToolInputs(modelMessages)
: modelMessages
// Filter out messages with empty content arrays (Bedrock API rejects these)
// This is a safety measure - ideally convertToModelMessages should handle all cases
let enhancedMessages = placeholderMessages.filter(
(msg: any) =>
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
)
// Filter out tool-calls with invalid inputs (from failed repair or interrupted streaming)
// Bedrock API rejects messages where toolUse.input is not a valid JSON object
enhancedMessages = enhancedMessages
.map((msg: any) => {
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
return msg
}
const filteredContent = msg.content.filter((part: any) => {
if (part.type === "tool-call") {
// Check if input is a valid object (not null, undefined, or empty)
if (
!part.input ||
typeof part.input !== "object" ||
Object.keys(part.input).length === 0
) {
console.warn(
`[route.ts] Filtering out tool-call with invalid input:`,
{ toolName: part.toolName, input: part.input },
)
return false
}
}
return true
})
return { ...msg, content: filteredContent }
})
.filter((msg: any) => msg.content && msg.content.length > 0)
// DEBUG: Log modelMessages structure (what's being sent to AI)
console.log("[route.ts] Model messages count:", enhancedMessages.length)
enhancedMessages.forEach((msg: any, idx: number) => {
console.log(
`[route.ts] ModelMsg ${idx} role:`,
msg.role,
"content count:",
msg.content?.length,
)
if (msg.content) {
msg.content.forEach((part: any, partIdx: number) => {
if (part.type === "tool-call" || part.type === "tool-result") {
console.log(`[route.ts] Content ${partIdx}:`, {
type: part.type,
toolName: part.toolName,
hasInput: !!part.input,
inputType: typeof part.input,
inputValue:
part.input === undefined
? "undefined"
: part.input === null
? "null"
: "object",
})
}
})
}
})
// Update the last message with user input only (XML moved to separate cached system message)
if (enhancedMessages.length >= 1) {
const lastModelMessage = enhancedMessages[enhancedMessages.length - 1]
if (lastModelMessage.role === "user") {
// Build content array with user input text and file parts
const contentParts: any[] = [
{ type: "text", text: formattedUserInput },
]
// Add image parts back
for (const filePart of fileParts) {
contentParts.push({
type: "image",
image: filePart.url,
mimeType: filePart.mediaType,
})
}
enhancedMessages = [
...enhancedMessages.slice(0, -1),
{ ...lastModelMessage, content: contentParts },
]
}
}
// Add cache point to the last assistant message in conversation history
// This caches the entire conversation prefix for subsequent requests
// Strategy: system (cached) + history with last assistant (cached) + new user message
if (shouldCache && enhancedMessages.length >= 2) {
// Find the last assistant message (should be second-to-last, before current user message)
for (let i = enhancedMessages.length - 2; i >= 0; i--) {
if (enhancedMessages[i].role === "assistant") {
enhancedMessages[i] = {
...enhancedMessages[i],
providerOptions: {
bedrock: { cachePoint: { type: "default" } },
},
}
break // Only cache the last assistant message
}
}
}
// System messages with multiple cache breakpoints for optimal caching:
// - Breakpoint 1: System instructions + custom instructions - changes when user updates custom system message
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
// Some providers (e.g. MiniMax) don't support multiple system messages
// Merge them into a single system message for compatibility
const isSingleSystemProvider = SINGLE_SYSTEM_PROVIDERS.has(resolvedProvider)
const xmlContext = `${
previousXml
? `Previous diagram XML (before user's last message):
"""xml
${previousXml}
"""
`
: ""
}Current diagram XML (AUTHORITATIVE - the source of truth):
"""xml
${xml || ""}
"""
IMPORTANT: The "Current diagram XML" is the SINGLE SOURCE OF TRUTH for what's on the canvas right now. The user can manually add, delete, or modify shapes directly in draw.io. Always count and describe elements based on the CURRENT XML, not on what you previously generated. If both previous and current XML are shown, compare them to understand what the user changed. When using edit_diagram, COPY search patterns exactly from the CURRENT XML - attribute order matters!`
const systemMessages = isSingleSystemProvider
? [
{
role: "system" as const,
content: `${finalSystemMessage}\n\n${xmlContext}`,
},
]
: [
// Cache breakpoint 1: Instructions (+ optional custom instructions)
{
role: "system" as const,
content: finalSystemMessage,
...(shouldCache && {
providerOptions: {
bedrock: { cachePoint: { type: "default" } },
},
}),
},
// Cache breakpoint 2: Previous and Current diagram XML context
{
role: "system" as const,
content: xmlContext,
...(shouldCache && {
providerOptions: {
bedrock: { cachePoint: { type: "default" } },
},
}),
},
]
const allMessages = [...systemMessages, ...enhancedMessages]
const result = streamText({
model,
abortSignal: req.signal,
...(process.env.MAX_OUTPUT_TOKENS && {
maxOutputTokens: parseInt(process.env.MAX_OUTPUT_TOKENS, 10),
}),
stopWhen: stepCountIs(5),
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
experimental_repairToolCall: async ({ toolCall, error }) => {
// DEBUG: Log what we're trying to repair
console.log(`[repairToolCall] Tool: ${toolCall.toolName}`)
console.log(
`[repairToolCall] Error: ${error.name} - ${error.message}`,
)
console.log(`[repairToolCall] Input type: ${typeof toolCall.input}`)
console.log(`[repairToolCall] Input value:`, toolCall.input)
// Only attempt repair for invalid tool input (broken JSON from truncation)
if (
error instanceof InvalidToolInputError ||
error.name === "AI_InvalidToolInputError"
) {
try {
// Pre-process to fix common LLM JSON errors that jsonrepair can't handle
let inputToRepair = toolCall.input
if (typeof inputToRepair === "string") {
// Fix `:=` instead of `: ` (LLM sometimes generates this)
inputToRepair = inputToRepair.replace(/:=/g, ": ")
// Fix `= "` instead of `: "`
inputToRepair = inputToRepair.replace(/=\s*"/g, ': "')
// Fix inconsistent quote escaping in XML attributes within JSON strings
// Pattern: attribute="value\" where opening quote is unescaped but closing is escaped
// Example: y="-20\" should be y=\"-20\"
inputToRepair = inputToRepair.replace(
/(\w+)="([^"]*?)\\"/g,
'$1=\\"$2\\"',
)
}
// Use jsonrepair to fix truncated JSON
const repairedInput = jsonrepair(inputToRepair)
console.log(
`[repairToolCall] Repaired truncated JSON for tool: ${toolCall.toolName}`,
)
return { ...toolCall, input: repairedInput }
} catch (repairError) {
console.warn(
`[repairToolCall] Failed to repair JSON for tool: ${toolCall.toolName}`,
repairError,
)
// Return a placeholder input to avoid API errors in multi-step
// The tool will fail gracefully on client side
if (toolCall.toolName === "edit_diagram") {
return {
...toolCall,
input: {
operations: [],
_error: "JSON repair failed - no operations to apply",
},
}
}
if (toolCall.toolName === "display_diagram") {
return {
...toolCall,
input: {
xml: "",
_error: "JSON repair failed - empty diagram",
},
}
}
return null
}
}
// Don't attempt to repair other errors (like NoSuchToolError)
return null
},
messages: allMessages,
...(providerOptions && { providerOptions }), // This now includes all reasoning configs
...(headers && { headers }),
// Langfuse telemetry config (returns undefined if not configured)
...(getTelemetryConfig({ sessionId: validSessionId, userId }) && {
experimental_telemetry: getTelemetryConfig({
sessionId: validSessionId,
userId,
}),
}),
onFinish: ({ text, totalUsage }) => {
// AI SDK 6 telemetry auto-reports token usage on its spans
setTraceOutput(text)
// Record token usage for server-side quota tracking (if enabled)
// Use totalUsage (cumulative across all steps) instead of usage (final step only)
// Include all 4 token types: input, output, cache read, cache write
if (
isQuotaEnabled() &&
!hasOwnApiKey &&
userId !== "anonymous" &&
totalUsage
) {
const totalTokens =
(totalUsage.inputTokens || 0) +
(totalUsage.outputTokens || 0) +
(totalUsage.cachedInputTokens || 0) +
(totalUsage.inputTokenDetails?.cacheWriteTokens || 0)
recordTokenUsage(userId, totalTokens)
}
},
tools: {
// Client-side tool that will be executed on the client
display_diagram: {
description: `Display a diagram on draw.io. Pass ONLY the mxCell elements - wrapper tags and root cells are added automatically.
VALIDATION RULES (XML will be rejected if violated):
1. Generate ONLY mxCell elements - NO wrapper tags (<mxfile>, <mxGraphModel>, <root>)
2. Do NOT include root cells (id="0" or id="1") - they are added automatically
3. All mxCell elements must be siblings - never nested
4. Every mxCell needs a unique id (start from "2")
5. Every mxCell needs a valid parent attribute (use "1" for top-level)
6. Escape special chars in values: < > & "
Example (generate ONLY this - no wrapper tags):
<mxCell id="lane1" value="Frontend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step1" value="Step 1" style="rounded=1;" vertex="1" parent="lane1">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="lane2" value="Backend" style="swimlane;" vertex="1" parent="1">
<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>
</mxCell>
<mxCell id="step2" value="Step 2" style="rounded=1;" vertex="1" parent="lane2">
<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
Notes:
- For AWS diagrams, use **AWS 2025 icons**.
- For animated connectors, add "flowAnimation=1" to edge style.
`,
inputSchema: z.object({
xml: z
.string()
.describe("XML string to be displayed on draw.io"),
}),
},
edit_diagram: {
description: `Edit the current diagram by ID-based operations (update/add/delete cells).
Operations:
- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.
- add: Add a new cell. Provide cell_id (new unique id) and new_xml.
- delete: Remove a cell. Cascade is automatic: children AND edges (source/target) are auto-deleted. Only specify ONE cell_id.
For update/add, new_xml must be a complete mxCell element including mxGeometry.
⚠️ JSON ESCAPING: Every " inside new_xml MUST be escaped as \\". Example: id=\\"5\\" value=\\"Label\\"
Example - Add a rectangle:
{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}
Example - Delete container (children & edges auto-deleted):
{"operations": [{"operation": "delete", "cell_id": "2"}]}`,
inputSchema: z.object({
operations: z
.array(
z.object({
operation: z
.enum(["update", "add", "delete"])
.describe(
"Operation to perform: add, update, or delete",
),
cell_id: z
.string()
.describe(
"The id of the mxCell. Must match the id attribute in new_xml.",
),
new_xml: z
.string()
.optional()
.describe(
"Complete mxCell XML element (required for update/add)",
),
}),
)
.describe("Array of operations to apply"),
}),
},
append_diagram: {
description: `Continue generating diagram XML when previous display_diagram output was truncated due to length limits.
WHEN TO USE: Only call this tool after display_diagram was truncated (you'll see an error message about truncation).
CRITICAL INSTRUCTIONS:
1. Do NOT include any wrapper tags - just continue the mxCell elements
2. Continue from EXACTLY where your previous output stopped
3. Complete the remaining mxCell elements
4. If still truncated, call append_diagram again with the next fragment
Example: If previous output ended with '<mxCell id="x" style="rounded=1', continue with ';" vertex="1">...' and complete the remaining elements.`,
inputSchema: z.object({
xml: z
.string()
.describe(
"Continuation XML fragment to append (NO wrapper tags)",
),
}),
},
get_shape_library: {
description: `Get draw.io shape/icon library documentation with style syntax and shape names.
Available libraries:
- Cloud: aws4, azure2, gcp2, alibaba_cloud, openstack, salesforce
- Networking: cisco19, network, kubernetes, vvd, rack
- Business: bpmn, lean_mapping
- General: flowchart, basic, arrows2, infographic, sitemap
- UI/Mockups: android, material_design
- Enterprise: citrix, sap, mscae, atlassian
- Engineering: fluidpower, electrical, pid, cabinets, floorplan
- Icons: webicons
Call this tool to get shape names and usage syntax for a specific library.`,
inputSchema: z.object({
library: z
.string()
.describe(
"Library name (e.g., 'aws4', 'kubernetes', 'flowchart')",
),
}),
execute: async ({ library }) => {
// Sanitize input - prevent path traversal attacks
const sanitizedLibrary = library
.toLowerCase()
.replace(/[^a-z0-9_-]/g, "")
if (sanitizedLibrary !== library.toLowerCase()) {
return `Invalid library name "${library}". Use only letters, numbers, underscores, and hyphens.`
}
const baseDir = path.join(
process.cwd(),
"docs/shape-libraries",
)
const filePath = path.join(
baseDir,
`${sanitizedLibrary}.md`,
)
// Verify path stays within expected directory
const resolvedPath = path.resolve(filePath)
if (!resolvedPath.startsWith(path.resolve(baseDir))) {
return `Invalid library path.`
}
try {
const content = await fs.readFile(filePath, "utf-8")
return content
} catch (error) {
if (
(error as NodeJS.ErrnoException).code === "ENOENT"
) {
return `Library "${library}" not found. Available: aws4, azure2, gcp2, alibaba_cloud, cisco19, kubernetes, network, bpmn, flowchart, basic, arrows2, vvd, salesforce, citrix, sap, mscae, atlassian, fluidpower, electrical, pid, cabinets, floorplan, webicons, infographic, sitemap, android, material_design, lean_mapping, openstack, rack`
}
console.error(
`[get_shape_library] Error loading "${library}":`,
error,
)
return `Error loading library "${library}". Please try again.`
}
},
},
},
...(process.env.TEMPERATURE !== undefined && {
temperature: parseFloat(process.env.TEMPERATURE),
}),
})
return result.toUIMessageStreamResponse({
sendReasoning: true,
messageMetadata: ({ part }) => {
if (part.type === "finish") {
const usage = (part as any).totalUsage
// AI SDK 6 provides totalTokens directly
return {
totalTokens: usage?.totalTokens ?? 0,
finishReason: (part as any).finishReason,
}
}
return undefined
},
})
}
// Helper to categorize errors and return appropriate response
function handleError(error: unknown): Response {
console.error("Error in chat route:", error)
const isDev = process.env.NODE_ENV === "development"
// Check for specific AI SDK error types
if (APICallError.isInstance(error)) {
return Response.json(
{
error: error.message,
...(isDev && {
details: error.responseBody,
stack: error.stack,
}),
},
{ status: error.statusCode || 500 },
)
}
if (LoadAPIKeyError.isInstance(error)) {
return Response.json(
{
error: "Authentication failed. Please check your API key.",
...(isDev && {
stack: error.stack,
}),
},
{ status: 401 },
)
}
// Fallback for other errors with safety filter
const message =
error instanceof Error ? error.message : "An unexpected error occurred"
const status = (error as any)?.statusCode || (error as any)?.status || 500
// Prevent leaking API keys, tokens, or other sensitive data
const lowerMessage = message.toLowerCase()
const safeMessage =
lowerMessage.includes("key") ||
lowerMessage.includes("token") ||
lowerMessage.includes("sig") ||
lowerMessage.includes("signature") ||
lowerMessage.includes("secret") ||
lowerMessage.includes("password") ||
lowerMessage.includes("credential")
? "Authentication failed. Please check your credentials."
: message
return Response.json(
{
error: safeMessage,
...(isDev && {
details: message,
stack: error instanceof Error ? error.stack : undefined,
}),
},
{ status },
)
}
// Wrap handler with error handling
async function safeHandler(req: Request): Promise<Response> {
try {
return await handleChatRequest(req)
} catch (error) {
return handleError(error)
}
}
// Wrap with Langfuse observe (if configured)
const observedHandler = wrapWithObserve(safeHandler)
export async function POST(req: Request) {
return observedHandler(req)
}
================================================
FILE: app/api/chat/xml_guide.md
================================================
# Draw.io XML Schema Guide
This guide explains the structure of draw.io (diagrams.net) XML files to help you understand and create diagrams programmatically.
## Basic Structure
A draw.io XML file has the following hierarchy:
```xml
<mxfile>
<diagram>
<mxGraphModel>
<root>
<mxCell /> <!-- Cells that make up the diagram -->
</root>
</mxGraphModel>
</diagram>
</mxfile>
```
## Root Element: `<mxfile>`
The root element of a draw.io file.
**Attributes:**
- `host`: The application that created the file (e.g., "app.diagrams.net")
- `modified`: Last modification timestamp
- `agent`: Browser / user agent information
- `version`: Version of the application
- `type`: File type (usually "device" or "google")
**Example:**
```xml
<mxfile host="app.diagrams.net" modified="2023-07-14T10:20:30.123Z" agent="Mozilla/5.0" version="21.5.2" type="device">
```
## Diagram Element: `<diagram>`
Each page in your draw.io document is represented by a `<diagram>` element.
**Attributes:**
- `id`: Unique identifier for the diagram
- `name`: The name of the diagram / page
**Example:**
```xml
<diagram id="pWHN0msd4Ud1ZK5cD-Hr" name="Page-1">
```
## Graph Model: `<mxGraphModel>`
Contains the actual diagram data.
**Attributes:**
- `dx`: Grid size in x-direction (usually 1)
- `dy`: Grid size in y-direction (usually 1)
- `grid`: Whether grid is enabled (0 or 1)
- `gridSize`: Grid cell size (usually 10)
- `guides`: Whether guides are enabled (0 or 1)
- `tooltips`: Whether tooltips are enabled (0 or 1)
- `connect`: Whether connections are enabled (0 or 1)
- `arrows`: Whether arrows are enabled (0 or 1)
- `fold`: Whether folding is enabled (0 or 1)
- `page`: Whether page view is enabled (0 or 1)
- `pageScale`: Scale of the page (usually 1)
- `pageWidth`: Width of the page (e.g., 850)
- `pageHeight`: Height of the page (e.g., 1100)
- `math`: Whether math typesetting is enabled (0 or 1)
- `shadow`: Whether shadows are enabled (0 or 1)
**Example:**
```xml
<mxGraphModel dx="1" dy="1" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
```
## Root Cell Container: `<root>`
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.
**Internal structure (auto-generated):**
```xml
<root>
<mxCell id="0"/> <!-- Auto-added -->
<mxCell id="1" parent="0"/> <!-- Auto-added -->
<!-- Your mxCell elements go here (start from id="2") -->
</root>
```
## Cell Elements: `<mxCell>`
The basic building block of diagrams. Cells represent shapes, connectors, text, etc.
**Attributes for all cells:**
- `id`: Unique identifier for the cell
- `parent`: ID of the parent cell (typically "1" for most cells)
- `value`: Text content of the cell
- `style`: Styling information (see Style section below)
**Attributes for shapes (vertices):**
- `vertex`: Set to "1" for shapes
- `connectable`: Whether the shape can be connected (0 or 1)
**Attributes for connectors (edges):**
- `edge`: Set to "1" for connectors
- `source`: ID of the source cell
- `target`: ID of the target cell
**Example (Rectangle shape):**
```xml
<mxCell id="2" value="Hello World" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="350" y="190" width="120" height="60" as="geometry"/>
</mxCell>
```
**Example (Connector):**
```xml
<mxCell id="3" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="2" target="4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="400" y="430" as="sourcePoint"/>
<mxPoint x="450" y="380" as="targetPoint"/>
</mxGeometry>
</mxCell>
```
## Geometry: `<mxGeometry>`
Defines the position and dimensions of cells.
**Attributes for shapes:**
- `x`: The x-coordinate of the **top-left** point of the shape.
- `y`: The y-coordinate of the **top-left** point of the shape.
- `width`: The width of the shape.
- `height`: The height of the shape.
- `as`: Specifies the role of this geometry within its parent cell. Typically set to `"geometry"` for the main shape definition.
**Attributes for connectors:**
- `relative`: Set to "1" for relative geometry
- `as`: Set to "geometry"
**Example for shapes:**
```xml
<mxGeometry x="350" y="190" width="120" height="60" as="geometry"/>
```
**Example for connectors:**
```xml
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="400" y="430" as="sourcePoint"/>
<mxPoint x="450" y="380" as="targetPoint"/>
</mxGeometry>
```
## Cell Style Reference
Styles are specified as semicolon-separated `key=value` pairs in the `style` attribute of `<mxCell>` elements.
### Shape-specific Styles
- Rectangle: `shape=rectangle`
- Ellipse: `shape=ellipse`
- Triangle: `shape=triangle`
- Rhombus: `shape=rhombus`
- Hexagon: `shape=hexagon`
- Cloud: `shape=cloud`
- Actor: `shape=actor`
- Cylinder: `shape=cylinder`
- Document: `shape=document`
- Note: `shape=note`
- Card: `shape=card`
- Parallelogram: `shape=parallelogram`
### Connector Styles
- `endArrow=classic`: Arrow type at the end (classic, open, oval, diamond, block)
- `startArrow=none`: Arrow type at the start (none, classic, open, oval, diamond)
- `curved=1`: Curved connector (0 or 1)
- `edgeStyle=orthogonalEdgeStyle`: Connector routing style
- `elbow=vertical`: Elbow direction (vertical, horizontal)
- `jumpStyle=arc`: Jump style for line crossing (arc, gap)
- `jumpSize=10`: Size of the jump
## Special Cells
Draw.io files contain two special cells that are always present:
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
## Tips for Creating Draw.io XML
1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically
2. Start IDs from "2" (id="0" and id="1" are reserved for root cells)
3. Assign unique and sequential IDs to all cells
4. Define parent relationships correctly (use parent="1" for top-level shapes)
5. Use `mxGeometry` elements to position shapes
6. For connectors, specify `source` and `target` attributes
7. **CRITICAL: All mxCell elements must be siblings. NEVER nest mxCell inside another mxCell.**
## Common Patterns
### Grouping Elements
To group elements, create a parent cell and set other cells' `parent` attribute to its ID:
```xml
<!-- Group container -->
<mxCell id="10" value="Group" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="200" y="200" width="200" height="100" as="geometry" />
</mxCell>
<!-- Elements inside the group -->
<mxCell id="11" value="Element 1" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="10">
<mxGeometry width="90" height="40" as="geometry" />
</mxCell>
<mxCell id="12" value="Element 2" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="10">
<mxGeometry x="110" width="90" height="40" as="geometry" />
</mxCell>
```
### Swimlanes
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
<root>
<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>
<!-- 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 use multiple cells with parent-child relationships:
```xml
<mxCell id="30" value="Table" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1">
<mxGeometry x="200" y="200" width="180" height="120" as="geometry" />
</mxCell>
<mxCell id="31" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[0,0.5,1,0.5];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="30">
<mxGeometry y="30" width="180" height="30" as="geometry" />
</mxCell>
```
## Advanced Features
### Custom Attributes
Draw.io allows adding custom attributes to cells:
```xml
<mxCell id="40" value="Custom Element" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="200" y="200" width="120" height="60" as="geometry"/>
<Object label="Custom Label" customAttr="value" />
</mxCell>
```
These custom attributes can store additional metadata or be used by plugins and custom behaviors.
### User-defined Styles
You can define custom styles for cells by combining various style attributes:
```xml
<mxCell id="50" value="Custom Styled Cell"
style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;fontSize=14;fontStyle=1"
vertex="1" parent="1">
<mxGeometry x="300" y="200" width="120" height="80" as="geometry"/>
</mxCell>
```
### Layers
You can create multiple layers in a diagram to organize complex diagrams:
```xml
<!-- Default layer (always present) -->
<mxCell id="1" parent="0"/>
<!-- Additional custom layer -->
<mxCell id="60" value="Layer 2" style="locked=0;group=" parent="0"/>
<!-- Elements in Layer 2 -->
<mxCell id="61" value="Element in Layer 2" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="60">
<mxGeometry x="200" y="300" width="120" height="60" as="geometry"/>
</mxCell>
```
================================================
FILE: app/api/config/route.ts
================================================
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,
})
}
================================================
FILE: app/api/log-feedback/route.ts
================================================
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 },
)
}
}
================================================
FILE: app/api/log-save/route.ts
================================================
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 },
)
}
}
================================================
FILE: app/api/parse-url/route.ts
================================================
import { extract } from "@extractus/article-extractor"
import { NextResponse } from "next/server"
import TurndownService from "turndown"
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
const EXTRACT_TIMEOUT_MS = 15000
const USER_AGENT = "Mozilla/5.0 (compatible; NextAIDrawio/1.0)"
export async function POST(req: Request) {
try {
const { url } = await req.json()
if (!url || typeof url !== "string") {
return NextResponse.json(
{ error: "URL is required" },
{ status: 400 },
)
}
// Validate URL format
try {
new URL(url)
} catch {
return NextResponse.json(
{ error: "Invalid URL format" },
{ status: 400 },
)
}
// SSRF protection
if (!allowPrivateUrls && isPrivateUrl(url)) {
return NextResponse.json(
{ error: "Cannot access private/internal URLs" },
{ status: 400 },
)
}
const headController = new AbortController()
const headTimeout = setTimeout(() => headController.abort(), 3000)
try {
const headResponse = await fetch(url, {
method: "HEAD",
headers: { "User-Agent": USER_AGENT },
signal: headController.signal,
})
const contentType = headResponse.headers.get("content-type")
if (contentType?.includes("application/pdf")) {
return NextResponse.json(
{
error: "PDF URLs are not supported. Please download and upload the PDF file directly",
},
{ status: 422 },
)
}
} catch (err) {
console.warn(
"HEAD pre-check failed, proceeding with extraction:",
err,
)
} finally {
clearTimeout(headTimeout)
}
// Extract article content with timeout to avoid tying up server resources
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, EXTRACT_TIMEOUT_MS)
let article
try {
article = await extract(url, undefined, {
headers: { "User-Agent": USER_AGENT },
signal: controller.signal,
})
} catch (err: any) {
if (err?.name === "AbortError") {
return NextResponse.json(
{ error: "Timed out while fetching URL content" },
{ status: 504 },
)
}
throw err
} finally {
clearTimeout(timeoutId)
}
if (!article || !article.content) {
return NextResponse.json(
{ error: "Could not extract content from URL" },
{ status: 400 },
)
}
// Convert HTML to Markdown
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
})
// Remove unwanted elements before conversion
turndownService.remove(["script", "style", "iframe", "noscript"])
const markdown = turndownService.turndown(article.content)
// Check content length
if (markdown.length > MAX_CONTENT_LENGTH) {
return NextResponse.json(
{
error: `Content exceeds ${MAX_CONTENT_LENGTH / 1000}k character limit (${(markdown.length / 1000).toFixed(1)}k chars)`,
},
{ status: 400 },
)
}
return NextResponse.json({
title: article.title || "Untitled",
content: markdown,
charCount: markdown.length,
})
} catch (error) {
console.error("URL extraction error:", error)
return NextResponse.json(
{ error: "Failed to fetch or parse URL content" },
{ status: 500 },
)
}
}
================================================
FILE: app/api/server-models/route.ts
================================================
import { NextResponse } from "next/server"
import { loadFlattenedServerModels } from "@/lib/server-model-config"
// Use dynamic rendering to read AI_MODEL/AI_PROVIDER env vars at runtime
// This ensures Docker users can set these values when starting containers
export const dynamic = "force-dynamic"
export async function GET() {
const models = await loadFlattenedServerModels()
return NextResponse.json({
models,
hasConfig: models.length > 0,
})
}
================================================
FILE: app/api/validate-diagram/route.ts
================================================
/**
* API endpoint for VLM-based diagram validation.
* Accepts a PNG image and streams validation results using useObject-compatible format.
*/
import { streamObject } from "ai"
import { getValidationModel } from "@/lib/ai-providers"
import { VALIDATION_SYSTEM_PROMPT } from "@/lib/validation-prompts"
import {
type ValidationResult,
ValidationResultSchema,
} from "@/lib/validation-schema"
export const maxDuration = 30
interface ValidateDiagramRequest {
imageData: string // Base64 PNG data URL
sessionId?: string
}
// Default valid result for disabled/error cases
const DEFAULT_VALID_RESULT: ValidationResult = {
valid: true,
issues: [],
suggestions: [],
}
/**
* Create a streaming response for useObject compatibility.
* useObject expects text stream format, not plain JSON.
*/
function createStreamingResponse(result: ValidationResult): Response {
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
// Stream the JSON as text (useObject parses this)
controller.enqueue(encoder.encode(JSON.stringify(result)))
controller.close()
},
})
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
})
}
export async function POST(req: Request): Promise<Response> {
try {
// Check if VLM validation is enabled (default: true)
const enableValidation = process.env.ENABLE_VLM_VALIDATION !== "false"
if (!enableValidation) {
return createStreamingResponse(DEFAULT_VALID_RESULT)
}
const body: ValidateDiagramRequest = await req.json()
const { imageData, sessionId } = body
if (!imageData) {
return Response.json(
{ error: "Missing imageData" },
{ status: 400 },
)
}
// Validate image data format
if (
!imageData.startsWith("data:image/png;base64,") &&
!imageData.startsWith("data:image/")
) {
return Response.json(
{ error: "Invalid image data format" },
{ status: 400 },
)
}
// Get the validation model
let model
try {
model = getValidationModel()
} catch (error) {
console.warn(
"[validate-diagram] Validation model not available:",
error,
)
// Return valid if no vision model is configured
return createStreamingResponse(DEFAULT_VALID_RESULT)
}
// Parse timeout with validation (minimum 1000ms, default 10000ms)
const timeout =
Math.max(
1000,
parseInt(process.env.VALIDATION_TIMEOUT || "10000", 10),
) || 10000
// Stream the VLM response for useObject consumption
const result = streamObject({
model,
schema: ValidationResultSchema,
system: VALIDATION_SYSTEM_PROMPT,
messages: [
{
role: "user",
content: [
{
type: "image",
image: imageData,
},
{
type: "text",
text: "Please analyze this diagram for visual quality issues.",
},
],
},
],
maxOutputTokens: 1024,
abortSignal: AbortSignal.timeout(timeout),
onFinish: ({ object }) => {
if (sessionId && object) {
console.log(
`[validate-diagram] Session ${sessionId}: valid=${object.valid}, issues=${object.issues?.length ?? 0}`,
)
}
},
})
return result.toTextStreamResponse()
} catch (error) {
// Log with session context if available
const errorMessage =
error instanceof Error ? error.message : String(error)
console.error("[validate-diagram] Error:", errorMessage)
// On error, return valid to not block the user
return createStreamingResponse(DEFAULT_VALID_RESULT)
}
}
================================================
FILE: app/api/validate-model/route.ts
================================================
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 { createVertex } from "@ai-sdk/google-vertex"
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"
import { normalizeMiniMaxBaseURL } from "@/lib/ai-providers"
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
export const runtime = "nodejs"
interface ValidateRequest {
provider: string
apiKey: string
baseUrl?: string
modelId: string
// AWS Bedrock specific
awsAccessKeyId?: string
awsSecretAccessKey?: string
awsRegion?: string
// Vertex AI specific
vertexApiKey?: string // Express Mode API key
}
export async function POST(req: Request) {
try {
const body: ValidateRequest = await req.json()
const {
provider,
apiKey,
baseUrl,
modelId,
awsAccessKeyId,
awsSecretAccessKey,
awsRegion,
// Note: Express Mode only needs vertexApiKey
vertexApiKey,
} = 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 && !allowPrivateUrls && 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 === "vertexai") {
if (!vertexApiKey) {
return NextResponse.json(
{
valid: false,
error: "Vertex AI API key is required for Express Mode",
},
{ 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 "vertexai": {
const vertex = createVertex({
apiKey: vertexApiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = vertex(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.cn/v1",
})
model = sf.chat(modelId)
break
}
case "ollama": {
// SECURITY: Mirror ai-providers.ts guard — only use server
// OLLAMA_API_KEY when the URL is also from server config.
const ollamaApiKey = baseUrl
? apiKey || undefined
: apiKey || process.env.OLLAMA_API_KEY || undefined
const ollamaProvider = createOllama({
baseURL:
baseUrl ||
process.env.OLLAMA_BASE_URL ||
"https://ollama.com/api",
...(ollamaApiKey && {
headers: { Authorization: `Bearer ${ollamaApiKey}` },
}),
})
model = ollamaProvider(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: use DeepSeek for DeepSeek/Kimi models, OpenAI for others
const doubaoBaseUrl =
baseUrl || "https://ark.cn-beijing.volces.com/api/v3"
const lowerModelId = modelId.toLowerCase()
if (
lowerModelId.includes("deepseek") ||
lowerModelId.includes("kimi")
) {
const doubao = createDeepSeek({
apiKey,
baseURL: doubaoBaseUrl,
})
model = doubao(modelId)
} else {
const doubao = createOpenAI({
apiKey,
baseURL: doubaoBaseUrl,
})
model = doubao.chat(modelId)
}
break
}
case "modelscope": {
const baseURL =
baseUrl || "https://api-inference.modelscope.cn/v1"
const startTime = Date.now()
try {
// Initiate a streaming request (required for QwQ-32B and certain Qwen3 models)
const response = await fetch(
`${baseURL}/chat/completions`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelId,
messages: [
{ role: "user", content: "Say 'OK'" },
],
max_tokens: 20,
stream: true,
enable_thinking: false,
}),
},
)
if (!response.ok) {
const errorText = await response.text()
throw new Error(
`ModelScope API error (${response.status}): ${errorText}`,
)
}
const contentType =
response.headers.get("content-type") || ""
const isValidStreamingResponse =
response.status === 200 &&
(contentType.includes("text/event-stream") ||
contentType.includes("application/json"))
if (!isValidStreamingResponse) {
throw new Error(
`Unexpected response format: ${contentType}`,
)
}
const responseTime = Date.now() - startTime
if (response.body) {
response.body.cancel().catch(() => {
/* Ignore cancellation errors */
})
}
return NextResponse.json({
valid: true,
responseTime,
note: "ModelScope model validated (using streaming API)",
})
} catch (error) {
console.error(
"[validate-model] ModelScope validation failed:",
error,
)
throw error
}
}
case "minimax": {
const rawUrl =
baseUrl ||
PROVIDER_INFO.minimax?.defaultBaseUrl ||
"https://api.minimaxi.com/anthropic"
const { baseURL: minimaxBaseUrl, isAnthropicCompatible } =
normalizeMiniMaxBaseURL(rawUrl)
if (isAnthropicCompatible) {
const minimax = createAnthropic({
apiKey,
baseURL: minimaxBaseUrl,
})
model = minimax.chat(modelId)
} else {
const minimax = createOpenAI({
apiKey,
baseURL: minimaxBaseUrl,
})
model = minimax.chat(modelId)
}
break
}
// GLM, Qwen, Kimi, Qiniu - OpenAI compatible
case "glm":
case "qwen":
case "kimi":
case "qiniu": {
const baseURL =
baseUrl ||
PROVIDER_INFO[provider as ProviderName]?.defaultBaseUrl ||
""
if (!baseURL) {
return NextResponse.json(
{
valid: false,
error: `No base URL configured for provider: ${provider}`,
},
{ status: 400 },
)
}
const openai = createOpenAI({
apiKey,
baseURL,
})
model = openai.chat(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
)
}
}
================================================
FILE: app/api/verify-access-code/route.ts
================================================
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" })
}
================================================
FILE: app/globals.css
================================================
@import "tailwindcss";
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(-
gitextract_moyqnob7/ ├── .dockerignore ├── .eslintrc.json ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── enhancement.md │ │ └── feature_request.md │ ├── renovate.json │ └── workflows/ │ ├── auto-format.yml │ ├── ci.yml │ ├── docker-build.yml │ ├── electron-release.yml │ └── test.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .vscode/ │ └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── app/ │ ├── [lang]/ │ │ ├── about/ │ │ │ ├── cn/ │ │ │ │ └── page.tsx │ │ │ ├── ja/ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api/ │ │ ├── chat/ │ │ │ ├── route.ts │ │ │ └── xml_guide.md │ │ ├── config/ │ │ │ └── route.ts │ │ ├── log-feedback/ │ │ │ └── route.ts │ │ ├── log-save/ │ │ │ └── route.ts │ │ ├── parse-url/ │ │ │ └── route.ts │ │ ├── server-models/ │ │ │ └── route.ts │ │ ├── validate-diagram/ │ │ │ └── route.ts │ │ ├── validate-model/ │ │ │ └── route.ts │ │ └── verify-access-code/ │ │ └── route.ts │ ├── globals.css │ ├── manifest.ts │ ├── robots.ts │ └── sitemap.ts ├── biome.json ├── components/ │ ├── ai-elements/ │ │ ├── model-selector.tsx │ │ ├── reasoning.tsx │ │ └── shimmer.tsx │ ├── button-with-tooltip.tsx │ ├── chat/ │ │ ├── ChatLobby.tsx │ │ ├── ToolCallCard.tsx │ │ ├── ValidationCard.tsx │ │ └── types.ts │ ├── chat-example-panel.tsx │ ├── chat-input.tsx │ ├── chat-message-display.tsx │ ├── chat-panel.tsx │ ├── code-block.tsx │ ├── dev-xml-simulator.tsx │ ├── error-toast.tsx │ ├── file-preview-list.tsx │ ├── history-dialog.tsx │ ├── image-with-basepath.tsx │ ├── model-config-dialog.tsx │ ├── model-selector.tsx │ ├── quota-limit-toast.tsx │ ├── reset-warning-modal.tsx │ ├── save-dialog.tsx │ ├── settings-dialog.tsx │ ├── ui/ │ │ ├── alert-dialog.tsx │ │ ├── button.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── switch.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ └── url-input-dialog.tsx ├── components.json ├── contexts/ │ └── diagram-context.tsx ├── docker-compose.yml ├── docs/ │ ├── cn/ │ │ ├── FAQ.md │ │ ├── README_CN.md │ │ ├── ai-providers.md │ │ ├── cloudflare-deploy.md │ │ ├── docker.md │ │ └── offline-deployment.md │ ├── en/ │ │ ├── FAQ.md │ │ ├── ai-providers.md │ │ ├── cloudflare-deploy.md │ │ ├── docker.md │ │ └── offline-deployment.md │ ├── ja/ │ │ ├── FAQ.md │ │ ├── README_JA.md │ │ ├── ai-providers.md │ │ ├── cloudflare-deploy.md │ │ ├── docker.md │ │ └── offline-deployment.md │ └── shape-libraries/ │ ├── README.md │ ├── alibaba_cloud.md │ ├── android.md │ ├── arrows2.md │ ├── atlassian.md │ ├── aws4.md │ ├── azure2.md │ ├── basic.md │ ├── bpmn.md │ ├── cabinets.md │ ├── cisco19.md │ ├── citrix.md │ ├── electrical.md │ ├── floorplan.md │ ├── flowchart.md │ ├── fluidpower.md │ ├── gcp2.md │ ├── infographic.md │ ├── kubernetes.md │ ├── lean_mapping.md │ ├── material_design.md │ ├── mscae.md │ ├── network.md │ ├── openstack.md │ ├── pid.md │ ├── rack.md │ ├── salesforce.md │ ├── sap.md │ ├── sitemap.md │ ├── vvd.md │ └── webicons.md ├── edge-functions/ │ └── api/ │ └── edgeai/ │ └── chat/ │ └── completions.ts ├── edgeone.json ├── electron/ │ ├── electron-builder.yml │ ├── electron.d.ts │ ├── main/ │ │ ├── app-menu.ts │ │ ├── config-manager.ts │ │ ├── env-loader.ts │ │ ├── index.ts │ │ ├── ipc-handlers.ts │ │ ├── menu-i18n.ts │ │ ├── next-server.ts │ │ ├── port-manager.ts │ │ ├── proxy-manager.ts │ │ ├── settings-window.ts │ │ └── window-manager.ts │ ├── preload/ │ │ ├── index.ts │ │ └── settings.ts │ ├── settings/ │ │ ├── index.html │ │ ├── settings.css │ │ └── settings.js │ └── tsconfig.json ├── env.example ├── hooks/ │ ├── use-diagram-tool-handlers.ts │ ├── use-dictionary.ts │ ├── use-model-config.ts │ ├── use-session-manager.ts │ └── use-validate-diagram.ts ├── instrumentation.ts ├── lib/ │ ├── ai-providers.ts │ ├── base-path.ts │ ├── cached-responses.ts │ ├── chat-helpers.ts │ ├── diagram-validator.ts │ ├── dynamo-quota-manager.ts │ ├── i18n/ │ │ ├── config.ts │ │ ├── dictionaries/ │ │ │ ├── en.json │ │ │ ├── ja.json │ │ │ ├── zh-Hant.json │ │ │ └── zh.json │ │ ├── dictionaries.ts │ │ └── utils.ts │ ├── langfuse.ts │ ├── pdf-utils.ts │ ├── server-model-config.ts │ ├── session-storage.ts │ ├── ssrf-protection.ts │ ├── storage.ts │ ├── system-prompts.ts │ ├── types/ │ │ └── model-config.ts │ ├── url-utils.ts │ ├── use-file-processor.tsx │ ├── use-quota-manager.tsx │ ├── user-id.ts │ ├── utils.ts │ ├── validation-prompts.ts │ └── validation-schema.ts ├── next.config.ts ├── open-next.config.ts ├── package.json ├── packages/ │ ├── claude-plugin/ │ │ ├── .claude-plugin/ │ │ │ └── plugin.json │ │ ├── .mcp.json │ │ └── README.md │ └── mcp-server/ │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── diagram-operations.ts │ │ ├── history.ts │ │ ├── http-server.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── xml-validation.ts │ └── tsconfig.json ├── playwright.config.ts ├── postcss.config.mjs ├── proxy.ts ├── public/ │ ├── _headers │ └── chain-of-thought.txt ├── resources/ │ └── entitlements.mac.plist ├── scripts/ │ ├── afterPack.cjs │ ├── electron-dev.mjs │ ├── prepare-electron-build.mjs │ └── test-diagram-operations.mjs ├── tests/ │ ├── e2e/ │ │ ├── chat.spec.ts │ │ ├── copy-paste.spec.ts │ │ ├── diagram-generation.spec.ts │ │ ├── error-handling.spec.ts │ │ ├── file-upload.spec.ts │ │ ├── fixtures/ │ │ │ └── diagrams.ts │ │ ├── history-restore.spec.ts │ │ ├── history.spec.ts │ │ ├── iframe-interaction.spec.ts │ │ ├── keyboard.spec.ts │ │ ├── language.spec.ts │ │ ├── lib/ │ │ │ ├── fixtures.ts │ │ │ └── helpers.ts │ │ ├── model-config.spec.ts │ │ ├── multi-turn.spec.ts │ │ ├── save.spec.ts │ │ ├── settings.spec.ts │ │ ├── smoke.spec.ts │ │ ├── theme.spec.ts │ │ └── upload.spec.ts │ └── unit/ │ ├── ai-providers.test.ts │ ├── cached-responses.test.ts │ ├── chat-helpers.test.ts │ ├── diagram-validator.test.ts │ ├── server-model-config.test.ts │ └── utils.test.ts ├── tsconfig.json ├── vercel.json ├── vitest.config.mts └── wrangler.jsonc
SYMBOL INDEX (564 symbols across 113 files)
FILE: app/[lang]/about/cn/page.tsx
function AboutCN (line 13) | function AboutCN() {
FILE: app/[lang]/about/ja/page.tsx
function AboutJA (line 20) | function AboutJA() {
FILE: app/[lang]/about/page.tsx
function About (line 20) | function About() {
FILE: app/[lang]/layout.tsx
function generateStaticParams (line 33) | async function generateStaticParams() {
function generateMetadata (line 38) | async function generateMetadata({
function RootLayout (line 136) | async function RootLayout({
FILE: app/[lang]/page.tsx
function Home (line 15) | function Home() {
FILE: app/api/chat/route.ts
function createCachedStreamResponse (line 45) | function createCachedStreamResponse(xml: string): Response {
function handleChatRequest (line 75) | async function handleChatRequest(req: Request): Promise<Response> {
function handleError (line 783) | function handleError(error: unknown): Response {
function safeHandler (line 845) | async function safeHandler(req: Request): Promise<Response> {
function POST (line 856) | async function POST(req: Request) {
FILE: app/api/config/route.ts
function GET (line 3) | async function GET() {
FILE: app/api/log-feedback/route.ts
function POST (line 12) | async function POST(req: Request) {
FILE: app/api/log-save/route.ts
function POST (line 11) | async function POST(req: Request) {
FILE: app/api/parse-url/route.ts
constant MAX_CONTENT_LENGTH (line 6) | const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
constant EXTRACT_TIMEOUT_MS (line 7) | const EXTRACT_TIMEOUT_MS = 15000
constant USER_AGENT (line 8) | const USER_AGENT = "Mozilla/5.0 (compatible; NextAIDrawio/1.0)"
function POST (line 10) | async function POST(req: Request) {
FILE: app/api/server-models/route.ts
function GET (line 8) | async function GET() {
FILE: app/api/validate-diagram/route.ts
type ValidateDiagramRequest (line 16) | interface ValidateDiagramRequest {
constant DEFAULT_VALID_RESULT (line 22) | const DEFAULT_VALID_RESULT: ValidationResult = {
function createStreamingResponse (line 32) | function createStreamingResponse(result: ValidationResult): Response {
function POST (line 46) | async function POST(req: Request): Promise<Response> {
FILE: app/api/validate-model/route.ts
type ValidateRequest (line 18) | interface ValidateRequest {
function POST (line 31) | async function POST(req: Request) {
FILE: app/api/verify-access-code/route.ts
function POST (line 1) | async function POST(req: Request) {
FILE: app/manifest.ts
function manifest (line 3) | function manifest(): MetadataRoute.Manifest {
FILE: app/robots.ts
function robots (line 3) | function robots(): MetadataRoute.Robots {
FILE: app/sitemap.ts
function sitemap (line 3) | function sitemap(): MetadataRoute.Sitemap {
FILE: components/ai-elements/model-selector.tsx
type ModelSelectorProps (line 23) | type ModelSelectorProps = ComponentProps<typeof Dialog>
type ModelSelectorTriggerProps (line 29) | type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>
type ModelSelectorContentProps (line 35) | type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
type ModelSelectorDialogProps (line 53) | type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>
type ModelSelectorInputProps (line 59) | type ModelSelectorInputProps = ComponentProps<typeof CommandInput>
type ModelSelectorListProps (line 68) | type ModelSelectorListProps = ComponentProps<typeof CommandList>
type ModelSelectorEmptyProps (line 130) | type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
type ModelSelectorGroupProps (line 136) | type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>
type ModelSelectorItemProps (line 142) | type ModelSelectorItemProps = ComponentProps<typeof CommandItem>
type ModelSelectorShortcutProps (line 148) | type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>
type ModelSelectorSeparatorProps (line 154) | type ModelSelectorSeparatorProps = ComponentProps<
type ModelSelectorLogoProps (line 162) | type ModelSelectorLogoProps = Omit<
type ModelSelectorLogoGroupProps (line 192) | type ModelSelectorLogoGroupProps = ComponentProps<"div">
type ModelSelectorNameProps (line 207) | type ModelSelectorNameProps = ComponentProps<"span">
type ModelSelectorSectionHeaderProps (line 216) | type ModelSelectorSectionHeaderProps = {
FILE: components/ai-elements/reasoning.tsx
type ReasoningContextValue (line 15) | type ReasoningContextValue = {
type ReasoningProps (line 32) | type ReasoningProps = ComponentProps<typeof Collapsible> & {
constant AUTO_CLOSE_DELAY (line 40) | const AUTO_CLOSE_DELAY = 1000
constant MS_IN_S (line 41) | const MS_IN_S = 1000
type ReasoningTriggerProps (line 113) | type ReasoningTriggerProps = ComponentProps<
type ReasoningContentProps (line 163) | type ReasoningContentProps = ComponentProps<
FILE: components/ai-elements/shimmer.tsx
type TextShimmerProps (line 13) | type TextShimmerProps = {
FILE: components/button-with-tooltip.tsx
type ButtonWithTooltipProps (line 11) | interface ButtonWithTooltipProps
function ButtonWithTooltip (line 19) | function ButtonWithTooltip({
FILE: components/chat-example-panel.tsx
type ExampleCardProps (line 14) | interface ExampleCardProps {
function ExampleCard (line 22) | function ExampleCard({
function ExamplePanel (line 70) | function ExamplePanel({
FILE: components/chat-input.tsx
constant MAX_IMAGE_SIZE (line 40) | const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
constant MAX_FILES (line 41) | const MAX_FILES = 5
function isValidFileType (line 43) | function isValidFileType(file: File): boolean {
function formatFileSize (line 47) | function formatFileSize(bytes: number): string {
function showErrorToast (line 53) | function showErrorToast(message: React.ReactNode) {
type ValidationResult (line 62) | interface ValidationResult {
function validateFiles (line 67) | function validateFiles(
function showValidationErrors (line 116) | function showValidationErrors(errors: string[], dict: any) {
type ChatInputRef (line 148) | interface ChatInputRef {
type ChatInputProps (line 152) | interface ChatInputProps {
FILE: components/chat-message-display.tsx
function getCompleteOperations (line 46) | function getCompleteOperations(
type TextSection (line 64) | interface TextSection {
function splitTextIntoFileSections (line 72) | function splitTextIntoFileSections(text: string): TextSection[] {
type SessionMetadata (line 138) | interface SessionMetadata {
type ChatMessageDisplayProps (line 145) | interface ChatMessageDisplayProps {
function ChatMessageDisplay (line 164) | function ChatMessageDisplay({
FILE: components/chat-panel.tsx
constant STORAGE_SESSION_ID_KEY (line 48) | const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
constant SESSION_STORAGE_INPUT_KEY (line 51) | const SESSION_STORAGE_INPUT_KEY = "next-ai-draw-io-input"
type MessagePart (line 54) | interface MessagePart {
type ChatMessage (line 62) | interface ChatMessage {
type ChatPanelProps (line 68) | interface ChatPanelProps {
constant TOOL_ERROR_STATE (line 79) | const TOOL_ERROR_STATE = "output-error" as const
constant DEBUG (line 80) | const DEBUG = process.env.NODE_ENV === "development"
constant MAX_AUTO_RETRY_COUNT (line 82) | const MAX_AUTO_RETRY_COUNT = 3
constant MAX_CONTINUATION_RETRY_COUNT (line 84) | const MAX_CONTINUATION_RETRY_COUNT = 2 // Limit for truncation continuat...
function hasToolErrors (line 90) | function hasToolErrors(messages: ChatMessage[]): boolean {
function ChatPanel (line 109) | function ChatPanel({
FILE: components/chat/ChatLobby.tsx
type SessionMetadata (line 25) | interface SessionMetadata {
type ChatLobbyProps (line 32) | interface ChatLobbyProps {
function formatSessionDate (line 58) | function formatSessionDate(
function ChatLobby (line 78) | function ChatLobby({
FILE: components/chat/ToolCallCard.tsx
type ToolCallCardProps (line 9) | interface ToolCallCardProps {
function OperationsDisplay (line 22) | function OperationsDisplay({ operations }: { operations: DiagramOperatio...
function ToolCallCard (line 59) | function ToolCallCard({
FILE: components/chat/ValidationCard.tsx
type ValidationStatus (line 18) | type ValidationStatus =
type ValidationState (line 28) | interface ValidationState {
type ValidationCardProps (line 37) | interface ValidationCardProps {
function ValidationCard (line 42) | function ValidationCard({
FILE: components/chat/types.ts
type DiagramOperation (line 1) | interface DiagramOperation {
type ToolPartLike (line 7) | interface ToolPartLike {
FILE: components/code-block.tsx
type CodeBlockProps (line 5) | interface CodeBlockProps {
function CodeBlock (line 10) | function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
FILE: components/dev-xml-simulator.tsx
constant DEV_XML_PRESETS (line 8) | const DEV_XML_PRESETS: Record<string, string> = {
type DevXmlSimulatorProps (line 135) | interface DevXmlSimulatorProps {
function DevXmlSimulator (line 141) | function DevXmlSimulator({
FILE: components/error-toast.tsx
type ErrorToastProps (line 5) | interface ErrorToastProps {
function ErrorToast (line 10) | function ErrorToast({ message, onDismiss }: ErrorToastProps) {
FILE: components/file-preview-list.tsx
function formatCharCount (line 9) | function formatCharCount(count: number): string {
type FilePreviewListProps (line 16) | interface FilePreviewListProps {
function FilePreviewList (line 30) | function FilePreviewList({
FILE: components/history-dialog.tsx
type HistoryDialogProps (line 18) | interface HistoryDialogProps {
function HistoryDialog (line 23) | function HistoryDialog({
FILE: components/model-config-dialog.tsx
type ModelConfigDialogProps (line 64) | interface ModelConfigDialogProps {
type ValidationStatus (line 70) | type ValidationStatus = "idle" | "validating" | "success" | "error"
function ProviderLogo (line 73) | function ProviderLogo({
function ConfigSection (line 105) | function ConfigSection({
function ConfigCard (line 133) | function ConfigCard({ children }: { children: React.ReactNode }) {
function ModelConfigDialog (line 141) | function ModelConfigDialog({
FILE: components/model-selector.tsx
type ModelSelectorProps (line 36) | interface ModelSelectorProps {
function groupModelsByProvider (line 46) | function groupModelsByProvider(
function ModelSelector (line 69) | function ModelSelector({
FILE: components/quota-limit-toast.tsx
type QuotaLimitToastProps (line 9) | interface QuotaLimitToastProps {
function QuotaLimitToast (line 17) | function QuotaLimitToast({
FILE: components/reset-warning-modal.tsx
type ResetWarningModalProps (line 14) | interface ResetWarningModalProps {
function ResetWarningModal (line 20) | function ResetWarningModal({
FILE: components/save-dialog.tsx
type ExportFormat (line 23) | type ExportFormat = "drawio" | "png" | "svg"
type SaveDialogProps (line 25) | interface SaveDialogProps {
function SaveDialog (line 32) | function SaveDialog({
FILE: components/settings-dialog.tsx
function SettingItem (line 32) | function SettingItem({
constant LANGUAGE_LABELS (line 56) | const LANGUAGE_LABELS: Record<Locale, string> = {
type SettingsDialogProps (line 63) | interface SettingsDialogProps {
constant STORAGE_ACCESS_CODE_KEY (line 79) | const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
constant STORAGE_ACCESS_CODE_REQUIRED_KEY (line 80) | const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-re...
function getStoredAccessCodeRequired (line 82) | function getStoredAccessCodeRequired(): boolean | null {
function SettingsContent (line 89) | function SettingsContent({
function SettingsDialog (line 617) | function SettingsDialog(props: SettingsDialogProps) {
FILE: components/ui/alert-dialog.tsx
function AlertDialog (line 9) | function AlertDialog({
function AlertDialogTrigger (line 15) | function AlertDialogTrigger({
function AlertDialogPortal (line 23) | function AlertDialogPortal({
function AlertDialogOverlay (line 31) | function AlertDialogOverlay({
function AlertDialogContent (line 47) | function AlertDialogContent({
function AlertDialogHeader (line 66) | function AlertDialogHeader({
function AlertDialogFooter (line 79) | function AlertDialogFooter({
function AlertDialogTitle (line 95) | function AlertDialogTitle({
function AlertDialogDescription (line 108) | function AlertDialogDescription({
function AlertDialogAction (line 121) | function AlertDialogAction({
function AlertDialogCancel (line 133) | function AlertDialogCancel({
FILE: components/ui/button.tsx
function Button (line 38) | function Button({
FILE: components/ui/collapsible.tsx
function Collapsible (line 5) | function Collapsible({
function CollapsibleTrigger (line 11) | function CollapsibleTrigger({
function CollapsibleContent (line 22) | function CollapsibleContent({
FILE: components/ui/command.tsx
function Command (line 16) | function Command({
function CommandDialog (line 32) | function CommandDialog({
function CommandInput (line 58) | function CommandInput({
function CommandEmpty (line 98) | function CommandEmpty({
function CommandGroup (line 110) | function CommandGroup({
function CommandSeparator (line 126) | function CommandSeparator({
function CommandItem (line 139) | function CommandItem({
function CommandShortcut (line 167) | function CommandShortcut({
FILE: components/ui/dialog.tsx
function Dialog (line 9) | function Dialog({
function DialogTrigger (line 15) | function DialogTrigger({
function DialogPortal (line 21) | function DialogPortal({
function DialogClose (line 27) | function DialogClose({
function DialogOverlay (line 33) | function DialogOverlay({
function DialogContent (line 52) | function DialogContent({
function DialogHeader (line 97) | function DialogHeader({ className, ...props }: React.ComponentProps<"div...
function DialogFooter (line 107) | function DialogFooter({ className, ...props }: React.ComponentProps<"div...
function DialogTitle (line 120) | function DialogTitle({
function DialogDescription (line 136) | function DialogDescription({
FILE: components/ui/input.tsx
function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...
FILE: components/ui/label.tsx
function Label (line 8) | function Label({
FILE: components/ui/popover.tsx
function Popover (line 8) | function Popover({
function PopoverTrigger (line 14) | function PopoverTrigger({
function PopoverContent (line 20) | function PopoverContent({
function PopoverAnchor (line 42) | function PopoverAnchor({
FILE: components/ui/resizable.tsx
function ResizablePanelGroup (line 9) | function ResizablePanelGroup({
function ResizablePanel (line 25) | function ResizablePanel({
function ResizableHandle (line 31) | function ResizableHandle({
FILE: components/ui/scroll-area.tsx
function ScrollArea (line 8) | function ScrollArea({
function ScrollBar (line 31) | function ScrollBar({
FILE: components/ui/select.tsx
function Select (line 9) | function Select({
function SelectGroup (line 15) | function SelectGroup({
function SelectValue (line 21) | function SelectValue({
function SelectTrigger (line 27) | function SelectTrigger({
function SelectContent (line 53) | function SelectContent({
function SelectLabel (line 90) | function SelectLabel({
function SelectItem (line 103) | function SelectItem({
function SelectSeparator (line 127) | function SelectSeparator({
function SelectScrollUpButton (line 140) | function SelectScrollUpButton({
function SelectScrollDownButton (line 158) | function SelectScrollDownButton({
FILE: components/ui/switch.tsx
function Switch (line 8) | function Switch({
FILE: components/ui/textarea.tsx
function Textarea (line 5) | function Textarea({ className, ...props }: React.ComponentProps<"textare...
FILE: components/ui/tooltip.tsx
function TooltipProvider (line 8) | function TooltipProvider({
function Tooltip (line 21) | function Tooltip({
function TooltipTrigger (line 31) | function TooltipTrigger({
function TooltipContent (line 37) | function TooltipContent({
FILE: components/url-input-dialog.tsx
type UrlInputDialogProps (line 17) | interface UrlInputDialogProps {
function UrlInputDialog (line 24) | function UrlInputDialog({
FILE: contexts/diagram-context.tsx
type DiagramContextType (line 15) | interface DiagramContextType {
function DiagramProvider (line 44) | function DiagramProvider({ children }: { children: React.ReactNode }) {
function useDiagram (line 410) | function useDiagram() {
FILE: edge-functions/api/edgeai/chat/completions.ts
constant ALLOWED_MODELS (line 58) | const ALLOWED_MODELS = [
constant MODEL_ALIASES (line 64) | const MODEL_ALIASES: Record<string, string> = {
constant CORS_HEADERS (line 70) | const CORS_HEADERS = {
function createResponse (line 79) | function createResponse(body: any, status = 200, extraHeaders = {}): Res...
function handleOptionsRequest (line 93) | function handleOptionsRequest(): Response {
function onRequest (line 102) | async function onRequest({ request, env: _env }: any) {
FILE: electron/electron.d.ts
type ConfigPreset (line 6) | interface ConfigPreset {
type ApplyPresetResult (line 22) | interface ApplyPresetResult {
type ProxyConfig (line 29) | interface ProxyConfig {
type SetProxyResult (line 35) | interface SetProxyResult {
type SetUserLocaleResult (line 42) | interface SetUserLocaleResult {
type Window (line 48) | interface Window {
FILE: electron/main/app-menu.ts
function buildAppMenu (line 22) | function buildAppMenu(): void {
function rebuildAppMenu (line 31) | function rebuildAppMenu(): void {
function getMenuTemplate (line 38) | function getMenuTemplate(): MenuItemConstructorOptions[] {
function buildConfigMenu (line 184) | function buildConfigMenu(
FILE: electron/main/config-manager.ts
constant SENSITIVE_FIELDS (line 9) | const SENSITIVE_FIELDS = ["AI_API_KEY"] as const
constant ENCRYPTED_PREFIX (line 14) | const ENCRYPTED_PREFIX = "encrypted:"
function isEncryptionAvailable (line 19) | function isEncryptionAvailable(): boolean {
function encryptValue (line 32) | function encryptValue(value: string): string {
function decryptValue (line 65) | function decryptValue(value: string): string {
function encryptConfig (line 88) | function encryptConfig(
function decryptConfig (line 103) | function decryptConfig(
type ConfigPreset (line 118) | interface ConfigPreset {
type ConfigPresetsFile (line 136) | interface ConfigPresetsFile {
constant CONFIG_FILE_NAME (line 143) | const CONFIG_FILE_NAME = "config-presets.json"
function getConfigFilePath (line 148) | function getConfigFilePath(): string {
function loadPresets (line 157) | function loadPresets(): ConfigPresetsFile {
function savePresets (line 195) | function savePresets(data: ConfigPresetsFile): void {
function getAllPresets (line 224) | function getAllPresets(): ConfigPreset[] {
function getCurrentPresetId (line 232) | function getCurrentPresetId(): string | null {
function getCurrentPreset (line 240) | function getCurrentPreset(): ConfigPreset | null {
function createPreset (line 251) | function createPreset(
function updatePreset (line 274) | function updatePreset(
function deletePreset (line 300) | function deletePreset(id: string): boolean {
function setCurrentPreset (line 322) | function setCurrentPreset(id: string | null): boolean {
constant PROVIDER_ENV_MAP (line 340) | const PROVIDER_ENV_MAP: Record<string, { apiKey: string; baseUrl: string...
function applyPresetToEnv (line 371) | function applyPresetToEnv(id: string): Record<string, string> | null {
function getCurrentPresetEnv (line 427) | function getCurrentPresetEnv(): Record<string, string> {
function getUserLocale (line 473) | function getUserLocale(): "en" | "zh" | "ja" | "zh-Hant" | undefined {
function setUserLocale (line 481) | function setUserLocale(
FILE: electron/main/env-loader.ts
function loadEnvFile (line 9) | function loadEnvFile(): void {
function loadEnvFromFile (line 34) | function loadEnvFromFile(filePath: string): void {
FILE: electron/main/ipc-handlers.ts
constant ALLOWED_CONFIG_KEYS (line 28) | const ALLOWED_CONFIG_KEYS = new Set([
function sanitizePresetConfig (line 39) | function sanitizePresetConfig(
function registerIpcHandlers (line 54) | function registerIpcHandlers(): void {
FILE: electron/main/menu-i18n.ts
type MenuLocale (line 8) | type MenuLocale = "en" | "zh" | "ja" | "zh-Hant"
type MenuTranslations (line 10) | interface MenuTranslations {
function getMenuTranslations (line 156) | function getMenuTranslations(locale: string): MenuTranslations {
function detectSystemLocale (line 179) | function detectSystemLocale(appLocale: string): MenuLocale {
function getPreferredLocale (line 202) | function getPreferredLocale(appLocale: string): MenuLocale {
FILE: electron/main/next-server.ts
function getResourcePath (line 18) | function getResourcePath(): string {
function waitForServer (line 28) | async function waitForServer(url: string, timeout = 30000): Promise<void> {
function startNextServer (line 48) | async function startNextServer(): Promise<string> {
function stopNextServer (line 128) | async function stopNextServer(): Promise<void> {
function waitForServerStop (line 167) | async function waitForServerStop(timeout = 5000): Promise<void> {
function restartNextServer (line 187) | async function restartNextServer(): Promise<string> {
FILE: electron/main/port-manager.ts
constant PORT_CONFIG (line 9) | const PORT_CONFIG = {
function isPortAvailable (line 27) | function isPortAvailable(port: number): Promise<boolean> {
function findAvailablePort (line 49) | async function findAvailablePort(reuseExisting = true): Promise<number> {
function getAllocatedPort (line 96) | function getAllocatedPort(): number | null {
function resetAllocatedPort (line 103) | function resetAllocatedPort(): void {
function getServerUrl (line 110) | function getServerUrl(): string {
FILE: electron/main/proxy-manager.ts
constant CONFIG_FILE (line 8) | const CONFIG_FILE = "proxy-config.json"
function getConfigPath (line 10) | function getConfigPath(): string {
function loadProxyConfig (line 17) | function loadProxyConfig(): ProxyConfig {
function saveProxyConfig (line 33) | function saveProxyConfig(config: ProxyConfig): void {
function applyProxyToEnv (line 47) | function applyProxyToEnv(): void {
function getProxyConfig (line 70) | function getProxyConfig(): ProxyConfig {
FILE: electron/main/settings-window.ts
function showSettingsWindow (line 9) | function showSettingsWindow(parentWindow?: BrowserWindow): void {
function closeSettingsWindow (line 57) | function closeSettingsWindow(): void {
function isSettingsWindowOpen (line 67) | function isSettingsWindowOpen(): boolean {
function registerSettingsWindowHandlers (line 74) | function registerSettingsWindowHandlers(): void {
FILE: electron/main/window-manager.ts
function getIconPath (line 11) | function getIconPath(): string | undefined {
function createWindow (line 30) | function createWindow(serverUrl: string): BrowserWindow {
function getMainWindow (line 82) | function getMainWindow(): BrowserWindow | null {
FILE: electron/settings/settings.js
function loadPresets (line 40) | async function loadPresets() {
function renderPresets (line 52) | function renderPresets() {
function setupEventListeners (line 112) | function setupEventListeners() {
function openAddModal (line 137) | function openAddModal() {
function openEditModal (line 147) | function openEditModal(id) {
function closeModal (line 167) | function closeModal() {
function openDeleteModal (line 173) | function openDeleteModal(id) {
function closeDeleteModal (line 183) | function closeDeleteModal() {
function savePreset (line 189) | async function savePreset() {
function confirmDelete (line 237) | async function confirmDelete() {
function applyPreset (line 258) | async function applyPreset(id) {
function getProviderLabel (line 281) | function getProviderLabel(provider) {
function showToast (line 298) | function showToast(message, type = "") {
function escapeHtml (line 308) | function escapeHtml(text) {
FILE: hooks/use-diagram-tool-handlers.ts
constant DEBUG (line 12) | const DEBUG = process.env.NODE_ENV === "development"
type ToolCall (line 14) | interface ToolCall {
type AddToolOutputSuccess (line 20) | type AddToolOutputSuccess = {
type AddToolOutputError (line 28) | type AddToolOutputError = {
type AddToolOutputParams (line 36) | type AddToolOutputParams = AddToolOutputSuccess | AddToolOutputError
type AddToolOutputFn (line 38) | type AddToolOutputFn = (params: AddToolOutputParams) => void
constant MAX_VALIDATION_RETRIES (line 40) | const MAX_VALIDATION_RETRIES = 3
type ValidateDiagramFn (line 43) | type ValidateDiagramFn = (
type UseDiagramToolHandlersParams (line 48) | interface UseDiagramToolHandlersParams {
function useDiagramToolHandlers (line 72) | function useDiagramToolHandlers({
FILE: hooks/use-dictionary.ts
function DictionaryProvider (line 8) | function DictionaryProvider({
function useDictionary (line 19) | function useDictionary() {
FILE: hooks/use-model-config.ts
constant OLD_KEYS (line 21) | const OLD_KEYS = {
function migrateOldConfig (line 31) | function migrateOldConfig(): MultiModelConfig | null {
function loadConfig (line 69) | function loadConfig(): MultiModelConfig {
function saveConfig (line 99) | function saveConfig(config: MultiModelConfig): void {
type UseModelConfigReturn (line 104) | interface UseModelConfigReturn {
function useModelConfig (line 134) | function useModelConfig(): UseModelConfigReturn {
function getSelectedAIConfig (line 374) | function getSelectedAIConfig(): {
FILE: hooks/use-session-manager.ts
type SessionData (line 19) | interface SessionData {
type UseSessionManagerReturn (line 27) | interface UseSessionManagerReturn {
type UseSessionManagerOptions (line 47) | interface UseSessionManagerOptions {
function useSessionManager (line 52) | function useSessionManager(
FILE: hooks/use-validate-diagram.ts
constant DEFAULT_VALID_RESULT (line 18) | const DEFAULT_VALID_RESULT: ValidationResult = {
type UseValidateDiagramOptions (line 24) | interface UseValidateDiagramOptions {
type PendingValidation (line 30) | type PendingValidation = {
function useValidateDiagram (line 35) | function useValidateDiagram(options: UseValidateDiagramOptions = {}) {
FILE: instrumentation.ts
function register (line 4) | function register() {
FILE: lib/ai-providers.ts
type ModelConfig (line 16) | interface ModelConfig {
constant SINGLE_SYSTEM_PROVIDERS (line 25) | const SINGLE_SYSTEM_PROVIDERS = new Set<ProviderName>([
function normalizeMiniMaxBaseURL (line 37) | function normalizeMiniMaxBaseURL(rawUrl: string): {
type ClientOverrides (line 59) | interface ClientOverrides {
constant ALLOWED_CLIENT_PROVIDERS (line 80) | const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
constant BEDROCK_ANTHROPIC_BETA (line 104) | const BEDROCK_ANTHROPIC_BETA = {
constant ANTHROPIC_BETA_HEADERS (line 111) | const ANTHROPIC_BETA_HEADERS = {
function resolveBaseURL (line 127) | function resolveBaseURL(
function resolveApiKey (line 152) | function resolveApiKey(
function resolveBaseUrlEnv (line 188) | function resolveBaseUrlEnv(
function parseIntSafe (line 199) | function parseIntSafe(
function buildProviderOptions (line 238) | function buildProviderOptions(
constant PROVIDER_ENV_VARS (line 537) | const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
function detectProvider (line 564) | function detectProvider(): ProviderName | null {
function validateProviderCredentials (line 598) | function validateProviderCredentials(
function getAIModel (line 661) | function getAIModel(overrides?: ClientOverrides): ModelConfig {
function supportsPromptCaching (line 1304) | function supportsPromptCaching(modelId: string): boolean {
function supportsImageInput (line 1318) | function supportsImageInput(modelId: string): boolean {
function getValidationModel (line 1362) | function getValidationModel(): ReturnType<typeof getAIModel>["model"] {
FILE: lib/base-path.ts
function getBasePath (line 10) | function getBasePath(): string {
function getApiEndpoint (line 24) | function getApiEndpoint(endpoint: string): string {
function getAssetUrl (line 34) | function getAssetUrl(assetPath: string): string {
FILE: lib/cached-responses.ts
type CachedResponse (line 1) | interface CachedResponse {
constant CACHED_EXAMPLE_RESPONSES (line 7) | const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
function findCachedResponse (line 882) | function findCachedResponse(
FILE: lib/chat-helpers.ts
constant MAX_FILE_SIZE (line 5) | const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
constant MAX_FILES (line 6) | const MAX_FILES = 5
function validateFileParts (line 9) | function validateFileParts(messages: any[]): {
function isMinimalDiagram (line 45) | function isMinimalDiagram(xml: string): boolean {
function replaceHistoricalToolInputs (line 53) | function replaceHistoricalToolInputs(messages: any[]): any[] {
FILE: lib/diagram-validator.ts
function formatValidationFeedback (line 18) | function formatValidationFeedback(result: ValidationResult): string {
FILE: lib/dynamo-quota-manager.ts
constant TABLE (line 10) | const TABLE = process.env.DYNAMODB_QUOTA_TABLE
constant DYNAMODB_REGION (line 11) | const DYNAMODB_REGION = process.env.DYNAMODB_REGION || "ap-northeast-1"
constant QUOTA_TIMEZONE (line 14) | let QUOTA_TIMEZONE = process.env.QUOTA_TIMEZONE || "UTC"
function getTodayInTimezone (line 32) | function getTodayInTimezone(): string {
function isQuotaEnabled (line 45) | function isQuotaEnabled(): boolean {
type QuotaLimits (line 49) | interface QuotaLimits {
type QuotaCheckResult (line 55) | interface QuotaCheckResult {
function checkAndIncrementRequest (line 68) | async function checkAndIncrementRequest(
function recordTokenUsage (line 193) | async function recordTokenUsage(
FILE: lib/i18n/config.ts
type Locale (line 6) | type Locale = (typeof i18n)["locales"][number]
FILE: lib/i18n/dictionaries.ts
type Dictionary (line 13) | type Dictionary = Awaited<ReturnType<(typeof dictionaries)["en"]>>
function getDictionary (line 18) | async function getDictionary(locale: Locale): Promise<Dictionary> {
FILE: lib/i18n/utils.ts
function formatMessage (line 1) | function formatMessage(
FILE: lib/langfuse.ts
function getLangfuseClient (line 8) | function getLangfuseClient(): LangfuseClient | null {
function isLangfuseEnabled (line 25) | function isLangfuseEnabled(): boolean {
function setTraceInput (line 32) | function setTraceInput(params: {
function setTraceOutput (line 50) | function setTraceOutput(output: string) {
function getTelemetryConfig (line 63) | function getTelemetryConfig(params: {
function wrapWithObserve (line 81) | function wrapWithObserve<T>(
FILE: lib/pdf-utils.ts
constant DEFAULT_MAX_EXTRACTED_CHARS (line 4) | const DEFAULT_MAX_EXTRACTED_CHARS = 150000 // 150k chars
constant MAX_EXTRACTED_CHARS (line 5) | const MAX_EXTRACTED_CHARS =
constant TEXT_EXTENSIONS (line 10) | const TEXT_EXTENSIONS = [
function extractPdfText (line 44) | async function extractPdfText(file: File): Promise<string> {
function isPdfFile (line 54) | function isPdfFile(file: File): boolean {
function isTextFile (line 61) | function isTextFile(file: File): boolean {
function extractTextFileContent (line 73) | async function extractTextFileContent(file: File): Promise<string> {
FILE: lib/server-model-config.ts
type ServerProviderConfig (line 33) | type ServerProviderConfig = z.infer<typeof ServerProviderSchema>
type ServerModelsConfig (line 34) | type ServerModelsConfig = z.infer<typeof ServerModelsConfigSchema>
type FlattenedServerModel (line 36) | interface FlattenedServerModel {
function slugify (line 52) | function slugify(name: string): string {
function getConfigPath (line 59) | function getConfigPath(): string {
function loadRawServerModelsConfig (line 65) | async function loadRawServerModelsConfig(): Promise<ServerModelsConfig |...
function loadFlattenedServerModels (line 99) | async function loadFlattenedServerModels(): Promise<
function findServerModelById (line 148) | async function findServerModelById(
FILE: lib/session-storage.ts
constant DB_NAME (line 5) | const DB_NAME = "next-ai-drawio"
constant DB_VERSION (line 6) | const DB_VERSION = 1
constant STORE_NAME (line 7) | const STORE_NAME = "sessions"
constant MIGRATION_FLAG (line 8) | const MIGRATION_FLAG = "next-ai-drawio-migrated-to-idb"
constant MAX_SESSIONS (line 9) | const MAX_SESSIONS = 50
type ChatSession (line 12) | interface ChatSession {
type StoredMessage (line 24) | interface StoredMessage {
type SessionMetadata (line 30) | interface SessionMetadata {
type ChatSessionDB (line 40) | interface ChatSessionDB extends DBSchema {
function getDB (line 51) | async function getDB(): Promise<IDBPDatabase<ChatSessionDB>> {
function isIndexedDBAvailable (line 69) | function isIndexedDBAvailable(): boolean {
function getAllSessionMetadata (line 79) | async function getAllSessionMetadata(): Promise<SessionMetadata[]> {
function getSession (line 109) | async function getSession(id: string): Promise<ChatSession | null> {
function saveSession (line 120) | async function saveSession(session: ChatSession): Promise<boolean> {
function deleteSession (line 153) | async function deleteSession(id: string): Promise<void> {
function getSessionCount (line 163) | async function getSessionCount(): Promise<number> {
function deleteOldestSession (line 174) | async function deleteOldestSession(): Promise<void> {
function enforceSessionLimit (line 191) | async function enforceSessionLimit(): Promise<void> {
function createEmptySession (line 202) | function createEmptySession(): ChatSession {
constant MAX_TITLE_LENGTH (line 215) | const MAX_TITLE_LENGTH = 100
function extractTitle (line 217) | function extractTitle(messages: StoredMessage[]): string {
function sanitizeMessage (line 235) | function sanitizeMessage(message: unknown): StoredMessage | null {
function sanitizeMessages (line 263) | function sanitizeMessages(messages: unknown[]): StoredMessage[] {
function migrateFromLocalStorage (line 270) | async function migrateFromLocalStorage(): Promise<string | null> {
FILE: lib/ssrf-protection.ts
function isPrivateUrl (line 9) | function isPrivateUrl(urlString: string): boolean {
FILE: lib/storage.ts
constant STORAGE_KEYS (line 4) | const STORAGE_KEYS = {
FILE: lib/system-prompts.ts
constant DEFAULT_SYSTEM_PROMPT (line 10) | const DEFAULT_SYSTEM_PROMPT = `
constant STYLE_INSTRUCTIONS (line 194) | const STYLE_INSTRUCTIONS = `
constant MINIMAL_STYLE_INSTRUCTION (line 202) | const MINIMAL_STYLE_INSTRUCTION = `
constant EXTENDED_ADDITIONS (line 228) | const EXTENDED_ADDITIONS = `
constant EXTENDED_SYSTEM_PROMPT (line 359) | const EXTENDED_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT + EXTENDED_ADDITIONS
constant EXTENDED_PROMPT_MODEL_PATTERNS (line 363) | const EXTENDED_PROMPT_MODEL_PATTERNS = [
function getSystemPrompt (line 375) | function getSystemPrompt(
FILE: lib/types/model-config.ts
type ProviderName (line 3) | type ProviderName =
type ModelConfig (line 26) | interface ModelConfig {
type ProviderConfig (line 34) | interface ProviderConfig {
type MultiModelConfig (line 53) | interface MultiModelConfig {
type FlattenedModel (line 61) | interface FlattenedModel {
constant PROVIDER_LOGO_MAP (line 88) | const PROVIDER_LOGO_MAP: Record<string, string> = {
constant PROVIDER_INFO (line 107) | const PROVIDER_INFO: Record<
constant SUGGESTED_MODELS (line 185) | const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
function generateId (line 356) | function generateId(): string {
function createEmptyConfig (line 361) | function createEmptyConfig(): MultiModelConfig {
function createProviderConfig (line 370) | function createProviderConfig(provider: ProviderName): ProviderConfig {
function createModelConfig (line 382) | function createModelConfig(modelId: string): ModelConfig {
function flattenModels (line 390) | function flattenModels(config: MultiModelConfig): FlattenedModel[] {
function findModelById (line 425) | function findModelById(
FILE: lib/url-utils.ts
type UrlData (line 4) | interface UrlData {
function extractUrlContent (line 18) | async function extractUrlContent(url: string): Promise<UrlData> {
FILE: lib/use-file-processor.tsx
type FileData (line 13) | interface FileData {
function useFileProcessor (line 23) | function useFileProcessor() {
FILE: lib/use-quota-manager.tsx
type QuotaConfig (line 9) | interface QuotaConfig {
function useQuotaManager (line 21) | function useQuotaManager(config: QuotaConfig): {
FILE: lib/user-id.ts
function getUserIdFromRequest (line 6) | function getUserIdFromRequest(req: Request): string {
FILE: lib/utils.ts
function cn (line 8) | function cn(...inputs: ClassValue[]) {
constant MIN_REAL_DIAGRAM_LENGTH (line 20) | const MIN_REAL_DIAGRAM_LENGTH = 300
function isRealDiagram (line 27) | function isRealDiagram(xml: string | undefined | null): boolean {
constant MAX_XML_SIZE (line 36) | const MAX_XML_SIZE = 1_000_000
constant MAX_DROP_ITERATIONS (line 39) | const MAX_DROP_ITERATIONS = 10
constant STRUCTURAL_ATTRS (line 42) | const STRUCTURAL_ATTRS = [
constant VALID_ENTITIES (line 52) | const VALID_ENTITIES = new Set(["lt", "gt", "amp", "quot", "apos"])
function isMxCellXmlComplete (line 66) | function isMxCellXmlComplete(xml: string | undefined | null): boolean {
function extractCompleteMxCells (line 95) | function extractCompleteMxCells(xml: string | undefined | null): string {
type ParsedTag (line 134) | interface ParsedTag {
function parseXmlTags (line 147) | function parseXmlTags(xml: string): ParsedTag[] {
function formatXML (line 202) | function formatXML(xml: string, indent: string = " "): string {
function convertToLegalXml (line 251) | function convertToLegalXml(xmlString: string): string {
function wrapWithMxFile (line 326) | function wrapWithMxFile(xml: string): string {
function replaceNodes (line 379) | function replaceNodes(currentXML: string, nodes: string): string {
type OperationError (line 479) | interface OperationError {
type ApplyOperationsResult (line 485) | interface ApplyOperationsResult {
function applyDiagramOperations (line 498) | function applyDiagramOperations(
function checkDuplicateAttributes (line 738) | function checkDuplicateAttributes(xml: string): string | null {
function checkDuplicateIds (line 762) | function checkDuplicateIds(xml: string): string | null {
function checkTagMismatches (line 780) | function checkTagMismatches(xml: string): string | null {
function checkCharacterReferences (line 805) | function checkCharacterReferences(xml: string): string | null {
function checkEntityReferences (line 832) | function checkEntityReferences(xml: string): string | null {
function checkNestedMxCells (line 851) | function checkNestedMxCells(xml: string): string | null {
function validateMxCellStructure (line 879) | function validateMxCellStructure(xml: string): string | null {
function autoFixXml (line 985) | function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
function validateAndFixXml (line 1639) | function validateAndFixXml(xml: string): {
function extractDiagramXML (line 1676) | function extractDiagramXML(xml_svg_string: string): string {
FILE: lib/validation-prompts.ts
constant VALIDATION_SYSTEM_PROMPT (line 6) | const VALIDATION_SYSTEM_PROMPT = `You are a diagram quality validator. A...
FILE: lib/validation-schema.ts
type ValidationResult (line 37) | type ValidationResult = z.infer<typeof ValidationResultSchema>
type ValidationIssue (line 38) | type ValidationIssue = ValidationResult["issues"][number]
FILE: packages/mcp-server/src/diagram-operations.ts
type DiagramOperation (line 6) | interface DiagramOperation {
type OperationError (line 12) | interface OperationError {
type ApplyOperationsResult (line 18) | interface ApplyOperationsResult {
function applyDiagramOperations (line 31) | function applyDiagramOperations(
FILE: packages/mcp-server/src/history.ts
constant MAX_HISTORY (line 8) | const MAX_HISTORY = 20
function addHistory (line 11) | function addHistory(sessionId: string, xml: string, svg = ""): number {
function getHistory (line 35) | function getHistory(
function getHistoryEntry (line 41) | function getHistoryEntry(
function clearHistory (line 49) | function clearHistory(sessionId: string): void {
function updateLastHistorySvg (line 53) | function updateLastHistorySvg(sessionId: string, svg: string): boolean {
FILE: packages/mcp-server/src/http-server.ts
constant DRAWIO_BASE_URL (line 17) | const DRAWIO_BASE_URL =
function getOrigin (line 21) | function getOrigin(url: string): string {
constant DRAWIO_ORIGIN (line 30) | const DRAWIO_ORIGIN = getOrigin(DRAWIO_BASE_URL)
constant DEFAULT_DIAGRAM_XML (line 34) | const DEFAULT_DIAGRAM_XML = `<mxfile host="app.diagrams.net"><diagram id...
function normalizeUrl (line 37) | function normalizeUrl(url: string): string {
function isLikelyMcpSessionId (line 42) | function isLikelyMcpSessionId(sessionId: string): boolean {
function getMostRecentSessionId (line 48) | function getMostRecentSessionId(): string | null {
function ensureSessionStateInitialized (line 58) | function ensureSessionStateInitialized(sessionId: string): void {
type SessionState (line 66) | interface SessionState {
constant MAX_PORT (line 80) | const MAX_PORT = 6020
constant SESSION_TTL (line 81) | const SESSION_TTL = 60 * 60 * 1000
function getState (line 83) | function getState(sessionId: string): SessionState | undefined {
function setState (line 87) | function setState(sessionId: string, xml: string, svg?: string): number {
function requestSync (line 103) | function requestSync(sessionId: string): boolean {
function waitForSync (line 114) | async function waitForSync(
function startHttpServer (line 128) | function startHttpServer(port = 6002): Promise<number> {
function stopHttpServer (line 166) | function stopHttpServer(): void {
function cleanupExpiredSessions (line 173) | function cleanupExpiredSessions(): void {
function shutdown (line 186) | function shutdown(): void {
function getServerPort (line 191) | function getServerPort(): number {
function handleRequest (line 195) | function handleRequest(
function handleStateApi (line 242) | function handleStateApi(
function handleHistoryApi (line 309) | function handleHistoryApi(
function handleRestoreApi (line 337) | function handleRestoreApi(
function handleHistorySvgApi (line 383) | function handleHistorySvgApi(
function getHtmlPage (line 416) | function getHtmlPage(sessionId: string): string {
FILE: packages/mcp-server/src/index.ts
class XMLSerializerPolyfill (line 16) | class XMLSerializerPolyfill {
method serializeToString (line 17) | serializeToString(node: any): string {
function gracefulShutdown (line 719) | function gracefulShutdown(reason: string) {
function main (line 743) | async function main() {
FILE: packages/mcp-server/src/xml-validation.ts
constant MAX_XML_SIZE (line 11) | const MAX_XML_SIZE = 1_000_000
constant MAX_DROP_ITERATIONS (line 14) | const MAX_DROP_ITERATIONS = 10
constant STRUCTURAL_ATTRS (line 17) | const STRUCTURAL_ATTRS = [
constant VALID_ENTITIES (line 27) | const VALID_ENTITIES = new Set(["lt", "gt", "amp", "quot", "apos"])
type ParsedTag (line 33) | interface ParsedTag {
function parseXmlTags (line 45) | function parseXmlTags(xml: string): ParsedTag[] {
function checkDuplicateAttributes (line 99) | function checkDuplicateAttributes(xml: string): string | null {
function checkDuplicateIds (line 123) | function checkDuplicateIds(xml: string): string | null {
function checkTagMismatches (line 141) | function checkTagMismatches(xml: string): string | null {
function checkCharacterReferences (line 166) | function checkCharacterReferences(xml: string): string | null {
function checkEntityReferences (line 193) | function checkEntityReferences(xml: string): string | null {
function checkNestedMxCells (line 212) | function checkNestedMxCells(xml: string): string | null {
function validateMxCellStructure (line 244) | function validateMxCellStructure(xml: string): string | null {
function autoFixXml (line 353) | function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
function validateAndFixXml (line 873) | function validateAndFixXml(xml: string): {
function isMxCellXmlComplete (line 912) | function isMxCellXmlComplete(xml: string | undefined | null): boolean {
FILE: proxy.ts
function getLocale (line 7) | function getLocale(request: NextRequest): string | undefined {
function proxy (line 27) | function proxy(request: NextRequest) {
FILE: scripts/afterPack.cjs
function copyDereferenced (line 23) | function copyDereferenced(src, dst) {
FILE: scripts/electron-dev.mjs
constant NEXT_PORT (line 22) | const NEXT_PORT = 6002
constant NEXT_URL (line 23) | const NEXT_URL = `http://localhost:${NEXT_PORT}`
function getUserDataPath (line 28) | function getUserDataPath() {
function loadPresetConfig (line 52) | function loadPresetConfig() {
function waitForServer (line 86) | async function waitForServer(url, timeout = 120000) {
function runCommand (line 110) | function runCommand(command, args, options = {}) {
function startNextServer (line 134) | function startNextServer(presetEnv) {
function main (line 163) | async function main() {
FILE: scripts/prepare-electron-build.mjs
function copyDereferenced (line 29) | function copyDereferenced(src, dst) {
FILE: scripts/test-diagram-operations.mjs
function applyDiagramOperations (line 14) | function applyDiagramOperations(xmlContent, operations) {
function test (line 184) | function test(name, fn) {
function assert (line 196) | function assert(condition, message) {
FILE: tests/e2e/fixtures/diagrams.ts
constant CAT_DIAGRAM_XML (line 6) | const CAT_DIAGRAM_XML = `<mxCell id="cat-head" value="Cat Head" style="e...
constant FLOWCHART_XML (line 14) | const FLOWCHART_XML = `<mxCell id="start" value="Start" style="rounded=1...
constant SINGLE_BOX_XML (line 25) | const SINGLE_BOX_XML = `<mxCell id="box" value="Test Box" style="rounded...
constant TEST_NODE_XML (line 30) | const TEST_NODE_XML = `<mxCell id="test-node-123" value="Test Node" styl...
constant ARCHITECTURE_XML (line 35) | const ARCHITECTURE_XML = `<mxCell id="arch" value="Architecture" style="...
constant NEW_NODE_XML (line 40) | const NEW_NODE_XML = `<mxCell id="new-node" value="New Node" style="roun...
constant TRUNCATED_XML (line 45) | const TRUNCATED_XML = `<mxCell id="node1" value="Start" style="rounded=1...
FILE: tests/e2e/lib/fixtures.ts
function getChatInput (line 32) | function getChatInput(page: Page) {
function getIframe (line 37) | function getIframe(page: Page) {
function getIframeContent (line 42) | function getIframeContent(page: Page) {
function getSettingsButton (line 47) | function getSettingsButton(page: Page) {
function sendMessage (line 56) | async function sendMessage(page: Page, message: string) {
function waitForComplete (line 64) | async function waitForComplete(page: Page, timeout = 15000) {
function waitForCompleteCount (line 69) | async function waitForCompleteCount(
function waitForText (line 80) | async function waitForText(page: Page, text: string, timeout = 15000) {
function openSettings (line 85) | async function openSettings(page: Page) {
type MockResponse (line 94) | interface MockResponse {
function createMultiTurnMock (line 104) | function createMultiTurnMock(responses: MockResponse[]) {
function createTextOnlyMock (line 125) | function createTextOnlyMock(responses: string[]) {
function createMixedMock (line 141) | function createMixedMock(
function createErrorMock (line 171) | function createErrorMock(status: number, error: string) {
function expectBeforeAndAfterReload (line 194) | async function expectBeforeAndAfterReload(
function sleep (line 206) | function sleep(ms: number): Promise<void> {
FILE: tests/e2e/lib/helpers.ts
function createMockSSEResponse (line 9) | function createMockSSEResponse(
function createTextOnlyResponse (line 42) | function createTextOnlyResponse(text: string) {
function createToolErrorResponse (line 63) | function createToolErrorResponse(text: string, errorMessage: string) {
FILE: tests/unit/server-model-config.test.ts
constant ORIGINAL_ENV (line 8) | const ORIGINAL_ENV = { ...process.env }
Condensed preview — 242 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,681K chars).
[
{
"path": ".dockerignore",
"chars": 546,
"preview": "# Dependencies\nnode_modules\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Build output\n.next\nout\ndist\nbuild\n\n# Testi"
},
{
"path": ".eslintrc.json",
"chars": 63,
"preview": "{\n \"extends\": [\"next/core-web-vitals\", \"next/typescript\"]\n}\n"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 2074,
"preview": "# Contributing\n\n## Setup\n\n```bash\ngit clone https://github.com/YOUR_USERNAME/next-ai-draw-io.git\ncd next-ai-draw-io\nnpm "
},
{
"path": ".github/FUNDING.yml",
"chars": 855,
"preview": "# These are supported funding model platforms\n\ngithub: dayuanjiang\npatreon: # Replace with a single Patreon username\nope"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 768,
"preview": "---\nname: Bug Report\nabout: Report a bug to help us improve\ntitle: '[Bug] '\nlabels: bug\nassignees: ''\n---\n\n> **Note**: T"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 200,
"preview": "blank_issues_enabled: true\ncontact_links:\n - name: Discussions\n url: https://github.com/DayuanJiang/next-ai-draw-io/"
},
{
"path": ".github/ISSUE_TEMPLATE/enhancement.md",
"chars": 669,
"preview": "---\nname: Enhancement\nabout: Suggest an improvement to existing functionality\ntitle: '[Enhancement] '\nlabels: enhancemen"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 705,
"preview": "---\nname: Feature Request\nabout: Suggest a new feature for this project\ntitle: '[Feature] '\nlabels: enhancement\nassignee"
},
{
"path": ".github/renovate.json",
"chars": 1249,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\"config:recommended\"],\n \"schedu"
},
{
"path": ".github/workflows/auto-format.yml",
"chars": 1867,
"preview": "name: Auto Format\n\non:\n pull_request:\n types: [opened, synchronize]\n\npermissions:\n contents: write\n\njobs:\n format:"
},
{
"path": ".github/workflows/ci.yml",
"chars": 754,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n\nconcurrency:\n group: ${{ g"
},
{
"path": ".github/workflows/docker-build.yml",
"chars": 2951,
"preview": "name: Docker Build and Push\n\non:\n push:\n branches:\n - main\n - master\n - dev\n tags:\n - 'v*'\n "
},
{
"path": ".github/workflows/electron-release.yml",
"chars": 3247,
"preview": "name: Electron Release\n\non:\n push:\n tags:\n - \"v*\"\n workflow_dispatch:\n inputs:\n version:\n descr"
},
{
"path": ".github/workflows/test.yml",
"chars": 1709,
"preview": "name: Test\n\non:\n pull_request:\n branches: [main]\n push:\n branches: [main]\n\njobs:\n lint-and-unit:\n name: Lint"
},
{
"path": ".gitignore",
"chars": 922,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\npack"
},
{
"path": ".husky/pre-commit",
"chars": 33,
"preview": "npx lint-staged\nnpx tsc --noEmit\n"
},
{
"path": ".husky/pre-push",
"chars": 122,
"preview": "# Skip if node_modules not installed (e.g., on EC2 push server)\nif [ -d \"node_modules\" ]; then\n npm run test -- --run\nf"
},
{
"path": ".vscode/settings.json",
"chars": 623,
"preview": "{\n \"editor.formatOnSave\": true,\n \"editor.defaultFormatter\": \"biomejs.biome\",\n \"editor.codeActionsOnSave\": {\n "
},
{
"path": "Dockerfile",
"chars": 2064,
"preview": "# Multi-stage Dockerfile for Next.js\n\n# Stage 1: Install dependencies\nFROM node:24-alpine AS deps\nRUN apk add --no-cache"
},
{
"path": "LICENSE",
"chars": 10761,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 10924,
"preview": "# Next AI Draw.io\n\n<div align=\"center\">\n\n**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**\n\nEnglish | [中文](./"
},
{
"path": "app/[lang]/about/cn/page.tsx",
"chars": 18528,
"preview": "import type { Metadata } from \"next\"\nimport Link from \"next/link\"\nimport { FaGithub } from \"react-icons/fa\"\nimport Image"
},
{
"path": "app/[lang]/about/ja/page.tsx",
"chars": 18509,
"preview": "import type { Metadata } from \"next\"\nimport Link from \"next/link\"\nimport { FaGithub } from \"react-icons/fa\"\nimport Image"
},
{
"path": "app/[lang]/about/page.tsx",
"chars": 20668,
"preview": "import type { Metadata } from \"next\"\nimport Link from \"next/link\"\nimport { FaGithub } from \"react-icons/fa\"\nimport Image"
},
{
"path": "app/[lang]/layout.tsx",
"chars": 6323,
"preview": "import { GoogleAnalytics } from \"@next/third-parties/google\"\nimport type { Metadata, Viewport } from \"next\"\nimport { Jet"
},
{
"path": "app/[lang]/page.tsx",
"chars": 10320,
"preview": "\"use client\"\nimport { usePathname, useRouter } from \"next/navigation\"\nimport { Suspense, useCallback, useEffect, useRef,"
},
{
"path": "app/api/chat/route.ts",
"chars": 34682,
"preview": "import {\n APICallError,\n convertToModelMessages,\n createUIMessageStream,\n createUIMessageStreamResponse,\n "
},
{
"path": "app/api/chat/xml_guide.md",
"chars": 10668,
"preview": "# Draw.io XML Schema Guide\n\nThis guide explains the structure of draw.io (diagrams.net) XML files to help you understand"
},
{
"path": "app/api/config/route.ts",
"chars": 370,
"preview": "import { NextResponse } from \"next/server\"\n\nexport async function GET() {\n return NextResponse.json({\n accessC"
},
{
"path": "app/api/log-feedback/route.ts",
"chars": 3914,
"preview": "import { randomUUID } from \"crypto\"\nimport { z } from \"zod\"\nimport { getLangfuseClient } from \"@/lib/langfuse\"\nimport { "
},
{
"path": "app/api/log-save/route.ts",
"chars": 2350,
"preview": "import { randomUUID } from \"crypto\"\nimport { z } from \"zod\"\nimport { getLangfuseClient } from \"@/lib/langfuse\"\n\nconst sa"
},
{
"path": "app/api/parse-url/route.ts",
"chars": 4190,
"preview": "import { extract } from \"@extractus/article-extractor\"\nimport { NextResponse } from \"next/server\"\nimport TurndownService"
},
{
"path": "app/api/server-models/route.ts",
"chars": 480,
"preview": "import { NextResponse } from \"next/server\"\nimport { loadFlattenedServerModels } from \"@/lib/server-model-config\"\n\n// Use"
},
{
"path": "app/api/validate-diagram/route.ts",
"chars": 4361,
"preview": "/**\n * API endpoint for VLM-based diagram validation.\n * Accepts a PNG image and streams validation results using useObj"
},
{
"path": "app/api/validate-model/route.ts",
"chars": 14953,
"preview": "import { createAmazonBedrock } from \"@ai-sdk/amazon-bedrock\"\nimport { createAnthropic } from \"@ai-sdk/anthropic\"\nimport "
},
{
"path": "app/api/verify-access-code/route.ts",
"chars": 915,
"preview": "export async function POST(req: Request) {\n const accessCodes =\n process.env.ACCESS_CODE_LIST?.split(\",\")\n "
},
{
"path": "app/globals.css",
"chars": 11018,
"preview": "@import \"tailwindcss\";\n\n@plugin \"tailwindcss-animate\";\n@plugin \"@tailwindcss/typography\";\n\n@custom-variant dark (&:is(.d"
},
{
"path": "app/manifest.ts",
"chars": 983,
"preview": "import type { MetadataRoute } from \"next\"\nimport { getAssetUrl } from \"@/lib/base-path\"\nexport default function manifest"
},
{
"path": "app/robots.ts",
"chars": 296,
"preview": "import type { MetadataRoute } from \"next\"\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n "
},
{
"path": "app/sitemap.ts",
"chars": 482,
"preview": "import type { MetadataRoute } from \"next\"\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n return [\n "
},
{
"path": "biome.json",
"chars": 2090,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/2.4.4/schema.json\",\n \"vcs\": {\n \"enabled\": true,\n \"clientK"
},
{
"path": "components/ai-elements/model-selector.tsx",
"chars": 6904,
"preview": "import { Cloud } from \"lucide-react\"\nimport type { ComponentProps, ElementRef, ReactNode } from \"react\"\nimport { useEffe"
},
{
"path": "components/ai-elements/reasoning.tsx",
"chars": 5903,
"preview": "\"use client\"\n\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\"\nimport { BrainIcon, ChevronD"
},
{
"path": "components/ai-elements/shimmer.tsx",
"chars": 1752,
"preview": "\"use client\"\n\nimport { motion } from \"motion/react\"\nimport {\n type CSSProperties,\n type ElementType,\n type JSX,"
},
{
"path": "components/button-with-tooltip.tsx",
"chars": 995,
"preview": "import type { VariantProps } from \"class-variance-authority\"\nimport type React from \"react\"\nimport { Button, type button"
},
{
"path": "components/chat/ChatLobby.tsx",
"chars": 11764,
"preview": "\"use client\"\n\nimport {\n ChevronDown,\n ChevronUp,\n MessageSquare,\n Search,\n Trash2,\n X,\n} from \"lucide-"
},
{
"path": "components/chat/ToolCallCard.tsx",
"chars": 10869,
"preview": "\"use client\"\n\nimport { Check, ChevronDown, ChevronUp, Copy, Cpu } from \"lucide-react\"\nimport type { Dispatch, SetStateAc"
},
{
"path": "components/chat/ValidationCard.tsx",
"chars": 13665,
"preview": "\"use client\"\n\nimport {\n AlertTriangle,\n Check,\n ChevronDown,\n ChevronUp,\n Eye,\n ImageIcon,\n Refresh"
},
{
"path": "components/chat/types.ts",
"chars": 343,
"preview": "export interface DiagramOperation {\n operation: \"update\" | \"add\" | \"delete\"\n cell_id: string\n new_xml?: string\n"
},
{
"path": "components/chat-example-panel.tsx",
"chars": 8709,
"preview": "\"use client\"\n\nimport {\n Cloud,\n FileText,\n GitBranch,\n Palette,\n Terminal,\n Zap,\n} from \"lucide-react\""
},
{
"path": "components/chat-input.tsx",
"chars": 22792,
"preview": "\"use client\"\n\nimport {\n Download,\n History,\n Image as ImageIcon,\n Link,\n Send,\n Square,\n} from \"lucide"
},
{
"path": "components/chat-message-display.tsx",
"chars": 83629,
"preview": "\"use client\"\n\nimport type { UIMessage } from \"ai\"\n\nimport {\n Check,\n ChevronDown,\n ChevronUp,\n Copy,\n Fil"
},
{
"path": "components/chat-panel.tsx",
"chars": 56581,
"preview": "\"use client\"\n\nimport { useChat } from \"@ai-sdk/react\"\nimport { DefaultChatTransport } from \"ai\"\nimport {\n MessageSqua"
},
{
"path": "components/code-block.tsx",
"chars": 1942,
"preview": "\"use client\"\n\nimport { Highlight, themes } from \"prism-react-renderer\"\n\ninterface CodeBlockProps {\n code: string\n "
},
{
"path": "components/dev-xml-simulator.tsx",
"chars": 21278,
"preview": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { useDictionary } from \"@/hooks/use-dictionary\""
},
{
"path": "components/error-toast.tsx",
"chars": 1630,
"preview": "\"use client\"\n\nimport type React from \"react\"\n\ninterface ErrorToastProps {\n message: React.ReactNode\n onDismiss: ()"
},
{
"path": "components/file-preview-list.tsx",
"chars": 11877,
"preview": "\"use client\"\n\nimport { FileCode, FileText, Link, Loader2, X } from \"lucide-react\"\nimport { useEffect, useRef, useState }"
},
{
"path": "components/history-dialog.tsx",
"chars": 4553,
"preview": "\"use client\"\n\nimport { useState } from \"react\"\nimport Image from \"@/components/image-with-basepath\"\nimport { Button } fr"
},
{
"path": "components/image-with-basepath.tsx",
"chars": 513,
"preview": "import NextImage, { type ImageProps } from \"next/image\"\nimport { forwardRef } from \"react\"\nimport { getAssetUrl } from \""
},
{
"path": "components/model-config-dialog.tsx",
"chars": 116975,
"preview": "\"use client\"\n\nimport {\n AlertCircle,\n Check,\n ChevronRight,\n Clock,\n Cloud,\n Eye,\n EyeOff,\n Key,"
},
{
"path": "components/model-selector.tsx",
"chars": 23119,
"preview": "\"use client\"\n\nimport {\n AlertTriangle,\n Bot,\n Check,\n ChevronDown,\n Monitor,\n Server,\n Settings2,\n "
},
{
"path": "components/quota-limit-toast.tsx",
"chars": 5913,
"preview": "\"use client\"\n\nimport { Coffee, Settings, X } from \"lucide-react\"\nimport type React from \"react\"\nimport { FaGithub } from"
},
{
"path": "components/reset-warning-modal.tsx",
"chars": 1398,
"preview": "\"use client\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogDescript"
},
{
"path": "components/save-dialog.tsx",
"chars": 4994,
"preview": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n D"
},
{
"path": "components/settings-dialog.tsx",
"chars": 25642,
"preview": "\"use client\"\n\nimport { ChevronRight, Github, Info, Moon, Sun, Tag } from \"lucide-react\"\nimport { usePathname, useRouter,"
},
{
"path": "components/ui/alert-dialog.tsx",
"chars": 3864,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimpor"
},
{
"path": "components/ui/button.tsx",
"chars": 2123,
"preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
},
{
"path": "components/ui/collapsible.tsx",
"chars": 800,
"preview": "\"use client\"\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nfunction Collapsible({\n ...props\n}: "
},
{
"path": "components/ui/command.tsx",
"chars": 5337,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { SearchIcon } fr"
},
{
"path": "components/ui/dialog.tsx",
"chars": 4326,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } "
},
{
"path": "components/ui/input.tsx",
"chars": 1363,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.Co"
},
{
"path": "components/ui/label.tsx",
"chars": 611,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\n\nimport { cn } from"
},
{
"path": "components/ui/popover.tsx",
"chars": 1635,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } "
},
{
"path": "components/ui/resizable.tsx",
"chars": 2028,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport * as ResizablePrimit"
},
{
"path": "components/ui/scroll-area.tsx",
"chars": 1688,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport "
},
{
"path": "components/ui/select.tsx",
"chars": 6295,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CheckIco"
},
{
"path": "components/ui/switch.tsx",
"chars": 1177,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\"\n\nimport { cn } fr"
},
{
"path": "components/ui/textarea.tsx",
"chars": 759,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Textarea({ className, ...props }: React.Compo"
},
{
"path": "components/ui/tooltip.tsx",
"chars": 1891,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } "
},
{
"path": "components/url-input-dialog.tsx",
"chars": 3513,
"preview": "\"use client\"\n\nimport { Link, Loader2 } from \"lucide-react\"\nimport { useState } from \"react\"\nimport { Button } from \"@/co"
},
{
"path": "components.json",
"chars": 485,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"ta"
},
{
"path": "contexts/diagram-context.tsx",
"chars": 14792,
"preview": "\"use client\"\n\nimport type React from \"react\"\nimport { createContext, useContext, useEffect, useRef, useState } from \"rea"
},
{
"path": "docker-compose.yml",
"chars": 499,
"preview": "services:\n drawio:\n image: jgraph/drawio:latest\n ports: [\"8080:8080\"]\n next-ai-draw-io:\n build:\n context"
},
{
"path": "docs/cn/FAQ.md",
"chars": 1405,
"preview": "# 常见问题解答 (FAQ)\n\n---\n\n## 1. 无法导出 PDF\n\n**问题**: Web 版点击导出 PDF 后跳转到 `convert.diagrams.net/node/export` 然后无响应\n\n**原因**: 嵌入式 Dr"
},
{
"path": "docs/cn/README_CN.md",
"chars": 7470,
"preview": "# Next AI Draw.io\n\n<div align=\"center\">\n\n**AI驱动的图表创建工具 - 对话、绘制、可视化**\n\n[English](../../README.md) | 中文 | [日本語](../ja/READ"
},
{
"path": "docs/cn/ai-providers.md",
"chars": 6841,
"preview": "# AI 提供商配置\n\n本指南介绍如何为 next-ai-draw-io 配置不同的 AI 模型提供商。\n\n## 快速开始\n\n1. 将 `.env.example` 复制为 `.env.local`\n2. 设置所选提供商的 API 密钥\n3"
},
{
"path": "docs/cn/cloudflare-deploy.md",
"chars": 4523,
"preview": "# 部署到 Cloudflare Workers\n\n本项目可以通过 **OpenNext 适配器** 部署为 **Cloudflare Worker**,为您提供:\n\n- 全球边缘部署\n- 极低延迟\n- 免费的 `workers.dev` "
},
{
"path": "docs/cn/docker.md",
"chars": 653,
"preview": "# 使用 Docker 运行\n\n如果您只是想在本地运行,最好的方式是使用 Docker。\n\n首先,如果您尚未安装 Docker,请先安装:[获取 Docker](https://docs.docker.com/get-docker/)\n\n然"
},
{
"path": "docs/cn/offline-deployment.md",
"chars": 815,
"preview": "# 离线部署\n\n通过自托管 draw.io 来替代 `embed.diagrams.net`,从而离线部署 Next AI Draw.io。\n\n**注意:** `NEXT_PUBLIC_DRAWIO_BASE_URL` 是一个**构建时**"
},
{
"path": "docs/en/FAQ.md",
"chars": 2268,
"preview": "# Frequently Asked Questions (FAQ)\n\n---\n\n## 1. Cannot Export PDF\n\n**Problem**: Web version redirects to `convert.diagram"
},
{
"path": "docs/en/ai-providers.md",
"chars": 9900,
"preview": "# AI Provider Configuration\n\nThis guide explains how to configure different AI model providers for next-ai-draw-io.\n\n## "
},
{
"path": "docs/en/cloudflare-deploy.md",
"chars": 5975,
"preview": "# Deploy on Cloudflare Workers\n\nThis project can be deployed as a **Cloudflare Worker** using the **OpenNext adapter**, "
},
{
"path": "docs/en/docker.md",
"chars": 1575,
"preview": "# Run with Docker\n\nIf you just want to run it locally, the best way is to use Docker.\n\nFirst, install Docker if you have"
},
{
"path": "docs/en/offline-deployment.md",
"chars": 1048,
"preview": "# Offline Deployment\n\nDeploy Next AI Draw.io offline by self-hosting draw.io to replace `embed.diagrams.net`.\n\n**Note:**"
},
{
"path": "docs/ja/FAQ.md",
"chars": 1648,
"preview": "# よくある質問 (FAQ)\n\n---\n\n## 1. PDFをエクスポートできない\n\n**問題**: Web版でPDFエクスポートをクリックすると `convert.diagrams.net/node/export` にリダイレクトされ、そ"
},
{
"path": "docs/ja/README_JA.md",
"chars": 8190,
"preview": "# Next AI Draw.io\n\n<div align=\"center\">\n\n**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**\n\n[English](../../README.md) | [中文](../cn/REA"
},
{
"path": "docs/ja/ai-providers.md",
"chars": 7581,
"preview": "# AIプロバイダーの設定\n\nこのガイドでは、next-ai-draw-io でさまざまな AI モデルプロバイダーを設定する方法について説明します。\n\n## クイックスタート\n\n1. `.env.example` を `.env.loca"
},
{
"path": "docs/ja/cloudflare-deploy.md",
"chars": 5092,
"preview": "# Cloudflare Workers へのデプロイ\n\nこのプロジェクトは **OpenNext アダプター** を使用して **Cloudflare Worker** としてデプロイすることができ、以下のメリットがあります:\n\n- グロ"
},
{
"path": "docs/ja/docker.md",
"chars": 747,
"preview": "# Dockerで実行する\n\nローカルで実行したいだけであれば、Dockerを使用するのが最も良い方法です。\n\nまず、Dockerがまだインストールされていない場合はインストールしてください: [Dockerを入手](https://doc"
},
{
"path": "docs/ja/offline-deployment.md",
"chars": 915,
"preview": "# オフラインデプロイ\n\n`embed.diagrams.net` の代わりに draw.io をセルフホストすることで、Next AI Draw.io をオフライン環境にデプロイできます。\n\n**注:** `NEXT_PUBLIC_DRA"
},
{
"path": "docs/shape-libraries/README.md",
"chars": 4861,
"preview": "# Draw.io Shape Libraries\n\nReference: `style=\"shape=mxgraph.<library>.<shape_name>\"`\n\n## Cloud Providers\n\n| Library | Sh"
},
{
"path": "docs/shape-libraries/alibaba_cloud.md",
"chars": 8043,
"preview": "# alibaba_cloud\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.alibaba_cloud`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" st"
},
{
"path": "docs/shape-libraries/android.md",
"chars": 1304,
"preview": "# android\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.android`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=m"
},
{
"path": "docs/shape-libraries/arrows2.md",
"chars": 613,
"preview": "# arrows2\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.arrows2`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=m"
},
{
"path": "docs/shape-libraries/atlassian.md",
"chars": 663,
"preview": "# atlassian\n\n**Type:** SVG images\n**Path:** `img/lib/atlassian/`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"image;as"
},
{
"path": "docs/shape-libraries/aws4.md",
"chars": 22494,
"preview": "# aws4\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.aws4`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgraph"
},
{
"path": "docs/shape-libraries/azure2.md",
"chars": 11463,
"preview": "# azure2\n\n**Type:** SVG images\n**Path:** `img/lib/azure2/`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"image;aspect=f"
},
{
"path": "docs/shape-libraries/basic.md",
"chars": 744,
"preview": "# basic\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.basic`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgra"
},
{
"path": "docs/shape-libraries/bpmn.md",
"chars": 1243,
"preview": "# bpmn\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.bpmn`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgraph"
},
{
"path": "docs/shape-libraries/cabinets.md",
"chars": 1490,
"preview": "# cabinets\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.cabinets`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape"
},
{
"path": "docs/shape-libraries/cisco19.md",
"chars": 4860,
"preview": "# cisco19\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.cisco19`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=m"
},
{
"path": "docs/shape-libraries/citrix.md",
"chars": 2007,
"preview": "# citrix\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.citrix`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxg"
},
{
"path": "docs/shape-libraries/electrical.md",
"chars": 825,
"preview": "# electrical\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.electrical`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"s"
},
{
"path": "docs/shape-libraries/floorplan.md",
"chars": 914,
"preview": "# floorplan\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.floorplan`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"sha"
},
{
"path": "docs/shape-libraries/flowchart.md",
"chars": 869,
"preview": "# flowchart\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.flowchart`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"sha"
},
{
"path": "docs/shape-libraries/fluidpower.md",
"chars": 3093,
"preview": "# fluidpower\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.fluid_power`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\""
},
{
"path": "docs/shape-libraries/gcp2.md",
"chars": 5951,
"preview": "# gcp2\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.gcp2`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgraph"
},
{
"path": "docs/shape-libraries/infographic.md",
"chars": 525,
"preview": "# infographic\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.infographic`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style="
},
{
"path": "docs/shape-libraries/kubernetes.md",
"chars": 761,
"preview": "# kubernetes\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.kubernetes`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"s"
},
{
"path": "docs/shape-libraries/lean_mapping.md",
"chars": 585,
"preview": "# lean_mapping\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.lean_mapping`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" styl"
},
{
"path": "docs/shape-libraries/material_design.md",
"chars": 5383,
"preview": "# material_design\n\n**Type:** SVG images (Google Material Icons CDN)\n**URL Pattern:** `https://fonts.gstatic.com/s/i/mate"
},
{
"path": "docs/shape-libraries/mscae.md",
"chars": 601,
"preview": "# mscae\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.mscae`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgra"
},
{
"path": "docs/shape-libraries/network.md",
"chars": 1189,
"preview": "# network\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.networks`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape="
},
{
"path": "docs/shape-libraries/openstack.md",
"chars": 786,
"preview": "# openstack\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.openstack`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"sha"
},
{
"path": "docs/shape-libraries/pid.md",
"chars": 579,
"preview": "# pid\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.pid2valves`, `mxgraph.pid2inst`, `mxgraph.pid2misc`\n\n## Usage\n\n```x"
},
{
"path": "docs/shape-libraries/rack.md",
"chars": 1169,
"preview": "# rack\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.rack`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgraph"
},
{
"path": "docs/shape-libraries/salesforce.md",
"chars": 1841,
"preview": "# salesforce\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.salesforce`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"s"
},
{
"path": "docs/shape-libraries/sap.md",
"chars": 4835,
"preview": "# sap\n\n**Type:** SVG images\n**Path:** `img/lib/sap/`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"image;aspect=fixed;i"
},
{
"path": "docs/shape-libraries/sitemap.md",
"chars": 875,
"preview": "# sitemap\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.sitemap`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=m"
},
{
"path": "docs/shape-libraries/vvd.md",
"chars": 1992,
"preview": "# vvd\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.vvd`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape=mxgraph.v"
},
{
"path": "docs/shape-libraries/webicons.md",
"chars": 2568,
"preview": "# webicons\n\n**Type:** mxgraph shapes\n**Prefix:** `mxgraph.webicons`\n\n## Usage\n\n```xml\n<mxCell value=\"label\" style=\"shape"
},
{
"path": "edge-functions/api/edgeai/chat/completions.ts",
"chars": 8556,
"preview": "/**\n * EdgeOne Pages Edge Function for OpenAI-compatible Chat Completions API\n *\n * This endpoint provides an OpenAI-com"
},
{
"path": "edgeone.json",
"chars": 66,
"preview": "{\n \"nodeFunctionsConfig\": {\n \"maxDuration\": 120\n }\n}\n"
},
{
"path": "electron/electron-builder.yml",
"chars": 1958,
"preview": "appId: com.nextaidrawio.app\nproductName: Next AI Draw.io\ncopyright: Copyright © 2024 Next AI Draw.io\nelectronVersion: 39"
},
{
"path": "electron/electron.d.ts",
"chars": 3326,
"preview": "/**\n * Type declarations for Electron API exposed via preload script\n */\n\n/** Configuration preset interface */\ninterfac"
},
{
"path": "electron/main/app-menu.ts",
"chars": 8219,
"preview": "import {\n app,\n BrowserWindow,\n dialog,\n Menu,\n type MenuItemConstructorOptions,\n shell,\n} from \"elect"
},
{
"path": "electron/main/config-manager.ts",
"chars": 12934,
"preview": "import { randomUUID } from \"node:crypto\"\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nim"
},
{
"path": "electron/main/env-loader.ts",
"chars": 2076,
"preview": "import fs from \"node:fs\"\nimport path from \"node:path\"\nimport { app } from \"electron\"\n\n/**\n * Load environment variables "
},
{
"path": "electron/main/index.ts",
"chars": 3451,
"preview": "import { app, BrowserWindow, dialog, shell } from \"electron\"\nimport { buildAppMenu } from \"./app-menu\"\nimport { getCurre"
},
{
"path": "electron/main/ipc-handlers.ts",
"chars": 8334,
"preview": "import { app, BrowserWindow, dialog, ipcMain } from \"electron\"\nimport { rebuildAppMenu } from \"./app-menu\"\nimport {\n "
},
{
"path": "electron/main/menu-i18n.ts",
"chars": 4626,
"preview": "/**\n * Internationalization support for Electron menu\n * Translations for menu labels that don't use Electron's built-in"
},
{
"path": "electron/main/next-server.ts",
"chars": 5852,
"preview": "import { existsSync } from \"node:fs\"\nimport path from \"node:path\"\nimport { app, type UtilityProcess, utilityProcess } fr"
},
{
"path": "electron/main/port-manager.ts",
"chars": 3443,
"preview": "import net from \"node:net\"\nimport { app } from \"electron\"\n\n/**\n * Port configuration\n * Using fixed ports to preserve lo"
},
{
"path": "electron/main/proxy-manager.ts",
"chars": 2005,
"preview": "import { app } from \"electron\"\nimport * as fs from \"fs\"\nimport * as path from \"path\"\nimport type { ProxyConfig } from \"."
},
{
"path": "electron/main/settings-window.ts",
"chars": 2167,
"preview": "import path from \"node:path\"\nimport { app, BrowserWindow, ipcMain } from \"electron\"\n\nlet settingsWindow: BrowserWindow |"
},
{
"path": "electron/main/window-manager.ts",
"chars": 2296,
"preview": "import path from \"node:path\"\nimport { app, BrowserWindow, screen } from \"electron\"\n\nlet mainWindow: BrowserWindow | null"
},
{
"path": "electron/preload/index.ts",
"chars": 1130,
"preview": "import { contextBridge, ipcRenderer } from \"electron\"\n\n/**\n * Expose safe APIs to the renderer process\n */\ncontextBridge"
},
{
"path": "electron/preload/settings.ts",
"chars": 1140,
"preview": "/**\n * Preload script for settings window\n * Exposes APIs for managing configuration presets\n */\nimport { contextBridge,"
},
{
"path": "electron/settings/index.html",
"chars": 5297,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width"
},
{
"path": "electron/settings/settings.css",
"chars": 6017,
"preview": ":root {\n --bg-primary: #ffffff;\n --bg-secondary: #f5f5f5;\n --bg-hover: #e8e8e8;\n --text-primary: #1a1a1a;\n "
},
{
"path": "electron/settings/settings.js",
"chars": 9886,
"preview": "// Settings page JavaScript\n// This file handles the UI interactions for the settings window\n\nlet presets = []\nlet curre"
},
{
"path": "electron/tsconfig.json",
"chars": 459,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"CommonJS\",\n \"moduleResolution\": \"node\","
},
{
"path": "env.example",
"chars": 8444,
"preview": "# AI Provider Configuration\n# AI_PROVIDER: Which provider to use\n# Options: bedrock, openai, anthropic, google, vertexai"
},
{
"path": "hooks/use-diagram-tool-handlers.ts",
"chars": 21288,
"preview": "import type { MutableRefObject } from \"react\"\nimport { useRef } from \"react\"\nimport type { DiagramOperation } from \"@/co"
},
{
"path": "hooks/use-dictionary.ts",
"chars": 704,
"preview": "\"use client\"\n\nimport React, { createContext, useContext } from \"react\"\nimport type { Dictionary } from \"@/lib/i18n/dicti"
},
{
"path": "hooks/use-model-config.ts",
"chars": 15082,
"preview": "\"use client\"\n\nimport { useCallback, useEffect, useState } from \"react\"\nimport { getApiEndpoint } from \"@/lib/base-path\"\n"
},
{
"path": "hooks/use-session-manager.ts",
"chars": 11384,
"preview": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport {\n type ChatSession,\n create"
},
{
"path": "hooks/use-validate-diagram.ts",
"chars": 4092,
"preview": "\"use client\"\n\n/**\n * Hook for VLM-based diagram validation using AI SDK's useObject.\n */\n\nimport { experimental_useObjec"
},
{
"path": "instrumentation.ts",
"chars": 1312,
"preview": "import { LangfuseSpanProcessor } from \"@langfuse/otel\"\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node"
},
{
"path": "lib/ai-providers.ts",
"chars": 52195,
"preview": "import { createAmazonBedrock } from \"@ai-sdk/amazon-bedrock\"\nimport { createAnthropic } from \"@ai-sdk/anthropic\"\nimport "
},
{
"path": "lib/base-path.ts",
"chars": 1278,
"preview": "/**\n * Get the base path for API calls and static assets\n * This is used for subdirectory deployment support\n *\n * Examp"
},
{
"path": "lib/cached-responses.ts",
"chars": 56731,
"preview": "export interface CachedResponse {\n promptText: string\n hasImage: boolean\n xml: string\n}\n\nexport const CACHED_EX"
},
{
"path": "lib/chat-helpers.ts",
"chars": 3277,
"preview": "// Shared helper functions for chat route\n// Exported for testing\n\n// File upload limits (must match client-side)\nexport"
},
{
"path": "lib/diagram-validator.ts",
"chars": 1958,
"preview": "/**\n * Types and utilities for VLM-based diagram validation.\n * The actual validation is performed via useValidateDiagra"
},
{
"path": "lib/dynamo-quota-manager.ts",
"chars": 9557,
"preview": "import {\n ConditionalCheckFailedException,\n DynamoDBClient,\n GetItemCommand,\n UpdateItemCommand,\n} from \"@aw"
},
{
"path": "lib/i18n/config.ts",
"chars": 157,
"preview": "export const i18n = {\n defaultLocale: \"en\",\n locales: [\"en\", \"zh\", \"ja\", \"zh-Hant\"],\n} as const\n\nexport type Local"
},
{
"path": "lib/i18n/dictionaries/en.json",
"chars": 16806,
"preview": "{\n \"common\": {\n \"save\": \"Save\",\n \"cancel\": \"Cancel\",\n \"close\": \"Close\",\n \"confirm\": \"Conf"
},
{
"path": "lib/i18n/dictionaries/ja.json",
"chars": 13666,
"preview": "{\n \"common\": {\n \"save\": \"保存\",\n \"cancel\": \"キャンセル\",\n \"close\": \"閉じる\",\n \"confirm\": \"確認\",\n "
},
{
"path": "lib/i18n/dictionaries/zh-Hant.json",
"chars": 12259,
"preview": "{\n \"common\": {\n \"save\": \"儲存\",\n \"cancel\": \"取消\",\n \"close\": \"關閉\",\n \"confirm\": \"確認\",\n "
},
{
"path": "lib/i18n/dictionaries/zh.json",
"chars": 12261,
"preview": "{\n \"common\": {\n \"save\": \"保存\",\n \"cancel\": \"取消\",\n \"close\": \"关闭\",\n \"confirm\": \"确认\",\n "
},
{
"path": "lib/i18n/dictionaries.ts",
"chars": 669,
"preview": "import \"server-only\"\n\nimport type { Locale } from \"./config\"\n\nconst dictionaries = {\n en: () => import(\"./dictionarie"
},
{
"path": "lib/i18n/utils.ts",
"chars": 380,
"preview": "export function formatMessage(\n template: string | undefined,\n vars?: Record<string, string | number | undefined>,"
},
{
"path": "lib/langfuse.ts",
"chars": 2445,
"preview": "import { LangfuseClient } from \"@langfuse/client\"\nimport { observe, updateActiveTrace } from \"@langfuse/tracing\"\nimport "
},
{
"path": "lib/pdf-utils.ts",
"chars": 1681,
"preview": "import { extractText, getDocumentProxy } from \"unpdf\"\n\n// Maximum characters allowed for extracted text (configurable vi"
},
{
"path": "lib/server-model-config.ts",
"chars": 5203,
"preview": "import fs from \"fs/promises\"\nimport path from \"path\"\nimport { z } from \"zod\"\nimport type { ProviderName } from \"@/lib/ty"
},
{
"path": "lib/session-storage.ts",
"chars": 10772,
"preview": "import { type DBSchema, type IDBPDatabase, openDB } from \"idb\"\nimport { nanoid } from \"nanoid\"\n\n// Constants\nconst DB_NA"
},
{
"path": "lib/ssrf-protection.ts",
"chars": 1864,
"preview": "/**\n * SSRF (Server-Side Request Forgery) protection utilities\n */\n\n/**\n * Check if URL points to private/internal netwo"
},
{
"path": "lib/storage.ts",
"chars": 1194,
"preview": "// Centralized localStorage keys for quota tracking and settings\n// Chat data is now stored in IndexedDB via session-sto"
},
{
"path": "lib/system-prompts.ts",
"chars": 20886,
"preview": "/**\n * System prompts for different AI models\n * Extended prompt is used for models with higher cache token minimums (Op"
},
{
"path": "lib/types/model-config.ts",
"chars": 12587,
"preview": "// Types for multi-provider model configuration\n\nexport type ProviderName =\n | \"openai\"\n | \"anthropic\"\n | \"goog"
},
{
"path": "lib/url-utils.ts",
"chars": 1353,
"preview": "import { z } from \"zod\"\nimport { getApiEndpoint } from \"@/lib/base-path\"\n\nexport interface UrlData {\n url: string\n "
},
{
"path": "lib/use-file-processor.tsx",
"chars": 3637,
"preview": "\"use client\"\n\nimport { useState } from \"react\"\nimport { toast } from \"sonner\"\nimport {\n extractPdfText,\n extractTe"
},
{
"path": "lib/use-quota-manager.tsx",
"chars": 2847,
"preview": "\"use client\"\n\nimport { useCallback } from \"react\"\nimport { toast } from \"sonner\"\nimport { QuotaLimitToast } from \"@/comp"
},
{
"path": "lib/user-id.ts",
"chars": 489,
"preview": "/**\n * Generate a userId from request for tracking purposes.\n * Uses base64url encoding of IP for URL-safe identifier.\n "
},
{
"path": "lib/utils.ts",
"chars": 64807,
"preview": "import { type ClassValue, clsx } from \"clsx\"\nimport * as pako from \"pako\"\nimport { twMerge } from \"tailwind-merge\"\nimpor"
},
{
"path": "lib/validation-prompts.ts",
"chars": 1352,
"preview": "/**\n * VLM system prompt for diagram validation.\n * Note: Response parsing is now handled via AI SDK's structured output"
},
{
"path": "lib/validation-schema.ts",
"chars": 1259,
"preview": "/**\n * Shared validation schema for VLM-based diagram validation.\n * This file can be safely imported on both client and"
},
{
"path": "next.config.ts",
"chars": 996,
"preview": "import type { NextConfig } from \"next\"\nimport packageJson from \"./package.json\"\n\nconst nextConfig: NextConfig = {\n /*"
},
{
"path": "open-next.config.ts",
"chars": 332,
"preview": "// default open-next.config.ts file created by @opennextjs/cloudflare\nimport { defineCloudflareConfig } from \"@opennextj"
},
{
"path": "package.json",
"chars": 6405,
"preview": "{\n \"name\": \"next-ai-draw-io\",\n \"version\": \"0.4.13\",\n \"license\": \"Apache-2.0\",\n \"private\": true,\n \"main\": "
},
{
"path": "packages/claude-plugin/.claude-plugin/plugin.json",
"chars": 427,
"preview": "{\n \"name\": \"next-ai-drawio\",\n \"version\": \"1.0.0\",\n \"description\": \"AI-powered Draw.io diagram generation with r"
},
{
"path": "packages/claude-plugin/.mcp.json",
"chars": 148,
"preview": "{\n \"mcpServers\": {\n \"drawio\": {\n \"command\": \"npx\",\n \"args\": [\"@next-ai-drawio/mcp-server"
},
{
"path": "packages/claude-plugin/README.md",
"chars": 2923,
"preview": "# Next AI Draw.io - Claude Code Plugin\n\nAI-powered Draw.io diagram generation with real-time browser preview for Claude "
},
{
"path": "packages/mcp-server/README.md",
"chars": 5421,
"preview": "# Next AI Draw.io MCP Server\n\nMCP (Model Context Protocol) server that enables AI agents like Claude Desktop and Cursor "
},
{
"path": "packages/mcp-server/package.json",
"chars": 1394,
"preview": "{\n \"name\": \"@next-ai-drawio/mcp-server\",\n \"version\": \"0.1.16\",\n \"description\": \"MCP server for Next AI Draw.io "
},
{
"path": "packages/mcp-server/src/diagram-operations.ts",
"chars": 8926,
"preview": "/**\n * ID-based diagram operations\n * Copied from lib/utils.ts to avoid cross-package imports\n */\n\nexport interface Diag"
},
{
"path": "packages/mcp-server/src/history.ts",
"chars": 1609,
"preview": "/**\n * Simple diagram history - matches Next.js app pattern\n * Stores {xml, svg} entries in a circular buffer\n */\n\nimpor"
},
{
"path": "packages/mcp-server/src/http-server.ts",
"chars": 40177,
"preview": "/**\n * Embedded HTTP Server for MCP\n * Serves draw.io embed with state sync and history UI\n */\n\nimport http from \"node:h"
},
{
"path": "packages/mcp-server/src/index.ts",
"chars": 27280,
"preview": "#!/usr/bin/env node\n/**\n * MCP Server for Next AI Draw.io\n *\n * Enables AI agents (Claude Desktop, Cursor, etc.) to gene"
},
{
"path": "packages/mcp-server/src/logger.ts",
"chars": 785,
"preview": "/**\n * Logger for MCP server\n *\n * CRITICAL: MCP servers communicate via STDIO (stdin/stdout).\n * Using console.log() wi"
}
]
// ... and 42 more files (download for full content)
About this extraction
This page contains the full source code of the DayuanJiang/next-ai-draw-io GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 242 files (1.5 MB), approximately 361.4k tokens, and a symbol index with 564 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.