Compare commits

...

239 Commits

Author SHA1 Message Date
renovate[bot]
34896aa7f2 fix(deps): update dependency zod to v4 2025-12-31 08:24:25 +00:00
Dayuan Jiang
03ac9a79de fix: detect models that don't support image input and return clear error (#474)
Some models (Kimi K2, DeepSeek, Qwen text models) don't support image/vision
input. The AI SDK silently drops unsupported image parts, causing confusing
responses where the model acts as if no image was uploaded.

Added supportsImageInput() function to detect unsupported models by name,
and return a 400 error with clear guidance when users try to upload images
to these models.

Closes #469
2025-12-31 12:20:09 +09:00
E66Crisp
f97934d6e0 feat(i18n): sync Draw.io panel language with app locale (#473) 2025-12-31 11:48:02 +09:00
E66Crisp
73a36cf9de style(chat-panel): Improve aiChat label display in collapsed panel (#470)
* style(chat-panel): Improve aiChat label display in collapsed panel

* fix: update qs to fix high severity security vulnerability

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-31 11:25:23 +09:00
Dayuan Jiang
69f9df1792 fix: improve image not supported error detection for DeepSeek (#468) 2025-12-31 00:12:19 +09:00
Dayuan Jiang
aaa2938dac docs: reorganize documentation into i18n folder structure (#466)
* docs: reorganize docs into en/cn/ja folders

- Move documentation files into language-specific folders (en, cn, ja)
- Add Chinese and Japanese translations for all docs
- Extract Docker section from README to separate doc file
- Update README to link to new doc locations

* docs: fix links to new docs folder structure

* docs: update README and provider docs

* docs: fix broken import statements in cloudflare deploy guides

* docs: sync CN/JA READMEs with EN structure and fix all paths
2025-12-31 00:04:32 +09:00
broBinChen
24afa0b58a feat: add copy button for tool call blocks (#463)
* feat: add copy button for tool call blocks

* refactor: simplify copy state updates with helper function

---------

Co-authored-by: binge_c-admin <totchinaa@gmail.com>
2025-12-30 23:45:50 +09:00
Dayuan Jiang
1d19127855 chore: remove About link from header and language switcher from about pages (#464)
- Remove About link and sponsor notice icon from chat panel header
- Remove language switcher (English | 中文 | 日本語) from all about pages
- Fix About link in settings dialog to use current language
- Remove unused sponsorTooltip translation key from all dictionaries
2025-12-30 23:45:31 +09:00
zhoujie0531
ca21a5bb27 feat: add EdgeOne Pages as AI provider (#456)
* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* feat: edit diagram

* feat: edit diagram

* feat: edit diagram

* feat: edit diagram

* feat: edit diagram

* feat: edit diagram

* feat: edit diagram

* feat: add edgeone provider

* feat: add edgeone provider

* feat: add edgeone provider

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: add cookie

* fix: add cookie

* fix: add cookie

* fix: add cookie

* fix: add cookie

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* fix: build error

* feat: validate

* feat: document link

---------

Co-authored-by: zoejiezhou <zoejiezhou@tencent.com>
2025-12-30 22:13:22 +09:00
broBinChen
ad80e9c6f5 i18n: add missing translations for chat UI components (#457)
* i18n: add missing translations for chat UI components

* i18n: add missing translations for chat components and toast messages
2025-12-30 20:52:57 +09:00
Biki Kalita
1ab8d260a2 fix: restore locale redirection using Next.js middleware (#462)
* fix: restore locale redirection using Next.js middleware

* fix: use proxy.ts instead of middleware.ts for Next.js 16

---------

Co-authored-by: Dayuan Jiang <jdy.toh@gmail.com>
2025-12-30 20:40:33 +09:00
Dayuan Jiang
2d62496f9f fix(edit_diagram): implement cascade delete for children and edges (#451)
* fix(edit_diagram): implement cascade delete for children and edges

- Add automatic cascade deletion when deleting a cell
- Recursively delete all child cells (parent attribute references)
- Delete all edges referencing deleted cells (source/target)
- Skip silently if cell already deleted (handles AI redundant ops)
- Update prompts to inform AI about cascade behavior

Fixes #450

* fix: add root cell protection and sync MCP server cascade delete

- Add protection for root cells '0' and '1' to prevent full diagram wipe
- Sync MCP server with main app's cascade delete logic
- Both lib/utils.ts and packages/mcp-server now have identical delete behavior

* chore(mcp): bump version to 0.1.9

* fix(cascade-delete): recursively collect edge children (labels)

- Change from cellsToDelete.add(edgeId) to collectDescendants(edgeId)
- Fixes orphaned edge labels causing draw.io to crash/clear canvas
- Edge labels (parent=edgeId) are now deleted with their parent edge
2025-12-30 00:03:30 +09:00
Dayuan Jiang
c2aa7f49be fix(mcp): rename display_diagram to create_new_diagram (#449)
Rename tool to be less ambiguous and help AI models correctly
provide the required xml parameter on subsequent calls.

Closes #445
2025-12-29 15:15:13 +09:00
Dayuan Jiang
30b30550d9 chore: clean up root folder by relocating config files (#448)
* chore: clean up root folder by moving config files

- Move renovate.json to .github/renovate.json
- Move electron-builder.yml to electron/electron-builder.yml
- Move electron.d.ts to electron/electron.d.ts
- Delete proxy.ts (unused dead code)
- Update package.json dist scripts with --config flag
- Use tsconfig.json files array for electron.d.ts (bypasses exclude)

Reduces git-tracked root files from 23 to 19.

* chore: regenerate package-lock.json to fix CI

* fix: regenerate package-lock.json with cross-platform deps
2025-12-29 14:30:25 +09:00
Biki Kalita
49b086cef3 fix: make model selector label responsive to panel width (#443)
* fix: make model selector label responsive to panel width

* Apply suggestion from @DayuanJiang

Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com>

---------

Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com>
2025-12-29 12:54:13 +09:00
Dayuan Jiang
27f26d8b26 feat: improve quota toast with ByteDance Doubao sponsorship info and model config button (#447)
- Add 'Use Your API Key' button to open model config dialog
- Add ByteDance Doubao sponsorship message with registration link
- Update quota limit messages to be warmer and friendlier
- Add dev panel button to test quota toast
- Update i18n translations for EN, ZH, JA
2025-12-29 12:12:22 +09:00
Dayuan Jiang
6d1e12bb39 feat: add doubao provider and ByteDance sponsorship (#329)
* feat: add doubao provider and ByteDance sponsorship

- Add doubao provider using DeepSeek SDK with Volcengine base URL
- Add ByteDance Doubao sponsorship acknowledgment to about pages
- Update all README files (EN/CN/JA) with K2-thinking model info
- Update ai-providers.md with doubao configuration
- Keep both gateway and doubao providers after merge

* style: auto-format with Biome

* feat: add doubao and sglang to provider config panel

* fix: add doubao and sglang to validate-model API and logo maps

* docs: update ByteDance sponsorship note in all README versions

* docs: add Doubao logo to sponsorship note

* fix: use raw GitHub URL for Doubao logo in READMEs

* fix: separate link and image in sponsorship note

* fix: use PNG instead of SVG for Doubao logo

* fix: use current branch for PNG URL (will update to main after merge)

* docs: reorganize Deployment section and update image URLs to main

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-29 11:30:58 +09:00
Biki Kalita
226c336671 feat: move History and Download buttons to Settings dialog for cleaner chat interface (#442)
* fix: move History and Download buttons to Settings dialog for cleaner chat interface

* fix: cleanup unused imports/props, add i18n for diagram style

* fix: use npx directly to avoid package-lock.json changes in CI

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-28 22:16:10 +09:00
renovate[bot]
1527883360 fix(deps): update dependency jsdom to v27 (#438)
* fix(deps): update dependency jsdom to v27

* style: auto-format with Biome

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-28 21:05:23 +09:00
renovate[bot]
641a715d44 chore(deps): update dependency node to v24 (#435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-28 21:05:20 +09:00
renovate[bot]
41184969fa fix(deps): update dependency open to v11 (#439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-28 20:35:42 +09:00
renovate[bot]
c92975f831 chore(deps): update docker/build-push-action action to v6 (#436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-28 20:35:15 +09:00
Biki Kalita
9ac99a4690 [Feature] Add Cloudflare Worker as deployment option (#170)
* docs(cloudflare): add detailed Cloudflare Workers + R2 deploy guide

* separated cloudflare deploy guide from readme.md

* Missing R2 bucket binding for incremental cache

* docs: move Cloudflare guide to docs/ and improve documentation

- Move Cloudflare_Deploy.md to docs/ folder
- Add 'Deploy without R2' option for simple/free deployments
- Add workers.dev subdomain registration instructions
- Add missing global_fetch_strictly_public flag
- Add troubleshooting for common deployment issues
- Update README.md link to new location

* fix: conditional import for cloudflare dev and regenerate lockfile

- Use dynamic import for @opennextjs/cloudflare to avoid loading workerd during builds
- Regenerate package-lock.json with cross-platform dependencies

* fix: use main lockfile with cloudflare deps added

- Use main branch's package-lock.json as base to ensure cross-platform deps
- Add @opennextjs/cloudflare and wrangler

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-27 21:13:23 +09:00
Dayuan Jiang
6d84dade56 chore: add enhancement issue template (#434) 2025-12-27 18:16:38 +09:00
Dayuan Jiang
43f3fbb5ee Merge pull request #432 from DayuanJiang/renovate/aws-actions-configure-aws-credentials-5.x
chore(deps): update aws-actions/configure-aws-credentials action to v5
2025-12-27 14:52:41 +09:00
Dayuan Jiang
1915c817c3 Merge pull request #431 from DayuanJiang/renovate/actions-setup-node-6.x
chore(deps): update actions/setup-node action to v6
2025-12-27 14:52:18 +09:00
Dayuan Jiang
eeab1ba75d Merge pull request #425 from vansh-nagar/fix/theme-based-logo
fix: switch logo file based on dark mode
2025-12-27 14:48:55 +09:00
renovate[bot]
1f4eb02b0b chore(deps): update aws-actions/configure-aws-credentials action to v5 2025-12-27 05:17:32 +00:00
renovate[bot]
5d60ca74f7 chore(deps): update actions/setup-node action to v6 2025-12-27 05:17:29 +00:00
Dayuan Jiang
9fa1dd075b Merge pull request #430 from DayuanJiang/renovate/actions-checkout-6.x
chore(deps): update actions/checkout action to v6
2025-12-27 14:17:03 +09:00
Dayuan Jiang
743b317387 Merge pull request #429 from DayuanJiang/renovate/minor-and-patch-dependencies
fix(deps): update minor and patch dependencies
2025-12-27 14:16:37 +09:00
github-actions[bot]
5ed23784e7 style: auto-format with Biome 2025-12-27 04:35:31 +00:00
renovate[bot]
3a22e11651 chore(deps): update actions/checkout action to v6 2025-12-27 04:34:58 +00:00
renovate[bot]
eb89b9c052 fix(deps): update minor and patch dependencies 2025-12-27 04:34:47 +00:00
Dayuan Jiang
9c1117e8b0 Merge pull request #427 from DayuanJiang/renovate/core-framework-packages
chore(deps): update core framework packages
2025-12-27 13:33:41 +09:00
Dayuan Jiang
39bf3d6a49 Merge pull request #428 from DayuanJiang/renovate/radix-ui-packages
chore(deps): update radix ui packages
2025-12-27 13:33:32 +09:00
github-actions[bot]
ecd689162f style: auto-format with Biome 2025-12-27 01:26:41 +00:00
github-actions[bot]
7a03aec9be style: auto-format with Biome 2025-12-27 01:26:31 +00:00
renovate[bot]
95541dd284 chore(deps): update radix ui packages 2025-12-27 01:26:04 +00:00
renovate[bot]
49af6676b5 chore(deps): update core framework packages 2025-12-27 01:25:44 +00:00
Dayuan Jiang
18ab1bffa0 feat: migrate DynamoDB quota to composite key schema (#426)
- Change from single key (PK only) to composite key (PK + SK)
- PK = user ID, SK = date for per-day history tracking
- Remove two-step daily reset logic (SK handles day separation)
- Rename dailyReqCount/dailyTokenCount to reqCount/tokenCount
- Remove TTL (data never expires per user request)
- Simplify checkAndIncrementRequest to single atomic update
- Fix recordTokenUsage to handle new items explicitly

New table: next-ai-drawio-quota-v2
2025-12-27 10:24:43 +09:00
vansh-nagar
571ba3c6b0 fix: switch app logo based on theme 2025-12-26 17:16:12 +05:30
Divyesh
467561df47 docs(shape-libraries): add label positioning to shape library examples (#422)
- Add verticalLabelPosition=bottom, verticalAlign=top, and align=center to all shape library usage examples
- Update Alibaba Cloud shape library documentation
- Update Atlassian shape library documentation
- Update AWS4 shape library documentation
- Update Azure2 shape library documentation
- Update Cisco19 shape library documentation
- Update Citrix shape library documentation
- Update GCP2 shape library documentation
- Update Kubernetes shape library documentation
- Update MSCAE shape library documentation
- Update Network shape library documentation
- Update OpenStack shape library documentation
- Update Salesforce shape library documentation
- Update SAP shape library documentation
- Update VVD shape library documentation
- Update WebIcons shape library documentation
- Ensures consistent label positioning and alignment across all shape library examples for better visual consistency
2025-12-26 16:57:26 +09:00
Biki Kalita
e67ab37383 docs: fix cross-domain configuration to offline deployment docs (#405)
* docs: add cross-domain troubleshooting to offline deployment guide

* make it simple

* Remove common issues section from offline deployment docs

Removed common issues section regarding cross-domain configuration and rebuilding after configuration changes.
2025-12-26 16:52:56 +09:00
xunc lee
31644dbcd8 feat: add toggle to show unvalidated models in model selector (#413)
* feat: add toggle to show unvalidated models in model selector

Add a toggle switch in the model configuration dialog to allow users to
display models that haven't been validated. This helps users who work with
model providers that have disabled their verification endpoints.

Changes:
- Add showUnvalidatedModels field to MultiModelConfig type
- Add setShowUnvalidatedModels method to useModelConfig hook
- Add Switch toggle in model-config-dialog footer
- Update model-selector to filter based on showUnvalidatedModels setting
- Add warning icon for unvalidated models in the selector
- Add i18n translations for en/zh/ja

Closes #410

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: wrap AlertTriangle in span for title attribute

The AlertTriangle icon from lucide-react doesn't support the title prop directly.
Wrapped it in a span element to properly display the tooltip.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 12:19:59 +09:00
Dayuan Jiang
067d309927 fix: handle fork PRs in auto-format workflow (#419)
- Use head.sha instead of head_ref for checkout (works for forks)
- For fork PRs: fail with helpful message if formatting needed
- For same-repo PRs: auto-commit and push as before
2025-12-26 12:15:31 +09:00
Dayuan Jiang
d1d0de3dea chore: bump version to 0.4.7 (#416)
* chore: bump version to 0.4.7

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-25 22:30:48 +09:00
Dayuan Jiang
8c736cee0d fix: persist settings in Electron by using fixed port (#415)
- Use fixed port 61337 in production instead of random ports (10000-65535)
- localStorage is origin-specific, so random ports caused settings loss
- Add locale save/restore since language is URL-based
- Fixes #399
2025-12-25 22:20:59 +09:00
Dayuan Jiang
c5a04c9e50 feat: move delete provider button to header area (#412) 2025-12-25 19:52:07 +09:00
Dayuan Jiang
44c453403f fix: reset test button to idle state when switching providers (#411)
- Button now shows 'Test' by default instead of persisting 'Verified' state
- Verified status is still shown via green badge in provider header
- Updated OpenAI suggested models list with latest GPT-5.x series
2025-12-25 19:39:15 +09:00
Dayuan Jiang
9727aa5b39 chore: add CI workflow and Renovate configuration (#406) 2025-12-25 15:36:40 +09:00
Dayuan Jiang
51858dbf5d Add deprecation notice to Electron settings panel (#403)
- Add warning banner to settings window HTML
- Add CSS styling for deprecation notice (light/dark mode)
- Direct users to use AI Model Configuration button in chat panel
2025-12-25 13:56:07 +09:00
Dayuan Jiang
3047d19238 fix: rename edit_diagram type field to operation for better model compatibility (#402)
Fixes #374 - Models were confused by the `type` field name and sent
`operation` instead. This change:

- Renames DiagramOperation.type to DiagramOperation.operation across
  all files (MCP server, web app, hooks, components, system prompts)
- Adds JSON examples in tool descriptions to show correct format
- Updates all test data to use the new field name

Affected files:
- lib/utils.ts
- app/api/chat/route.ts
- hooks/use-diagram-tool-handlers.ts
- components/chat-message-display.tsx
- lib/system-prompts.ts
- packages/mcp-server/src/diagram-operations.ts
- packages/mcp-server/src/index.ts
- scripts/test-diagram-operations.mjs

MCP server version bumped to 0.1.6
2025-12-25 13:19:04 +09:00
Dayuan Jiang
ed069afdea fix: use full IP for userId to prevent quota collision (#400)
* fix: use full IP for userId to prevent quota collision

- Remove .slice(0, 8) from base64 encoded IP
- Each IP now has unique userId (no /16 collision)
- Affects: quota tracking, Langfuse tracing

* refactor: extract getUserIdFromRequest to shared utility

- Create lib/user-id.ts with shared function
- Fix misleading 'privacy' comment (base64 is not privacy)
- Remove duplicate code from chat and log-feedback routes
2025-12-25 12:20:46 +09:00
Biki Kalita
d2e5afb298 Hide scrollbar in model selector dropdown while maintaining scroll functionality (#396)
* fix: hide vertical scrollbar in model selector while maintaining scroll functionality

* feat: add gradient shadow indicator for scrollable content

---------

Co-authored-by: Dayuan Jiang <jdy.toh@gmail.com>
2025-12-25 08:58:04 +09:00
Biki Kalita
d3fb2314ee fix: add scrollable model list with visible scrollbar in AI Model Configuration dialog (#395) 2025-12-24 19:06:11 +09:00
Dayuan Jiang
447bb30745 refactor: extract diagram tool handlers to dedicated hook (#389)
- Create useDiagramToolHandlers hook for display_diagram, edit_diagram, append_diagram
- Remove ~300 lines from chat-panel.tsx
- Remove unused stopRef
- Gate debug console.log statements with DEBUG constant
2025-12-24 12:28:59 +09:00
Dayuan Jiang
63398d9f34 fix: filter Langfuse traces to only export chat and AI SDK spans (#392)
Switch from blocklist to whitelist approach - only export spans named
'chat' or starting with 'ai.' to filter out Next.js infrastructure noise
(HEAD, fetch, POST requests).
2025-12-24 10:47:34 +09:00
Dayuan Jiang
82f4deb23a fix: quota daily reset bug and add timezone support (#390)
- Fixed bug where daily quota counts weren't resetting on new day
  (if_not_exists only works for missing attributes, not day changes)
- Changed to two-phase approach: reset if new day, then increment
- Added QUOTA_TIMEZONE env var for local midnight reset (e.g., Asia/Tokyo)
- Added timezone validation with UTC fallback
2025-12-24 10:34:54 +09:00
Dayuan Jiang
1fab261cd0 refactor: extract dev XML streaming simulator to separate component (#388)
- Move DEV_XML_PRESETS constants to new file
- Create DevXmlSimulator component with all simulator logic
- Add preset dropdown with 5 test cases including HTML escape test
- Set default interval to 1ms and chunk size to 10 chars
- Simplify chat-panel.tsx by removing ~130 lines of inline code
2025-12-24 09:52:50 +09:00
Dayuan Jiang
7a4a04c263 fix: remove unused partialXmlRef prop from ChatMessageDisplay (#387) 2025-12-24 09:37:32 +09:00
Dayuan Jiang
0d2e7a7ad6 fix: escape HTML in XML attribute values to prevent parse errors (#386)
- Add HTML escaping (<, >) in convertToLegalXml for attribute values
- Update isMxCellXmlComplete to handle any LLM provider's wrapper tags
- Add wrapper tag stripping in wrapWithMxFile for DeepSeek/Anthropic tags
- Update autoFixXml to escape both < and > in attribute values

Fixes 'Malformed XML detected in final output' error when AI generates
diagrams with HTML content in value attributes like <b>Title</b>.
2025-12-24 09:31:54 +09:00
Dayuan Jiang
3218ccc909 feat: add dev XML streaming simulator for UI debugging (#385) 2025-12-24 09:29:29 +09:00
Dayuan Jiang
d3be96de79 refactor: redesign config panels with refined minimal aesthetic (#384)
- Add CSS design system tokens (surfaces, borders, animations) to globals.css
- Update dialog.tsx with rounded-2xl, shadow-dialog, refined close button
- Enhance input.tsx with rounded-xl and refined focus states
- Refactor settings-dialog with SettingItem pattern and consistent control sizing
- Refactor model-config-dialog with ConfigSection/ConfigCard helpers
- Replace emerald-* classes with success design tokens
- Remove unused ValidationButton component and scrollState
2025-12-23 21:52:04 +09:00
Dayuan Jiang
b2dfd5b890 fix: display correct quota values in limit toast (#383)
- Parse JSON error response from server to get actual used/limit values
- Previously showed 0/0 due to race condition (config fetch vs error)
- AI SDK puts full response body in error.message for non-OK responses
- Updated all quota toasts (request, token, TPM) to use server values
2025-12-23 21:08:21 +09:00
Dayuan Jiang
72d647de7a fix: use Chat Completions API for OpenAI-compatible proxies (#382)
Third-party OpenAI-compatible proxies typically don't support the
/responses endpoint. Use .chat() for custom baseURLs while keeping
Responses API for official OpenAI to preserve reasoning model support.

Fixes #377
2025-12-23 20:29:48 +09:00
Dayuan Jiang
c6b0e5ac62 fix: use totalUsage with all token types for accurate quota tracking (#381)
The onFinish callback's 'usage' only contains the final step's tokens,
which underreports usage for multi-step tool calls (like diagram generation).
Changed to 'totalUsage' which provides cumulative counts across all steps.

Include all 4 token types for accurate counting:
1. inputTokens - non-cached input tokens
2. outputTokens - generated output tokens
3. cachedInputTokens - tokens read from prompt cache
4. inputTokenDetails.cacheWriteTokens - tokens written to cache

Tested locally:
- Request 1 (cache write): 334 + 62 + 0 + 6671 = 7,067 tokens
- Request 2 (cache read): 334 + 184 + 6551 + 120 = 7,189 tokens
- DynamoDB total: 14,256 ✓
2025-12-23 20:19:28 +09:00
Dayuan Jiang
7de192e1fa fix: enable progressive diagram rendering during streaming (#380)
- Add extractCompleteMxCells() to extract only complete mxCell elements from partial XML
- Remove useEffect cleanup that was killing debounce timeouts on every re-render
- Wrap XML in <root> tags for proper DOMParser validation

Previously, diagrams only rendered after ALL XML finished streaming because:
1. useEffect cleanup cleared the 150ms debounce timeout on every message change
2. DOMParser rejected partial XML like '<mxCell id="2" value="...' (incomplete)

Now each complete mxCell renders progressively as it finishes streaming.
2025-12-23 18:54:03 +09:00
Dayuan Jiang
97ae9395cd feat: add server-side quota tracking with DynamoDB (#379)
- Add dynamo-quota-manager.ts for atomic quota checks using ConditionExpression
- Enforce daily request limit, daily token limit, and TPM limit
- Return 429 with quota details (type, used, limit) when exceeded
- Quota is opt-in: only enabled when DYNAMODB_QUOTA_TABLE env var is set
- Remove client-side quota enforcement (server is now source of truth)
- Simplify use-quota-manager.tsx to only display toasts
- Add @aws-sdk/client-dynamodb dependency
2025-12-23 18:36:27 +09:00
Dayuan Jiang
5ec05eb100 refactor: simplify Langfuse integration with AI SDK 6 (#375)
- Remove manual token attribute setting (AI SDK 6 telemetry auto-reports)
- Use totalTokens directly instead of inputTokens + outputTokens calculation
- Fix sessionId bug in log-save/log-feedback (prevents wrong trace attachment)
- Hash IP addresses for privacy instead of storing raw IPs
- Fix isLangfuseEnabled() to check both keys for consistency
2025-12-23 16:26:45 +09:00
Dayuan Jiang
9aec7eda79 fix: add continuation retry limit for truncated diagrams (#372)
Previously, continuation mode (for truncated XML) had unlimited client-side
retries, relying only on server stepCountIs(5) limit. This could cause
excessive API calls (495 observed) when XML truncation kept occurring.

Added MAX_CONTINUATION_RETRY_COUNT=2 to limit continuation attempts:
- After 2 failed continuation attempts, shows error toast and stops
- Resets on successful completion or user-initiated message
- Also resets when quota limits are hit
2025-12-23 14:17:06 +09:00
Dayuan Jiang
a0fbc0ad33 fix: use last user message for Langfuse trace input (#371)
In multi-step tool flows, messages array contains assistant messages
from previous steps. Using messages[messages.length - 1] would record
the assistant's response as trace input instead of the user's question.
2025-12-23 13:43:28 +09:00
Dayuan Jiang
0385c45a10 fix: OpenAI reasoning/thinking blocks not showing (#370)
- Use Responses API instead of Chat Completions API for OpenAI
  (.chat() -> default call) to support reasoning events
- Add o4 to reasoning model detection
- Change default reasoningSummary from 'detailed' to 'auto'
  (not all models support 'detailed')
- Update types to match AI SDK: 'auto' | 'detailed'
2025-12-23 13:38:50 +09:00
Dayuan Jiang
5262b7bfb2 chore: upgrade AI SDK to v6.0.1 (#369)
- Upgrade ai package from ^5.0.89 to ^6.0.1
- Upgrade @ai-sdk/* provider packages to latest v3/v4
- Update convertToModelMessages call to async (new API)
- Fix usage.cachedInputTokens to usage.inputTokenDetails?.cacheReadTokens
2025-12-23 13:31:42 +09:00
Dayuan Jiang
8cb7494d16 feat(i18n): add translations for model configuration UI (#368)
- Add ~40 new translation keys for model-config-dialog and model-selector
- Support English, Chinese, and Japanese translations
- Replace all hardcoded strings with dictionary lookups
2025-12-23 11:42:27 +09:00
Dayuan Jiang
98625dd72a docs: update about page model info to Haiku 4.5 (#367) 2025-12-23 10:22:31 +09:00
Dayuan Jiang
b5734aa5e1 chore: hide notice icon from header (#366) 2025-12-23 10:08:14 +09:00
Dayuan Jiang
87cdc53665 fix: improve Langfuse span filter to exclude all Next.js infrastructure traces (#365)
* debug: add log to verify instrumentation initialization

* fix: improve Langfuse span filter to exclude all Next.js infrastructure traces
2025-12-23 09:47:23 +09:00
Dayuan Jiang
b4fc259de8 chore: bump version to 0.4.6 (#364)
* chore: bump version to 0.4.6

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-23 09:09:39 +09:00
Dayuan Jiang
28f9a81e7b chore: add build-time arg for showing About and Notice (#360) 2025-12-23 01:06:42 +09:00
Dayuan Jiang
0f67884ead fix: include instrumentation.ts in standalone build for Langfuse (#359)
Add outputFileTracingIncludes to next.config.ts to ensure instrumentation.ts
is included in standalone builds (required for App Runner deployment)
2025-12-23 01:03:11 +09:00
Dayuan Jiang
3521495ead chore: conditionally show about and notice based on env var (#358) 2025-12-23 00:32:22 +09:00
Dayuan Jiang
6446454cd7 fix: add SSRF protection to validate-model endpoint (#357)
Block private IPs, localhost, cloud metadata endpoints (169.254.169.254),
and internal hostnames in custom baseUrl parameter to prevent server-side
request forgery attacks.
2025-12-23 00:26:01 +09:00
Biki Kalita
84959637db Support subdirectory deployment and fix API path handling (#311)
* feat: support subdirectory deployment (NEXT_PUBLIC_BASE_PATH)

* removed unwanted check and fix favicon issue

* Use getAssetUrl for manifest assets to avoid undefined NEXT_PUBLIC_BASE_PATH

* Add validation warning for NEXT_PUBLIC_BASE_PATH format

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-22 23:28:55 +09:00
pointerhacker
9e9ea10beb fix:feature/sglang-provider (#302)
Co-authored-by: zhaochaojin <zhaochaojin@didiglobal.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-22 23:13:45 +09:00
Biki Kalita
deae5c2c38 Fix: Localize TPM rate-limit toast via i18n (#353)
* TMP error toast hardcoded english fixed

* fix: correct JA/ZH translations to use tokens instead of requests

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-22 23:00:20 +09:00
Twelveeee
6e2d98e52d move Language Selector into SettingDialog (#352)
* fix:custom model setting bug

* refactor: consolidate aiProvider checks for cleaner code

* fix:Integrated the language selection option into the `SettingsDialog`

* fix:useSearchParams() should be wrapped in a suspense boundary at page

* fix: improve semantic HTML and maintainability

- Replace nested button>a with proper anchor element for GitHub link
- Use i18n.locales.map() with LANGUAGE_LABELS for language options

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-22 22:54:25 +09:00
Dayuan Jiang
85cb441e26 feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration

- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser

* feat: improve model config UI and move selector to chat input

- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation

* refactor: revert shadcn component changes, apply hover fix at usage site

* feat: add AWS credentials support for Bedrock provider

- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions

* fix: reset Test button after validation completes

* fix: reset validation button to Test after success

* fix: complete bedrock support and UI/UX improvements

- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback

* chore: remove unused code

- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts

* fix: UI/UX improvements for model configuration dialog

- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component

* fix: prevent duplicate model IDs within same provider

- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider

* fix: improve duplicate model ID notifications

- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input

* fix: improve duplicate model validation UX in config dialog

- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
Dayuan Jiang
b088a0653e chore: update app icons with new diagram hierarchy design (#350) 2025-12-22 13:24:08 +09:00
Dayuan Jiang
b25b944600 fix(ci): simplify electron release - let electron-builder publish directly (#349) 2025-12-22 11:36:38 +09:00
Dayuan Jiang
4f07a5fafc fix: add write permissions to electron build jobs (#347) 2025-12-22 11:08:03 +09:00
Dayuan Jiang
fc5eca877a chore: bump version to 0.4.5 (#346)
* chore: bump version to 0.4.5 and add desktop app to README

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-22 10:39:28 +09:00
chouheiwa
f58274bb84 feat(electron): add desktop application support with electron (#344)
* feat(electron): add desktop application support with electron

- implement complete Electron main process architecture with window management,
  app menu, IPC handlers, and settings window
- integrate Next.js server for production builds with embedded standalone server
- add configuration management with persistent storage and env file support
- create preload scripts with secure context bridge for renderer communication
- set up electron-builder configuration for multi-platform packaging (macOS,
  Windows, Linux)
- add GitHub Actions workflow for automated release builds
- include development scripts for hot-reload during Electron development

* feat(electron): enhance security and stability

- encrypt API keys using Electron safeStorage API before persisting to disk
- add error handling and rollback for preset switching failures
- extract inline styles to external CSS file and remove unsafe-inline from CSP
- implement dynamic port allocation with automatic fallback for production builds

* fix(electron): add maintainer field for Linux .deb package

- add maintainer email to linux configuration in electron-builder.yml
- required for building .deb packages

* fix(electron): use shx for cross-platform file copying

- replace Unix-only cp -r with npx shx cp -r
- add shx as devDependency for Windows compatibility

* fix(electron): fix runtime icon path for all platforms

- use icon.png directly instead of platform-specific formats
- electron-builder handles icon conversion during packaging
- macOS uses embedded icon from app bundle, no explicit path needed
- add icon.png to extraResources for Windows/Linux runtime access

* fix(electron): add security warning for plaintext API key storage

- warn user when safeStorage is unavailable (Linux without keyring)
- fail secure: throw error if encryption fails instead of storing plaintext
- prevent duplicate warnings with hasWarnedAboutPlaintext flag

* fix(electron): add remaining review fixes

- Add Windows ARM64 architecture support
- Add IPC input validation with config key whitelist
- Add server.js existence check before starting Next.js server
- Make afterPack throw error on missing directories
- Add workflow permissions for release job

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-22 10:18:21 +09:00
Dayuan Jiang
e03b65328d chore(mcp): bump version to 0.1.5 (#343) 2025-12-21 19:44:01 +09:00
Dayuan Jiang
14c1aa8e1c fix(mcp): sync browser state before get_diagram to prevent data loss (#342)
* fix(mcp): sync browser state before get_diagram to prevent data loss

- Add syncRequested flag to SessionState for browser sync coordination
- Add requestSync() and waitForSync() functions to http-server
- Browser polls for syncRequested flag and immediately pushes current state
- get_diagram now syncs fresh state from browser before returning
- edit_diagram requires get_diagram to be called within 30s to prevent stale edits
- Updated edit_diagram description to enforce workflow

* fix(mcp): make lastGetDiagramTime session-scoped and handle missing session in requestSync

- Move lastGetDiagramTime into currentSession object to prevent cross-session issues
- requestSync now returns boolean indicating if request was made
- Only wait for sync if session exists (avoids false-positive from undefined state)
2025-12-21 19:38:35 +09:00
Dayuan Jiang
9e651a51e6 Merge pull request #341 from DayuanJiang/feat/mcp-history
feat(mcp): add diagram version history with SVG previews
2025-12-21 18:08:14 +09:00
dayuan.jiang
2871265362 docs(mcp): add version history feature to README 2025-12-21 18:07:29 +09:00
dayuan.jiang
9d13bd7451 chore(mcp): bump version to 0.1.4 2025-12-21 18:05:44 +09:00
dayuan.jiang
b97f3ccda9 fix(mcp): minimal history integration in index.ts
Keep only essential history integration:
- Import addHistory from history.js
- Remove unused getServerPort import
- Add browser state sync and history saving in display_diagram
- Add history saving in edit_diagram

No changes to prompts, descriptions, or code style.
2025-12-21 17:41:27 +09:00
dayuan.jiang
864375b8e4 fix(mcp): capture SVG for AI-generated diagrams
- Sync browser state before saving history in display_diagram
- Save AI result to history (in addition to state before)
- Add SVG capture after browser loads AI diagrams
- Add /api/history-svg endpoint to update last entry's SVG
- Add updateLastHistorySvg() function to history module
2025-12-21 17:31:06 +09:00
dayuan.jiang
b9bc2a72c6 refactor(mcp): simplify history implementation
- Reduce history.ts from 169 to 51 lines
- Remove AI tools (list_history, restore_version, get_version)
- Remove /api/update-svg endpoint
- Remove 10-second history polling
- Simplify HistoryEntry to just {xml, svg}
- Use array index instead of version numbers

Total reduction: 1936 → 923 lines (-52%)
2025-12-21 16:11:49 +09:00
dayuan.jiang
c215d80688 feat(mcp): add diagram version history
- Add history.ts module with circular buffer (max 50 entries)
- Add history UI with floating button and modal
- Add HTTP endpoints: /api/history, /api/restore
- Add MCP tools: list_history, restore_version, get_version
- Save history before and after AI changes
- Track source (ai/human) for each entry
2025-12-21 16:09:14 +09:00
Dayuan Jiang
74b9e38114 chore: bump version to 0.4.4 (#338)
* chore: bump version to 0.4.4

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-21 01:01:59 +09:00
Dayuan Jiang
68ea4958b8 feat: display app version in Settings dialog (#337) 2025-12-21 00:55:54 +09:00
Dayuan Jiang
938faff6b2 feat(mcp): add XML validation and auto-fix to MCP server (#336)
* feat(mcp): add XML validation and auto-fix to MCP server

- Add xml-validation.ts with validateAndFixXml function
- Integrate validation into display_diagram tool (fails if unfixable)
- Integrate validation into edit_diagram tool (auto-fix each operation)
- Fix bug: typo fixes now run before foreign tag removal
- Fix bug: use before/after comparison instead of regex .test()

* style: auto-format with Biome

* chore(mcp): bump version to 0.1.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-21 00:32:51 +09:00
Biki Kalita
378bef435e Add i18n support, language toggle UI, and translate Settings dialog (#334)
* i18n support added

* fix: align i18n implementation with Next.js 16 guide

- Rename middleware.ts to proxy.ts (Next.js 16 convention)
- Fix params type to Promise<{lang: string}> for layout/metadata
- Add 'server-only' directive and dynamic imports to dictionaries.ts
- Add hasLocale type guard and notFound() for invalid locales
- Wrap LanguageToggle in Suspense for useSearchParams
- Fix dictionary key mismatch (learnmore -> learnMore)
- Improve Chinese translations per Gemini review:
  - loading ellipsis, new -> 新建, styledMode -> 精致
  - goodResponse/badResponse -> 有帮助/无帮助
  - closeProtection -> 关闭确认, fileExceeds phrasing
- Improve Japanese translations per Gemini review:
  - closeProtection -> ページ離脱確認
  - invalidAccessCode phrasing, appendDiagram -> に追加
  - styledMode -> スタイル付き

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-20 14:48:54 +00:00
Dayuan Jiang
f087b54ee4 feat: add get_shape_library tool for AI icon discovery (#335)
* feat: add get_shape_library tool for AI icon discovery

- Add server-side tool that returns shape library documentation
- AI can fetch icon/shape names on-demand before generating diagrams
- Includes path traversal protection and input sanitization
- Library index embedded in tool description for discoverability
- Supports 33 libraries: AWS, Azure, GCP, Kubernetes, Cisco, etc.

* fix: improve get_shape_library error handling and imports

- Move fs/path imports to top of file (avoid dynamic imports per call)
- Distinguish file-not-found vs other errors in catch block
- Include invalid input in validation error message
- Log unexpected errors for debugging

* docs: add get_shape_library to system prompt tool list

- Add Tool4 (get_shape_library) to available tools section
- Add usage guidance in 'Choose the right tool' section
- Update AWS icons note to reference get_shape_library for icon discovery

* fix: display get_shape_library tool output in chat UI

* fix: correct state check for get_shape_library output display

* fix: make get_shape_library output respect fold state

* style: auto-format with Biome

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-20 23:19:49 +09:00
Dayuan Jiang
6bb33eeda2 chore: add auto-format workflow and fix formatting (#319)
- Add GitHub Action to auto-format PRs with Biome
- Fix formatting in app/manifest.ts and scripts/test-diagram-operations.mjs
2025-12-18 23:03:08 +09:00
RainX
a91bd9d1e8 feat: add support for custom AI Gateway base URL (#315)
* feat: add support for custom AI Gateway base URL

- Add createGateway support with configurable baseURL
- Allow AI_GATEWAY_BASE_URL environment variable for:
  * Local development with custom Gateway
  * Self-hosted AI Gateway deployments
  * Enterprise proxy configurations
- Maintain backward compatibility: defaults to Vercel Gateway when not set
- Update documentation with usage examples and configuration notes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove errant character in error message

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-18 22:00:21 +09:00
E66Crisp
81eb71e704 fix(components): Send and sidebar buttons become inaccessible when chat-panel is resized (#309) 2025-12-18 21:16:32 +09:00
E66Crisp
58b6b19526 fix: Prevent DrawIO remount and data loss when resizing window across 768px breakpoint (#306)
* fix: Prevent DrawIO remount and data loss when resizing window across 768px breakpoint

* fix: prevent DrawIO remount and data loss when resizing window

- Move key from ResizablePanelGroup to chat-panel only
- Save diagram to localStorage before breakpoint change
- Restore defaultSize on drawio-panel to prevent layout flash
- Keep save button functionality from main

* fix: reset draw.io ready state on breakpoint change to restore diagram

* fix: skip initial render save and remove console logs

- Add isInitialRenderRef to skip unnecessary save/reset on first render
- Remove console.log statements for production cleanliness
- Add eslint-disable comment explaining loadDiagram dependency

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-18 21:14:10 +09:00
Dayuan Jiang
f65ef548b2 fix: make draw.io built-in save button work with mouse tracking (#296)
- Add showSaveDialog state to DiagramContext for shared state
- Add mouse tracking to only respond to save events when mouse is over draw.io panel
- Prevents save dialog from opening when clicking Send in chat panel
- Add DialogDescription to SaveDialog for accessibility
2025-12-17 20:24:53 +09:00
Dayuan Jiang
741a00db89 Revert "fix: make draw.io built-in save button work (#293)" (#294)
This reverts commit bcc6684ecb.
2025-12-17 19:46:52 +09:00
Dayuan Jiang
bcc6684ecb fix: make draw.io built-in save button work (#293)
- Lift showSaveDialog state to DiagramContext for sharing between components
- Add onSave handler to DrawIoEmbed that opens the save dialog
- Add guard (isSavingRef) with 1s delay to prevent repeated save events from draw.io
- Add deprecation notice to custom download button tooltip

Closes #93, Closes #290
2025-12-17 19:14:15 +09:00
Dayuan Jiang
a9415d24e7 Revise preview feature stability note
Updated the preview feature note for stability.
2025-12-17 14:52:39 +09:00
Dayuan Jiang
439bdd4577 feat: add MCP server package for npx distribution (#284)
* feat: add MCP server package for npx distribution

- Self-contained MCP server with embedded HTTP server
- Real-time browser preview via draw.io iframe
- Tools: start_session, display_diagram, edit_diagram, get_diagram, export_diagram
- Port retry limit (6002-6020) and session TTL cleanup (1 hour)
- Published as @next-ai-drawio/mcp-server on npm

* chore: bump version to 0.1.2

* docs: add MCP server section to README (preview feature)

* docs: add multi-client installation instructions for MCP server

* fix: exclude packages from Next.js build

* docs: use @latest instead of -y flag for npx (match Playwright MCP style)

* chore: bump version to 0.4.3 and add release notes

* chore: remove release notes

* feat: add MCP server notice to example panel
2025-12-17 14:50:07 +09:00
Ted Cao
98b890bb06 feat: add Vercel AI Gateway support (#274)
* feat: add Vercel AI Gateway support

- Updated environment configuration to include AI_GATEWAY_API_KEY for unified access to multiple AI providers.
- Added gateway provider to the list of supported AI providers in the codebase.
- Enhanced documentation to explain the usage of Vercel AI Gateway and its model format.

This change simplifies authentication and allows users to switch between providers seamlessly.

* Update package
@ai-sdk/gateway to latest version 2.0.21
2025-12-17 12:43:33 +09:00
Bridget Amana
f039e4a3c8 Feat/add manifest.ts (#270)
* Add manifest file for Next AI Draw.io application

This file defines the manifest for the Next AI Draw.io application, including metadata like name, description, and icons.

* Add different sizes of favicon

* Update app/manifest.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update app/manifest.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com>
2025-12-16 13:38:53 +09:00
Biki Kalita
7857858074 feat: add warning dialog for theme and UI style changes (#248)
## Summary

  - Auto-saves diagram to localStorage before theme or UI style changes to prevent data loss
  - Extracts inline handler to `handleDrawioUiChange` for cleaner code
  - Renames `toggleDarkMode` to `handleDarkModeChange` for consistency

  ## Problem

  Changing themes (dark/light) or draw.io UI styles (min/sketch) causes the DrawIoEmbed component to remount, losing all unsaved edits without warning.

  ## Solution

  Added `saveDiagramToStorage()` function that exports the current diagram and saves it to localStorage before any theme/UI change. The existing restore mechanism then loads it back after remount.

  ## Related Issues

  Fixes #243
2025-12-15 22:40:21 +09:00
dayuan.jiang
f0919117eb fix: lowercase repo name for docker pull in ECR push step 2025-12-15 21:47:32 +09:00
Dayuan Jiang
cd76fa615e fix: edit_diagram streaming and JSON repair improvements (#271)
- Add shared editDiagramOriginalXmlRef between streaming preview and tool handler
  to avoid conflicts when applying operations (fixes "cell already exists" errors)
- Add JSON repair preprocessing to fix LLM-generated malformed JSON like `:=`
- Filter out tool calls with invalid/undefined inputs from interrupted streaming
- Remove perf console logs
2025-12-15 21:28:31 +09:00
dayuan.jiang
c527ce1520 feat: add AWS App Runner deployment support
- Update Dockerfile CMD to fix HOSTNAME binding for App Runner
- Add ECR push step to GitHub Actions for auto-deploy
- Add .env*.local to gitignore
2025-12-15 15:48:33 +09:00
Dayuan Jiang
44840d27b3 fix: prevent SSRF attack via custom base URL (GHSA-9qf7-mprq-9qgm)
Require API key when custom base URL is provided to prevent attackers
from redirecting server API keys to malicious endpoints.

CVSS: 9.3 (Critical)
2025-12-15 15:02:18 +09:00
Dayuan Jiang
f175276872 refactor: replace text-based edit_diagram with ID-based operations (#267)
* refactor: replace text-based edit_diagram with ID-based operations

- Add applyDiagramOperations() function using DOMParser for ID lookup
- New schema: operations array with type (update/add/delete), cell_id, new_xml
- Update chat-panel.tsx handler for new operations format
- Update OperationsDisplay component to show operation type and cell_id
- Simplify system prompts with new ID-based examples
- Add ID validation for add operations
- Add warning for edges referencing deleted cells

* fix: add ID validation to update operation and remove dead code

- Add ID mismatch validation to update operation (consistency with add)
- Remove orphaned replaceXMLParts function (~300 lines of dead code)
- Update cell_id schema description for clarity
- Add unit tests for applyDiagramOperations (11 tests)
2025-12-15 14:22:56 +09:00
dayuan.jiang
09c556e4c3 chore: bump version to 0.4.1 2025-12-14 23:11:35 +09:00
Dayuan Jiang
ac1c2ce044 fix: remove overly aggressive message filtering on restore (#263)
The hasValidDiagramXml filter was deleting valid messages that had minor
XML issues. Error handling in handleDisplayChart now catches all errors,
so filtering is no longer needed - invalid XML just won't load the diagram
but the conversation is preserved.
2025-12-14 21:49:08 +09:00
Dayuan Jiang
78a77e102d fix: prevent browser crash during long streaming sessions (#262)
- Debounce streaming diagram updates (150ms) to reduce handleDisplayChart calls by 93%
- Debounce localStorage writes (1s) to prevent blocking main thread
- Limit diagramHistory to 20 entries to prevent unbounded memory growth
- Clean up debounce timeout on component unmount to prevent memory leaks
- Add console timing markers for performance profiling

Fixes #78
2025-12-14 21:23:14 +09:00
Dayuan Jiang
55821301dd fix: recover from invalid XML in localStorage on startup (#261)
When LLM generates invalid XML, the app previously saved corrupted messages
to localStorage, causing an unrecoverable crash loop on restart.

This fix validates messages when restoring from localStorage and filters out
any with invalid diagram XML. Users see a toast notification when corrupted
messages are removed.

Fixes #240
2025-12-14 20:01:24 +09:00
Dayuan Jiang
f743219c03 feat: add minimal style mode toggle for faster diagram generation (#260)
* feat: add minimal style mode toggle for faster diagram generation

- Add Minimal/Styled toggle switch in chat input UI
- When enabled, removes color/style instructions from system prompt
- Faster generation with plain black/white diagrams
- Improves XML auto-fix: handle foreign tags, extra closing tags, trailing garbage
- Fix isMxCellXmlComplete to strip Anthropic function-calling wrappers
- Add debug logging for truncation detection diagnosis

* fix: prevent false XML parse errors during streaming

- Escape unescaped & characters in convertToLegalXml() before DOMParser validation
- Only log console.error for final output, not during streaming updates
- Prevents Next.js dev mode error overlay from showing for expected streaming states
2025-12-14 19:38:40 +09:00
Ikko Eltociear Ashimine
ff34f0baf1 docs: update README.md (#257)
Azue -> Azure
2025-12-14 15:08:07 +09:00
Dayuan Jiang
0851b32b67 refactor: simplify LLM XML format to output bare mxCells only (#254)
* refactor: simplify LLM XML format to output bare mxCells only

- Update wrapWithMxFile() to always add root cells (id=0, id=1) automatically
- LLM now generates only mxCell elements starting from id=2 (no wrapper tags)
- Update system prompts and tool descriptions with new format instructions
- Update cached responses to remove root cells and wrapper tags
- Update truncation detection to check for complete mxCell endings
- Update documentation in xml_guide.md

* fix: address PR review issues for XML format refactor

- Fix critical bug: inconsistent truncation check using old </root> pattern
- Fix stale error message referencing </root> tag
- Add isMxCellXmlComplete() helper for consistent truncation detection
- Improve regex patterns to handle any attribute order in root cells
- Update wrapWithMxFile JSDoc to document root cell removal behavior

* fix: handle non-self-closing root cells in wrapWithMxFile regex
2025-12-14 14:04:44 +09:00
Dayuan Jiang
2e24071539 fix: shorten toast notification duration to 2 seconds (#253) 2025-12-14 13:04:18 +09:00
Dayuan Jiang
66bd0e5493 feat: add append_diagram tool and improve truncation handling (#252)
* feat: add append_diagram tool for truncation continuation

When LLM output hits maxOutputTokens mid-generation, instead of
failing with an error loop, the system now:

1. Detects truncation (missing </root> in XML)
2. Stores partial XML and tells LLM to use new append_diagram tool
3. LLM continues generating from where it stopped
4. Fragments are accumulated until XML is complete
5. Server limits to 5 steps via stepCountIs(5)

Key changes:
- Add append_diagram tool definition in route.ts
- Add append_diagram handler in chat-panel.tsx
- Track continuation mode separately from error mode
- Continuation mode has unlimited retries (not counted against limit)
- Error mode still limited to MAX_AUTO_RETRY_COUNT (1)
- Update system prompts to document append_diagram tool

* fix: show friendly message and yellow badge for truncated output

- Add yellow 'Truncated' badge in UI instead of red 'Error' when XML is incomplete
- Show friendly error message for toolUse.input is invalid errors
- Built on top of append_diagram continuation feature

* refactor: remove debug logs and simplify truncation state

- Remove all debug console.log statements
- Remove isContinuationModeRef, derive from partialXmlRef.current.length > 0

* docs: fix append_diagram instructions for consistency

- Change 'Do NOT include' to 'Do NOT start with' (clearer intent)
- Add <mxCell id="0"> to prohibited start patterns
- Change 'closing tags </root></mxGraphModel>' to just '</root>' (wrapWithMxFile handles the rest)
2025-12-14 12:34:34 +09:00
Dayuan Jiang
b33e09be05 feat: add XML auto-fix with refined validation logic (#247)
* feat: add XML auto-fix and improve validator accuracy

- Add autoFixXml() to automatically repair common XML issues:
  - CDATA wrapper removal
  - Duplicate attribute removal
  - Unescaped & and < character escaping
  - Invalid entity reference fixing
  - Unclosed tag completion
  - Nested mxCell flattening
  - Duplicate ID renaming

- Improve validateMxCellStructure() with DOM + regex approach:
  - Use DOMParser for syntax error detection (94% recall)
  - Add regex checks for edge cases
  - Stateful parser for handling > in attribute values

- Integrate validateAndFixXml() in chat-message-display and diagram-context
  - Auto-repair invalid XML before loading
  - Log fixes applied for debugging

Metrics: 99.77% accuracy, 94.06% recall, 94.4% auto-fix success rate

* fix: improve XML auto-fix from 58.7% to 99% fix rate

Key improvements:
- Reorder CDATA removal to run before text-before-root check (+35 cases)
- Implement Gemini's backslash-quote fix with regex backreference
  Handles attr="value", value="text\"inner\"more", and mixed patterns
- Add aggressive drop-broken-cells fix for unfixable mxCell elements
  Iteratively removes cells causing DOM parse errors (up to 50)

Results on 9,411 XML dataset:
- 206 invalid XMLs detected
- 204 successfully fixed (99.0% fix rate)
- 2 unfixable (completely broken, need regeneration)

* refactor: extract XML validation/fix helpers and add constants

- Add constants: MAX_XML_SIZE (1MB), MAX_DROP_ITERATIONS (10), STRUCTURAL_ATTRS, VALID_ENTITIES
- Extract parseXmlTags helper for shared tag parsing logic
- Extract validation helpers: checkDuplicateAttributes, checkDuplicateIds, checkTagMismatches, checkCharacterReferences, checkEntityReferences, checkNestedMxCells
- Simplify validateMxCellStructure from ~200 lines to ~55 lines
- Add logging to empty catch block in DOMParser section
- Add size warning for large XML documents
- Remove unused variables (isSelfClose, duplicate idPattern)

* fix: improve XML auto-fix with malformed quote pattern

- Fix =&quot;...&quot; pattern where &quot; was used as delimiter instead of actual quotes
- Common in dashPattern attributes like dashPattern=&quot;1 1;&quot;
2025-12-13 23:31:01 +09:00
Dayuan Jiang
987dc9f026 fix: add configurable MAX_OUTPUT_TOKENS to prevent truncation (#251)
- Add MAX_OUTPUT_TOKENS env var (fixes output truncation with Bedrock)
- Remove redundant fixToolCallInputs function
- Remove jsonrepair dependency
- Consolidate duplicate lastMessage/userInputText variables
2025-12-13 23:28:41 +09:00
dayuan.jiang
6024443816 fix: improve XML auto-fix from 58.7% to 99% fix rate
Key improvements:
- Reorder CDATA removal to run before text-before-root check (+35 cases)
- Implement Gemini's backslash-quote fix with regex backreference
  Handles attr="value", value="text\"inner\"more", and mixed patterns
- Add aggressive drop-broken-cells fix for unfixable mxCell elements
  Iteratively removes cells causing DOM parse errors (up to 50)

Results on 9,411 XML dataset:
- 206 invalid XMLs detected
- 204 successfully fixed (99.0% fix rate)
- 2 unfixable (completely broken, need regeneration)
2025-12-13 16:11:48 +09:00
dayuan.jiang
4b838fd6d5 feat: add XML auto-fix and improve validator accuracy
- Add autoFixXml() to automatically repair common XML issues:
  - CDATA wrapper removal
  - Duplicate attribute removal
  - Unescaped & and < character escaping
  - Invalid entity reference fixing
  - Unclosed tag completion
  - Nested mxCell flattening
  - Duplicate ID renaming

- Improve validateMxCellStructure() with DOM + regex approach:
  - Use DOMParser for syntax error detection (94% recall)
  - Add regex checks for edge cases
  - Stateful parser for handling > in attribute values

- Integrate validateAndFixXml() in chat-message-display and diagram-context
  - Auto-repair invalid XML before loading
  - Log fixes applied for debugging

Metrics: 99.77% accuracy, 94.06% recall, 94.4% auto-fix success rate
2025-12-13 16:11:47 +09:00
Dayuan Jiang
e321ba7959 chore: optimize Vercel costs by removing analytics and configuring functions (#238)
- Create vercel.json with optimized function settings:
  - Chat API: 512MB memory, 120s timeout
  - Other APIs: 256MB memory, 10s timeout
- Remove @vercel/analytics package and imports
- Reduce chat route maxDuration from 300s to 120s

Expected savings: $2-4/month, keeping costs under $20 included credit
2025-12-12 16:13:06 +09:00
Dayuan Jiang
aa15519fba fix: handle malformed XML from DeepSeek gracefully (#235)
* fix: handle malformed XML from DeepSeek gracefully

Add early XML validation with parsererror check before calling
replaceNodes to prevent application crashes when AI models
generate malformed XML with unescaped special characters.

Changes:
- Add toast import from sonner
- Parse and validate XML before processing
- Add parsererror detection to catch malformed XML early
- Wrap replaceNodes in try-catch for additional safety
- Add user-friendly toast notifications for all error cases
- Change console.log to console.error for validation failures

Fixes #220 #230 #231

* fix: prevent toast spam during streaming and merge silent failure fixes

- Only show error toasts after streaming completes (not during partial updates)
- Track which tool calls have shown errors to prevent duplicate toasts
- Merge clipboard copy error handling from PR #236
- Merge feedback submission error handling from PR #237
- Add comments explaining streaming vs completion behavior

* refactor: simplify toast deduplication with boolean flag

Based on code review feedback, simplified the approach from tracking
per-tool-call IDs in a Set to using a single boolean flag.

Changes:
- Replaced Set<string> with boolean ref for toast tracking
- Removed toolCallId and showToast parameters from handleDisplayChart
- Reset flag when streaming starts (simpler mental model)
- Same behavior: one toast per streaming session, no spam

Benefits:
- Fewer concepts (1 boolean vs Set + 2 parameters)
- No manual coordination between call sites
- Easier to understand and maintain
- ~15 fewer lines of tracking logic

* fix: only show toast for final malformed XML, not during streaming

- Remove errorToastShownRef tracking (no longer needed)
- Add showToast parameter to handleDisplayChart (default false)
- Pass false during streaming (XML may be incomplete)
- Pass true at completion (show toast if final XML is malformed)
- Simpler and more explicit error handling
2025-12-12 14:52:25 +09:00
Dayuan Jiang
c2c65973f9 fix: revert UI and notify user when feedback submission fails (#237)
When feedback submission to the API fails, revert the optimistic
UI update and show a toast notification to inform the user.

Changes:
- Add toast import from sonner
- Change console.warn to console.error for proper logging
- Add toast.error() notification when API call fails
- Revert optimistic UI update by removing feedback from state

Previously, feedback submission failures were completely silent.
Users would see the thumbs-up/down visual feedback but their
feedback was never recorded. This creates a false sense that
the feedback was successfully submitted.

Now users are immediately notified when submission fails and
can retry their feedback.
2025-12-12 14:08:20 +09:00
Dayuan Jiang
b5db980f69 fix: add user feedback for clipboard copy failures (#236)
Add toast notification when clipboard copy operation fails,
so users know when their copy attempt was unsuccessful.

Changes:
- Add toast import from sonner
- Add toast.error() notification when clipboard copy fails
- Show clear message: "Failed to copy message. Please copy
  manually or check clipboard permissions."

Previously, clipboard copy failures were only indicated by a
brief visual state change (setCopyFailedMessageId), which users
might miss. Now users receive persistent feedback when copy
operations fail.
2025-12-12 14:06:53 +09:00
Shashi kiran M S
c9b60bfdb2 feat: Add a new chat button with a confirmation modal (#229)
* feat: Add a new chat button with a confirmation modal

* Fix for PR comments

* fix: add error handling and proper cleanup in handleNewChat

- Add try-catch for localStorage operations to handle quota exceeded,
  private browsing, and other storage errors
- Use handleFileChange([]) instead of setFiles([]) to properly clear
  pdfData Map alongside files
- Only show success toast when localStorage operations succeed
- Show warning toast if localStorage fails but chat state is cleared

---------

Co-authored-by: Dayuan Jiang <jdy.toh@gmail.com>
2025-12-12 10:08:18 +09:00
Twelveeee
f170bb41ae fix:custom model setting bug (#227)
* fix:custom model setting bug

* refactor: consolidate aiProvider checks for cleaner code

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-12 09:33:07 +09:00
Dayuan Jiang
a0f163fe9e fix: improve Azure provider auto-detection and validation (#223) (#225)
* fix: improve Azure provider auto-detection and validation (#223)

- Fix detectProvider() to only detect Azure when it has complete config
  (both AZURE_API_KEY and AZURE_RESOURCE_NAME or AZURE_BASE_URL)
- Add validation in validateProviderCredentials() for Azure to provide
  clear error messages when configuration is incomplete
- Update docs/ai-providers.md to clarify Azure requires resource name

* docs: add Azure reasoning options to documentation
2025-12-11 21:49:50 +09:00
try2love
8fd3830b9d Fix/clipboard (#189)
* bugfix: clipboard error bug

* fix: use try-catch fallback for clipboard API instead of feature detection

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-11 21:09:42 +09:00
Biki Kalita
77a25d2543 Persist processed tool calls to prevent replay after chat restore (#224) 2025-12-11 20:48:48 +09:00
Dayuan Jiang
b9da24dd6d fix: limit auto-retry to 3 attempts and enforce quota checks (#219)
- Add MAX_AUTO_RETRY_COUNT (3) to prevent infinite retry loops
- Check token and TPM limits before each auto-retry
- Reset retry counter on user-initiated messages
- Show toast notification when limits are reached

Fixes issue where models returning invalid tool inputs caused 45+ API
requests due to sendAutomaticallyWhen having no retry limit or quota check.
2025-12-11 17:56:40 +09:00
Dayuan Jiang
97cc0a07dc fix: disable history XML replacement by default (#217)
Some models (e.g. minimax) copy placeholder text instead of generating
fresh XML, causing tool call validation failures and infinite loops.

Added ENABLE_HISTORY_XML_REPLACE env var (default: false) to control
this behavior.
2025-12-11 17:36:18 +09:00
dayuan.jiang
c42efdc702 chore: bump version to 0.4.0 and update README features 2025-12-11 14:59:09 +09:00
Dayuan Jiang
dd027f1856 chore: update chain-of-thought.txt example file (#215)
Added full content of the Chain-of-Thought Prompting paper by Wei et al. from Google Research. This example file demonstrates how chain-of-thought reasoning improves LLM performance on complex reasoning tasks.
2025-12-11 14:45:29 +09:00
Dayuan Jiang
869391a029 refactor: eliminate code duplication (DRY principle) (#211)
## Problem Solved
Previous refactoring added 105 lines (1476→1581) by extracting code into separate files without eliminating duplication. This refactor focuses on reducing code size through deduplication while maintaining file separation for maintainability.

## Summary
- Reduced total lines from 1581 to 1519 (-62 lines, 3.9% reduction)
- Eliminated duplicate patterns using generic helpers and factory functions
- Maintained file structure for maintainability
- Zero functional changes - same behavior

### Phase 1: DRY use-quota-manager.tsx
- Created parseStorageCount() helper (eliminates 6x localStorage read duplication)
- Created createQuotaChecker() factory (consolidates 3 check function bodies)
- Created createQuotaIncrementer() factory (consolidates 3 increment function bodies)
- Result: 242→247 lines (+5 lines, but fully DRY with eliminated duplication)

### Phase 2: DRY chat-panel.tsx (1176→1109 lines, -67 lines)

#### 2.1: Extract checkAllQuotaLimits helper
- Replaced 3 occurrences of 18-line quota check blocks
- Saved 36 lines

#### 2.2: Extract sendChatMessage helper
- Replaced 3 occurrences of 21-line sendMessage+headers blocks
- Saved 42 lines

#### 2.3: Extract processFilesAndAppendContent helper
- Replaced 2 occurrences of file processing loops
- Handles PDF, text, and image files uniformly
- Async helper with optional image parts parameter
2025-12-11 14:28:02 +09:00
Dayuan Jiang
8b9336466f feat: make PDF/text extraction char limit configurable via env (#214)
Add NEXT_PUBLIC_MAX_EXTRACTED_CHARS environment variable to allow
configuring the maximum characters extracted from PDF and text files.
Defaults to 150000 (150k chars) if not set.
2025-12-11 14:14:31 +09:00
Dayuan Jiang
ee514efa9e fix: implement AZURE_RESOURCE_NAME config for Azure OpenAI (#213)
Previously AZURE_RESOURCE_NAME was documented in env.example but not
actually used in the code. This caused Azure OpenAI configuration to fail
when users set AZURE_RESOURCE_NAME instead of AZURE_BASE_URL.

Changes:
- Read AZURE_RESOURCE_NAME from environment and pass to createAzure()
- resourceName constructs endpoint: https://{name}.openai.azure.com/openai/v1
- baseURL takes precedence over resourceName when both are set
- Updated env.example with clearer documentation

Fixes #208
2025-12-11 13:32:33 +09:00
Dayuan Jiang
e2757a34b7 Replace old asset link with new one
Updated asset link in README.md.
2025-12-11 12:25:00 +09:00
Dayuan Jiang
c0347dd55d fix: prevent diagram regeneration loop after successful display (#206)
- Changed sendAutomaticallyWhen to only auto-resubmit on tool errors
- Extracted logic to shouldAutoResubmit() function with JSDoc
- Added TypeScript interfaces for type safety (MessagePart, ChatMessage)
- Wrapped debug logs with DEBUG flag for production readiness
- Added TOOL_ERROR_STATE constant to avoid hardcoded strings

Problem: AI was regenerating diagrams 3+ times even after successful display
Root cause: lastAssistantMessageIsCompleteWithToolCalls auto-resubmits on both success AND error
Solution: Custom logic that only auto-resubmits on errors, stops on success
2025-12-11 09:47:30 +09:00
Biki Kalita
a047a6ff97 feat: Display AI reasoning/thinking blocks in chat interface (#152)
* feat: Add reasoning/thinking blocks display in chat interface

* feat: add multi-provider options support and replace custom reasoning UI with AI Elements

* resolve conflicting reasoning configs and correct provider-specific reasoning parameters

* try to solve conflict

* fix: simplify reasoning display and remove unnecessary dependencies

- Remove Streamdown dependency (~5MB) - reasoning is plain text only
- Fix Bedrock providerOptions merging for Claude reasoning configs
- Remove unsupported DeepSeek reasoning configuration
- Clean up unused environment variables (REASONING_BUDGET_TOKENS, REASONING_EFFORT, DEEPSEEK_REASONING_*)
- Remove dead commented code from route.ts

Reasoning blocks contain plain thinking text and don't need markdown/diagram/code rendering.

* feat: comprehensive reasoning support improvements

Major improvements:
- Auto-enable reasoning display for all supported models
- Fix provider-specific reasoning configurations
- Remove unnecessary Streamdown dependency (~5MB)
- Clean up debug logging

Provider changes:
- OpenAI: Auto-enable reasoningSummary for o1/o3/gpt-5 models
- Google: Auto-enable includeThoughts for Gemini 2.5/3 models
- Bedrock: Restrict reasoningConfig to only Claude/Nova (fixes MiniMax error)
- Ollama: Add thinking support for qwen3-like models

Other improvements:
- Remove ENABLE_REASONING toggle (always enabled)
- Fix Bedrock providerOptions merging for Claude
- Simplify reasoning component (plain text rendering)
- Clean up unused environment variables

* fix: critical bugs and documentation gaps in reasoning support

Critical fixes:
- Fix Bedrock shallow merge bug (deep merge preserves anthropicBeta + reasoningConfig)
- Add parseInt validation with parseIntSafe helper (prevents NaN errors)
- Validate all numeric env vars with min/max ranges

Documentation improvements:
- Add BEDROCK_REASONING_BUDGET_TOKENS and BEDROCK_REASONING_EFFORT to env.example
- Add OLLAMA_ENABLE_THINKING to env.example
- Update JSDoc with accurate env var list and ranges

Code cleanup:
- Remove debug console.log statements from route.ts
- Refactor duplicate providerOptions assignments

---------

Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com>
Co-authored-by: Dayuan Jiang <jdy.toh@gmail.com>
2025-12-11 00:24:43 +09:00
Dayuan Jiang
d2ba133eaf feat: add PDF and text file upload support (#205)
- Add client-side PDF text extraction using unpdf library
- Support text files (.txt, .md, .json, .csv, .py, .js, .ts, etc.)
- Add file preview with character count for PDF/text files
- Add 150k character limit for extracted content
- Highlight Paper to Diagram example with NEW badge
- Fix React hydration error by adding explicit IDs to ResizablePanelGroup
- Remove code duplication by centralizing file utilities in pdf-utils.ts
2025-12-10 21:32:35 +09:00
Dayuan Jiang
43e5993f47 fix: improve LLM diagram context awareness and image preview (#202)
- Add replaceHistoricalToolInputs to replace XML in tool calls with placeholders
- Send both previousXml and current xml so LLM can understand user's manual edits
- Update system message to mark current XML as authoritative source of truth
- Fix React StrictMode issue with blob URL cleanup in FilePreviewList
- Add unoptimized prop to Image components for blob URLs
2025-12-10 18:04:37 +09:00
Dayuan Jiang
9a954ccb44 fix: correct NEXT_PUBLIC_DRAWIO_BASE_URL in offline deployment docs (#198)
- Fix wrong URL (http://drawio:8080 -> http://localhost:8080)
- Browser cannot resolve Docker internal hostnames
- Add docker-compose.yml example
- Simplify documentation
2025-12-10 14:20:34 +09:00
Dayuan Jiang
9d4c89ec43 docs: improve offline deployment guide with docker compose (#191) 2025-12-10 09:40:48 +09:00
try2love
5da4ef67ec feat:light/dark mode switch (#138)
Summary

- Adds browser theme detection on first visit using
prefers-color-scheme media query
- Renames localStorage key from dark-mode to
next-ai-draw-io-dark-mode for consistency with other keys
- Uses STORAGE_DIAGRAM_XML_KEY constant instead of hardcoded
string in diagram-context.tsx

Changes

app/page.tsx:
- On first visit (no saved preference), detect browser's color
scheme preference
- Update localStorage key to follow project naming convention
(next-ai-draw-io-*)

contexts/diagram-context.tsx:
- Import STORAGE_DIAGRAM_XML_KEY from chat-panel.tsx
- Replace hardcoded "next-ai-draw-io-diagram-xml" with the
constant
2025-12-10 09:21:15 +09:00
Dayuan Jiang
67b0adf211 docs: align Chinese and Japanese README with main README (#190) 2025-12-10 08:42:53 +09:00
Dayuan Jiang
65af353852 Update model requirements and notes in README 2025-12-10 08:19:21 +09:00
Dayuan Jiang
b3deb65584 chore: remove duplicate self-hosted-drawio.md (#188)
Content has been consolidated into docs/offline-deployment.md
2025-12-09 22:27:34 +09:00
Dayuan Jiang
626f3a76b5 docs: add offline deployment guide (#187) 2025-12-09 22:19:28 +09:00
terrydash
97bb350dd6 feat: add support for self-hosted draw.io via DRAWIO_BASE_URL environment variable (#163)
## Summary
Add support for self-hosted draw.io instances via build-time configuration.

## Problem
In some corporate environments, `embed.diagrams.net` is blocked by network policies. 
Users cannot use the application without access to the default draw.io embed URL.

## Solution
- Add `NEXT_PUBLIC_DRAWIO_BASE_URL` environment variable support
- Pass the `baseUrl` prop to the `DrawIoEmbed` component
- Configure Dockerfile to accept build-time argument for the draw.io URL

## Usage
```yaml
# docker-compose.yaml
services:
  drawio:
    image: jgraph/drawio:latest
    ports:
      - "8080:8080"
  
  next-ai-draw-io:
    build:
      context: .
      args:
        - NEXT_PUBLIC_DRAWIO_BASE_URL=http://drawio:8080
```

Or build directly:
```bash
docker build --build-arg NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080 -t next-ai-draw-io .
```

**Note:** This is a build-time configuration. To change the draw.io URL, you need to rebuild the Docker image.
2025-12-09 22:00:54 +09:00
Dayuan Jiang
97ab82e027 feat: add bring-your-own-API-key support (#186)
- Add AI provider settings to config panel (provider, model, API key, base URL)
- Support 7 providers: OpenAI, Anthropic, Google, Azure, OpenRouter, DeepSeek, SiliconFlow
- Client API keys stored in localStorage, never stored on server
- Client settings override server env vars when provided
- Skip server credential validation when client provides API key
- Bypass usage limits (request/token/TPM) when using own API key
- Add /api/config endpoint for fetching usage limits
- Add privacy notices to settings dialog, about pages, and quota toast
- Add clear settings button to reset saved API keys
- Update README files (EN/CN/JA) with BYOK documentation

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-09 17:50:07 +09:00
dayuan.jiang
77cb10393b fix: translate image content block error to user-friendly message 2025-12-09 16:00:19 +09:00
Dayuan Jiang
967d63c57e feat: support minimax model (#185)
* feat: support minimax model with XML wrapping fix

- Add wrapWithMxFile utility to properly wrap XML for draw.io
- Fix 'Not a diagram file' error when model generates raw <root> XML
- Add supportsPromptCaching check for conditional caching
- Only enable Bedrock prompt caching for Claude models

* docs: update model mention to minimax-m2 across About pages and READMEs

- Update tooltip in chat-panel.tsx to mention minimax-m2 model change
- Update English, Chinese, and Japanese About pages with model change info
- Update English, Chinese, and Japanese READMEs with demo site model note

---------

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-09 15:53:59 +09:00
Dayuan Jiang
914e914423 feat: replace hardcoded usage limits with dynamic env variables (#180)
- About pages now read DAILY_REQUEST_LIMIT, DAILY_TOKEN_LIMIT, TPM_LIMIT from env
- Removed unused /app/api/config/ route
- Numbers formatted as Xk (e.g., 30k, 10k)
2025-12-09 09:57:09 +09:00
Dayuan Jiang
f6cfcab45a fix: upgrade React to 19.1.2 to patch CVE-2025-55182 (#176)
Fixes critical RCE vulnerability (CVSS 10.0) in React Server Components
caused by deserialization of untrusted data.

Closes #175
2025-12-08 23:31:17 +09:00
singledog957
95c5a75ca3 feat: Show detailed error messages instead of generic 'Internal server error' (#144) (#154)
* feat: Show detailed error messages instead of generic 'Internal server error' (#144)

* refactor: simplify error handling logic per feedback

* refactor: imported AI SDK error handler

* fix: remove unused import and expand sensitive data filter

- Remove unused NoSuchModelError import
- Add 'secret', 'password', 'credential' to sensitive data filter

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-08 20:52:18 +09:00
Dayuan Jiang
ac09f9f8f9 feat: redesign usage limits section on about pages (#173)
- Redesign usage limits card with gradient border and modern styling
- Remove emojis and combine title/subtitle on same line
- Make all 3 language pages (EN/CN/JP) consistent in design
- Update text content with exact localized wording
- Add warning triangle icon in chat panel linking to about page
- Add 'Learn more' link in quota limit toast
- Open about page links in new tab to preserve diagram state
2025-12-08 20:26:51 +09:00
Dayuan Jiang
622829b903 feat: add daily token limit with actual usage tracking (#171)
* feat: add daily token limit with actual usage tracking

- Add DAILY_TOKEN_LIMIT env var for configurable daily token limit
- Track actual tokens from Bedrock API response metadata (not estimates)
- Server sends inputTokens + cachedInputTokens + outputTokens via messageMetadata
- Client increments token count in onFinish callback with actual usage
- Add NaN guards to prevent corrupted localStorage values
- Add token limit toast notification with quota display
- Remove client-side token estimation (was blocking legitimate requests)
- Switch to js-tiktoken for client compatibility (pure JS, no WASM)

* feat: add TPM (tokens per minute) rate limiting

- Add 50k tokens/min client-side rate limit
- Track tokens per minute with automatic minute rollover
- Check TPM limit after daily limits pass
- Show toast when rate limit reached
- NaN guards for localStorage values

* feat: make TPM limit configurable via TPM_LIMIT env var

* chore: restore cache debug logs

* fix: prevent race condition in TPM tracking

checkTPMLimit was resetting TPM count to 0 when checking, which
overwrote the count saved by incrementTPMCount. Now checkTPMLimit
only reads and incrementTPMCount handles all writes.

* chore: improve TPM limit error message clarity
2025-12-08 18:56:34 +09:00
Dayuan Jiang
728dda5267 feat: add daily request limit with custom toast notification (#167)
- Add DAILY_REQUEST_LIMIT env var support in config API
- Track request count in localStorage (resets daily)
- Show friendly quota limit toast with self-host/sponsor links
- Apply limit to send, regenerate, and edit message actions
2025-12-08 14:26:01 +09:00
Dayuan Jiang
b68b1b0f33 chore: clean up root directory by moving docs (#166)
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-08 12:08:35 +09:00
Dayuan Jiang
bd23aed93b docs: update README with badges, TOC, and live demo button (#165)
* docs: update README with badges, TOC, and reorganized sections

* docs: add SVG button for live demo link
2025-12-08 11:48:14 +09:00
Dayuan Jiang
95aa4b8a56 chore: remove Amplify integration (#164)
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-08 11:39:32 +09:00
Dayuan Jiang
4070772733 perf: disable prefetch on About link to reduce requests (#162) 2025-12-08 11:03:29 +09:00
Dayuan Jiang
c4aaa7c915 feat: only show access code field when password is configured (#161)
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-08 11:00:14 +09:00
Dayuan Jiang
ecea8a6005 fix: use static maxDuration value for Next.js 16 compatibility (#160) 2025-12-08 10:56:37 +09:00
Dayuan Jiang
ebc622144b chore: add Cloudflare-related files to gitignore (#159) 2025-12-08 10:42:08 +09:00
Dayuan Jiang
ee9267d54c chore: make maxDuration configurable via env variable (#157)
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-08 10:20:52 +09:00
Dayuan Jiang
f6682fe3ac fix(diagram): wrap XML in mxfile structure when canvas is empty (#156)
When clicking an example diagram on a clean page (no existing diagram),
the code was passing raw <root>...</root> format XML directly to DrawIO.
However, DrawIO expects the full mxfile/diagram/mxGraphModel/root structure.

This fix provides a default mxfile template when chartXML is empty,
ensuring replaceNodes properly wraps the content before loading into DrawIO.

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-08 10:10:52 +09:00
Biki Kalita
03db4c8096 fix(diagram): save history snapshot after edit_diagram tool execution (#155) 2025-12-08 09:44:10 +09:00
dayuan.jiang
167f5ed36a feat: enable recordInputs in Langfuse telemetry
Enable full message history recording including XML tool calls for better observability.
2025-12-07 20:58:44 +09:00
Dayuan Jiang
cd8e0e2263 feat: add token counting utility for system prompts (#153)
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 20:33:43 +09:00
Dayuan Jiang
8c431ee6ed fix: preserve message parts order in chat display (#151)
- Fix bug where text after tool calls was merged with initial text
- Group consecutive text/file parts into bubbles while keeping tools in order
- Parts now display as: plan -> tool_result -> additional text
- Remove debug logs from fixToolCallInputs function

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 19:56:31 +09:00
Dayuan Jiang
86420a42c6 fix: implement client-side caching for example diagrams (#150)
- Add client-side cache check in onFormSubmit to bypass API calls for example prompts
- Use findCachedResponse to match input against cached examples
- Directly set messages with cached tool response when example matches
- Hide regenerate button for cached example responses (toolCallId starts with 'cached-')
- Prevents unnecessary API calls when using example buttons

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 19:36:09 +09:00
Dayuan Jiang
0baf21fadb fix: validate XML before displaying diagram to catch duplicate IDs (#147)
- Add validation to loadDiagram in diagram-context, returns error or null
- display_diagram and edit_diagram tools now check validation result
- Return error to AI agent with state: output-error so it can retry
- Skip validation for trusted sources (localStorage, history, internal templates)
- Add debug logging for tool call inputs to diagnose Bedrock API issues
2025-12-07 14:38:15 +09:00
Dayuan Jiang
a54068fec2 docs: add --env-file option for Docker and simplify install instructions (#142) 2025-12-07 11:49:01 +09:00
Dayuan Jiang
e25fd367d5 chore: add GitHub issue templates for bug reports and feature requests (#140) 2025-12-07 11:00:25 +09:00
Aurelius Huang
3264244fe9 fix: add deps: @opentelemetry/exporter-trace-otlp-http version 0.208.0 (#124) 2025-12-07 10:49:01 +09:00
QiyuanChen
d8cdd049d1 feat: add SiliconFlow as a supported AI provider (#137)
* feat: add SiliconFlow as a supported AI provider in documentation and configuration

* fix: update SiliconFlow configuration comment to English
2025-12-07 10:22:57 +09:00
Dayuan Jiang
b1bc1a6dc6 feat: auto-save and restore session state (#135)
- Save and restore chat messages, XML snapshots, session ID, and diagram XML to localStorage
- Restore diagram when DrawIO becomes ready (using new onLoad callback)
- Change close protection default to false since auto-save handles persistence
- Clear localStorage when clearing chat
- Handle edge cases: undefined edit fields, empty chartXML, missing access code header
2025-12-07 01:39:09 +09:00
Biki Kalita
8b578a456e fix: Remove hardcoded temperature parameter to support models that don't support it (#133)
* Fix: remove hardcoded temperature parameter to support reasoning models

* feat: make temperature configurable via AI_TEMPERATURE env var

- Instead of removing temperature entirely, make it optional via env var
- Set AI_TEMPERATURE=0 for deterministic output (recommended for diagrams)
- Leave unset for models that don't support temperature (e.g., GPT-5.1 reasoning)

* docs: add AI_TEMPERATURE env var documentation

- Update env.example with AI_TEMPERATURE option
- Update README.md configuration section
- Add Temperature Setting section in ai-providers.md

* docs: add TEMPERATURE env var documentation

- Update env.example with TEMPERATURE option
- Update README.md, README_CN.md, README_JA.md configuration sections
- Add Temperature Setting section in ai-providers.md
- Update route.ts to use TEMPERATURE env var

---------

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 01:34:59 +09:00
Dayuan Jiang
05d58025c4 fix: fix hydration mismatch for DrawIO theme loading (#131)
- Load DrawIO theme from localStorage after mount with useEffect
- Add loading spinner while theme loads
- Prevents SSR/client hydration mismatch (server has no localStorage)

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 00:45:19 +09:00
Dayuan Jiang
4be64317b3 feat: enhance system prompts with JSON escaping and edge routing rules (#132)
- Add JSON escaping warnings to help model generate valid tool calls
- Add comprehensive edge routing rules to prevent overlapping lines
- Add planning guidance for diagram creation
- Update token count estimates in comments

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 00:40:23 +09:00
Dayuan Jiang
2fac6323f0 fix: add orphaned mxPoint validation and cleanup (#130)
- Add validation for orphaned mxPoint elements in validateMxCellStructure()
- Add cleanup of orphaned mxPoint elements in convertToLegalXml()
- Orphaned mxPoints cause 'Could not add object mxPoint' errors in draw.io
- mxPoint elements must have 'as' attribute or be inside <Array as="points">

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 00:40:19 +09:00
Dayuan Jiang
a415c46b66 feat: improve XML search/replace matching strategies (#129)
- Add 6th strategy: match by value attribute (label text)
- Add 7th strategy: normalized whitespace match
- Remove lastProcessedIndex tracking - always search from beginning
- Pairs may not be in document order, so sequential tracking was unreliable

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 00:40:16 +09:00
Dayuan Jiang
3894abd9ed feat: add tool call JSON repair and Bedrock compatibility (#127)
- Add fixToolCallInputs() to fix Bedrock API requirement (JSON object, not string)
- Add experimental_repairToolCall for malformed JSON from model
- Add stepCountIs(5) limit to prevent infinite loops
- Update edit_diagram tool description with JSON escaping warning

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 00:40:13 +09:00
Dayuan Jiang
6965a54f48 feat: upgrade @ai-sdk/react to 2.0.107 and migrate to new API (#126)
- Upgrade @ai-sdk/react from 2.0.22 to 2.0.107
- Migrate from addToolResult to addToolOutput (new API)
- Add output-error state for proper error signaling to model
- Add sendAutomaticallyWhen for auto-retry on tool errors
- Add stop function ref for potential future use

Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-07 00:40:11 +09:00
Dayuan Jiang
46567cb0b8 feat: verify access code with server before saving (#128) 2025-12-07 00:21:59 +09:00
Dayuan Jiang
9f77199272 feat: add configurable close protection setting (#123)
- Add Close Protection toggle to Settings dialog
- Save setting to localStorage (default: enabled)
- Make beforeunload confirmation conditional
- Settings button now always visible in header
- Add shadcn Switch and Label components
2025-12-06 21:42:28 +09:00
dayuan.jiang
77f2569a3b chore: bump version to 0.3.0 2025-12-06 19:26:26 +09:00
Dayuan Jiang
cbb92bd636 fix: set maxDuration to 60 for Vercel hobby plan (#122) 2025-12-06 18:09:30 +09:00
Dayuan Jiang
8d898d8adc fix: revert maxDuration to static value (Next.js requirement) (#121) 2025-12-06 18:04:23 +09:00
Dayuan Jiang
1e0b1ed970 feat: make maxDuration configurable via MAX_DURATION env (#120) 2025-12-06 17:47:50 +09:00
Dayuan Jiang
1d03d10ba8 docs: add CONTRIBUTING.md (#119) 2025-12-06 17:39:47 +09:00
Dayuan Jiang
e893bd60f9 fix: resolve biome lint errors and memory leak in file preview (#118)
- Disable noisy biome rules (noExplicitAny, useExhaustiveDependencies, etc.)
- Fix memory leak in file-preview-list.tsx with useRef pattern
- Separate unmount cleanup into dedicated useEffect
- Add ToolPartLike interface for type safety in chat-message-display
- Add accessibility attributes (role, tabIndex, onKeyDown)
- Replace autoFocus with useEffect focus pattern
- Minor syntax improvements (optional chaining, key fixes)
2025-12-06 16:18:26 +09:00
Dayuan Jiang
9aaf9bf31f refactor: deduplicate system prompts with two-phase composition (#117) 2025-12-06 12:58:53 +09:00
Dayuan Jiang
150eb1ff63 chore: add Biome for formatting and linting (#116)
- Add Biome as formatter and linter (replaces Prettier)
- Configure Husky + lint-staged for pre-commit hooks
- Add VS Code settings for format on save
- Ignore components/ui/ (shadcn generated code)
- Remove semicolons, use 4-space indent
- Reformat all files to new style
2025-12-06 12:46:40 +09:00
Dayuan Jiang
215a101f54 fix: revert edit_diagram tool description to original (#115) 2025-12-06 12:41:01 +09:00
Dayuan Jiang
e00938d9d3 feat: enhance system prompt with app context and dynamic model name (#114)
- Add App Context section describing the left/right panel layout
- Add App Features section with icon locations (history, theme, upload, export, clear)
- Dynamically inject model name into system prompt via {{MODEL_NAME}} placeholder
- Expand edit_diagram tool description with usage guidelines
2025-12-06 12:37:37 +09:00
Dayuan Jiang
dd27d034e2 fix: force re-render when switching between mobile/desktop layout (#112) 2025-12-05 23:45:57 +09:00
Dayuan Jiang
9e781005af fix: make button hover state darker instead of lighter (#111) 2025-12-05 23:38:24 +09:00
Dayuan Jiang
fe1aa2747e fix: add viewport meta tag for mobile layout (#110) 2025-12-05 23:34:18 +09:00
Dayuan Jiang
7205896c8c feat: add mobile layout with chat panel at bottom (#109) 2025-12-05 23:25:59 +09:00
Dayuan Jiang
4e32a094b1 fix: restore status notice indicator removed in PR #77 (#107) 2025-12-05 23:22:29 +09:00
Dayuan Jiang
96a1111654 fix: ensure markdown text in user messages is visible (#108)
The prose plugin was overriding text colors for markdown elements
(bold, headings, etc.) in user message bubbles, causing text to
blend with the dark primary background.

Added conditional styling that forces all child elements in user
messages to use text-primary-foreground color with !important to
override prose defaults.
2025-12-05 23:16:59 +09:00
Dayuan Jiang
3f35c52527 feat: add draw.io theme toggle between minimal and sketch (#106)
- Add toggle button in chat input area to switch between min and sketch themes
- Show warning dialog before switching (clears messages and diagram)
- Persist theme selection in localStorage
- Default theme is minimal (hides shapes sidebar)
2025-12-05 23:10:48 +09:00
Dayuan Jiang
0af5229477 feat: add markdown rendering and resizable chat panel (#104)
* feat: add markdown rendering for chat messages

- Add react-markdown and @tailwindcss/typography for markdown support
- Use prose styling for assistant message formatting
- Fix Radix ScrollArea viewport horizontal overflow issue
- Add CSS fix for viewport width constraint

* feat: add resizable chat panel

- Replace fixed width layout with react-resizable-panels
- Chat panel can be resized by dragging the handle
- Panel is collapsible with min 15% and max 50% width
- Ctrl+B keyboard shortcut still works for toggle
2025-12-05 22:42:39 +09:00
Twelveeee
3fb349fb3e clear button cant clear error msg & feat: add setting dialog and add accesscode (#77)
* fix: clear button cant clear error msg

* new: add setting dialog and add accesscode

* fix: address review feedback - dark mode, types, formatting

* feat: only show Settings button when access code is required

* refactor: rename ACCESS_CODES to ACCESS_CODE_LIST

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2025-12-05 22:09:34 +09:00
Dayuan Jiang
ed29e32ba3 feat: restore Langfuse observability integration (#103)
- Add lib/langfuse.ts with client, trace input/output, telemetry config
- Add instrumentation.ts for OpenTelemetry setup with Langfuse span processor
- Add /api/log-save endpoint for logging diagram saves
- Add /api/log-feedback endpoint for thumbs up/down feedback
- Update chat route with sessionId tracking and telemetry
- Add feedback buttons (thumbs up/down) to chat messages
- Add sessionId tracking throughout the app
- Update env.example with Langfuse configuration
- Add @langfuse/client, @langfuse/otel, @langfuse/tracing, @opentelemetry/sdk-trace-node
2025-12-05 21:15:02 +09:00
Dayuan Jiang
4cd78dc561 chore: remove complex 503 error handling code (#102)
- Remove 15s streaming timeout detection (too slow, added complexity)
- Remove status indicator (issue resolved by switching model)
- Remove streamingError state and related refs
- Simplify onFinish callback (remove 503 detection logging)
- Remove errorHandler function (use default AI SDK errors)

The real fix was switching from global.* to us.* Bedrock model.
This removes ~134 lines of unnecessary complexity.
2025-12-05 20:18:19 +09:00
Dayuan Jiang
e0c5d966e3 feat: add image upload validation with 2MB limit and max 5 files (#101)
- Add 2MB file size limit with client and server-side validation
- Add max 5 files limit per upload
- Add sonner toast library for better error notifications
- Create ErrorToast component with keyboard accessibility
- Batch multiple validation errors into single toast
- Validate file size in all upload methods (input, paste, drag-drop)
- Add server-side validation in /api/chat endpoint
2025-12-05 19:30:50 +09:00
Dayuan Jiang
33471d5b3a docs: add AI provider configuration guide (#100)
- Add docs/ai-providers.md with detailed setup instructions for all providers
- Update README.md, README_CN.md, README_JA.md with provider guide links
- Add model capability requirements note
- Simplify provider list in READMEs

Closes #79
2025-12-05 18:53:34 +09:00
Dayuan Jiang
3ef9908df7 feat: add confirmation dialog to prevent accidental back navigation (#99)
Addresses conflict between right-click drag and browser back gesture in
Chromium-based browsers. Shows browser confirmation dialog when user
tries to navigate away, preventing accidental page exits.

Closes #80
2025-12-05 18:42:36 +09:00
Dayuan Jiang
57bfc9cef7 fix: update status indicator to show outage resolved (#98) 2025-12-05 18:07:25 +09:00
Dayuan Jiang
0543f71c43 fix: use console.log instead of console.error for XML validation during streaming (#96) 2025-12-05 16:59:14 +09:00
Dayuan Jiang
970b88612d fix: add service status indicator for ongoing issues (#95) 2025-12-05 16:46:17 +09:00
Dayuan Jiang
c805277a76 fix: enable UI retry when Bedrock returns early 503 error (#94)
- Add error prop to ChatInput to detect error state
- Update isDisabled logic to allow retry when there's an error
- Pass combined error (SDK error + streamingError) to ChatInput

When Bedrock returns 503 ServiceUnavailableException before streaming
starts, AI SDK's onError fires but status may not transition to "ready".
This fix ensures the input is re-enabled when an error occurs, allowing
users to retry their request.
2025-12-05 16:22:38 +09:00
Dayuan Jiang
95160f5a21 fix: handle Bedrock 503 streaming errors with timeout detection (#92)
- Add 15s streaming timeout to detect mid-stream stalls (e.g., Bedrock 503)
- Add stop() call to allow user retry after timeout
- Add streamingError state for timeout-detected errors
- Improve server-side error logging for empty usage detection
- Add user-friendly error messages for ServiceUnavailable and Throttling errors
2025-12-05 14:23:47 +09:00
broBinChen
b206e16c02 fix: clear files when clicking text-only examples (#82)
Fixed an issue where files from previous examples would persist when clicking on "Animated Diagram" or "Creative Drawing" examples that don't require image uploads.
2025-12-05 14:07:14 +09:00
broBinChen
563b18e8ff refactor: replace deprecated addToolResult with addToolOutput (#85)
Replaced the deprecated addToolResult API with the new addToolOutput API from ai to ensure compatibility with future versions.
2025-12-05 14:02:45 +09:00
dayuan.jiang
2366255e8f fix: use credential provider chain for bedrock IAM role support 2025-12-05 09:19:26 +09:00
dayuan.jiang
255308f829 fix: make bedrock credentials optional for IAM role support 2025-12-05 09:11:10 +09:00
dayuan.jiang
a9493c8877 fix: write env vars to .env.production for Amplify SSR runtime 2025-12-05 09:04:54 +09:00
dayuan.jiang
a0c3db100a fix: add favicon.ico to public folder for header logo 2025-12-05 08:56:34 +09:00
196 changed files with 46283 additions and 6384 deletions

View File

@@ -1,6 +1,3 @@
{ {
"extends": [ "extends": ["next/core-web-vitals", "next/typescript"]
"next/core-web-vitals",
"next/typescript"
]
} }

35
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,35 @@
# Contributing
## Setup
```bash
git clone https://github.com/YOUR_USERNAME/next-ai-draw-io.git
cd next-ai-draw-io
npm install
cp env.example .env.local
npm run dev
```
## Code Style
We use [Biome](https://biomejs.dev/) for linting and formatting:
```bash
npm run format # Format code
npm run lint # Check lint errors
npm run check # Run all checks (CI)
```
Pre-commit hooks via Husky will run Biome automatically on staged files.
For a better experience, install the [Biome VS Code extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) for real-time linting and format-on-save.
## Pull Requests
1. Create a feature branch
2. Make changes and ensure `npm run check` passes
3. Submit PR against `main` with a clear description
## Issues
Include steps to reproduce, expected vs actual behavior, and AI provider used.

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug Report
about: Report a bug to help us improve
title: '[Bug] '
labels: bug
assignees: ''
---
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your thoughts.
## Bug Description
A brief description of the issue.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll to '...'
4. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Screenshots
If applicable, add screenshots to help explain the problem.
## Environment
- OS: [e.g. Windows 11, macOS 14]
- Browser: [e.g. Chrome 120, Safari 17]
- Version: [e.g. 1.0.0]
## Additional Context
Any other information about the problem.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/DayuanJiang/next-ai-draw-io/discussions
about: Have questions or ideas? Feel free to start a discussion

24
.github/ISSUE_TEMPLATE/enhancement.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Enhancement
about: Suggest an improvement to existing functionality
title: '[Enhancement] '
labels: enhancement
assignees: ''
---
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
## Current Behavior
Describe how the feature currently works.
## Proposed Enhancement
How you'd like this to be improved.
## Motivation
Why this enhancement would be beneficial.
## Screenshots / Mockups
If applicable, add screenshots or mockups to illustrate the proposed changes.
## Additional Context
Any other information about the enhancement request.

View File

@@ -0,0 +1,25 @@
---
name: Feature Request
about: Suggest a new feature for this project
title: '[Feature] '
labels: enhancement
assignees: ''
---
> **Note**: This template is just a guide. Feel free to ignore the format entirely - any feedback is welcome! Don't let the template stop you from sharing your ideas.
## Feature Description
A brief description of the feature you'd like.
## Problem Context
Is this related to a problem? Please describe.
e.g. I'm always frustrated when [...]
## Proposed Solution
How you'd like this feature to work.
## Alternatives Considered
Any alternative solutions or features you've considered.
## Additional Context
Any other information or screenshots about the feature request.

40
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["after 10am on saturday"],
"timezone": "Asia/Tokyo",
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchPackagePatterns": ["*"],
"groupName": "minor and patch dependencies",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"matchPackagePatterns": ["*"],
"automerge": false
},
{
"matchPackagePatterns": ["@ai-sdk/*"],
"groupName": "AI SDK packages"
},
{
"matchPackagePatterns": ["@radix-ui/*"],
"groupName": "Radix UI packages"
},
{
"matchPackagePatterns": ["electron", "electron-builder"],
"groupName": "Electron packages",
"automerge": false
},
{
"matchPackagePatterns": ["@ai-sdk/*", "ai", "next"],
"groupName": "Core framework packages",
"automerge": false
}
],
"vulnerabilityAlerts": {
"enabled": true
}
}

54
.github/workflows/auto-format.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Auto Format
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: write
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Run Biome format
run: npx @biomejs/biome@latest check --write --no-errors-on-unmatched .
- name: Check for changes
id: changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# For fork PRs, just fail if formatting is needed (can't push to forks)
- name: Fail if fork PR needs formatting
if: steps.changes.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::This PR has formatting issues. Please run 'npx @biomejs/biome check --write .' locally and push the changes."
git diff --stat
exit 1
# For same-repo PRs, commit and push the changes
- name: Commit changes
if: steps.changes.outputs.has_changes == 'true' && github.event.pull_request.head.repo.full_name == github.repository
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git add .
git commit -m "style: auto-format with Biome"
git push origin HEAD:${{ github.head_ref }}

44
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Type check
run: npx tsc --noEmit
- name: Lint check
run: npm run check
- name: Build
run: npm run build
- name: Security audit
run: npm audit --audit-level=high --omit=dev

View File

@@ -26,7 +26,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -54,7 +54,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
@@ -63,4 +63,30 @@ jobs:
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: |
NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=true
# Push to AWS ECR for App Runner auto-deploy
- name: Configure AWS credentials
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to Amazon ECR
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Push to ECR (triggers App Runner auto-deploy)
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
env:
REPO_LOWER: ${{ github.repository }}
run: |
REPO_LOWER=$(echo "$REPO_LOWER" | tr '[:upper:]' '[:lower:]')
docker pull ghcr.io/${REPO_LOWER}:latest
docker tag ghcr.io/${REPO_LOWER}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest
docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com/next-ai-draw-io:latest

46
.github/workflows/electron-release.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Electron Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version tag (e.g., v0.4.5)"
required: false
jobs:
build:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: mac
- os: windows-latest
platform: win
- os: ubuntu-latest
platform: linux
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build and publish Electron app
run: npm run dist:${{ matrix.platform }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

28
.gitignore vendored
View File

@@ -2,6 +2,8 @@
# dependencies # dependencies
/node_modules /node_modules
packages/*/node_modules
packages/*/dist
/.pnp /.pnp
.pnp.* .pnp.*
.yarn/* .yarn/*
@@ -40,5 +42,27 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
push-via-ec2.sh push-via-ec2.sh
.claude/settings.local.json .claude/
.playwright-mcp/ .playwright-mcp/
# Cloudflare
.dev.vars
.open-next/
.wrangler/
.env*.local
# Electron
/dist-electron/
/release/
/electron-standalone/
*.dmg
*.exe
*.AppImage
*.deb
*.rpm
*.snap
CLAUDE.md
.spec-workflow
# edgeone
.edgeone

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

23
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
}
}

View File

@@ -1,7 +1,7 @@
# Multi-stage Dockerfile for Next.js # Multi-stage Dockerfile for Next.js
# Stage 1: Install dependencies # Stage 1: Install dependencies
FROM node:20-alpine AS deps FROM node:24-alpine AS deps
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@@ -9,10 +9,10 @@ WORKDIR /app
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
# Install dependencies # Install dependencies
RUN npm ci RUN npm install
# Stage 2: Build application # Stage 2: Build application
FROM node:20-alpine AS builder FROM node:24-alpine AS builder
WORKDIR /app WORKDIR /app
# Copy node_modules from deps stage # Copy node_modules from deps stage
@@ -22,11 +22,19 @@ COPY . .
# Disable Next.js telemetry during build # Disable Next.js telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
# Build-time argument for self-hosted draw.io URL
ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net
ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL}
# Build-time argument to show About link and Notice icon
ARG NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=false
ENV NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE=${NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE}
# Build Next.js application (standalone mode) # Build Next.js application (standalone mode)
RUN npm run build RUN npm run build
# Stage 3: Production runtime # Stage 3: Production runtime
FROM node:20-alpine AS runner FROM node:24-alpine AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -50,6 +58,6 @@ EXPOSE 3000
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
# Start the application # Start the application (HOSTNAME override needed for AWS App Runner)
CMD ["node", "server.js"] CMD ["sh", "-c", "HOSTNAME=0.0.0.0 exec node server.js"]

299
README.md
View File

@@ -4,31 +4,50 @@
**AI-Powered Diagram Creation Tool - Chat, Draw, Visualize** **AI-Powered Diagram Creation Tool - Chat, Draw, Visualize**
English | [中文](./README_CN.md) | [日本語](./README_JA.md) English | [中文](./docs/cn/README_CN.md) | [日本語](./docs/ja/README_JA.md)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TrendShift](https://trendshift.io/api/badge/repositories/15449)](https://next-ai-drawio.jiang.jp/)
[![Next.js](https://img.shields.io/badge/Next.js-15.x-black)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Next.js](https://img.shields.io/badge/Next.js-16.x-black)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19.x-61dafb)](https://react.dev/)
[![Sponsor](https://img.shields.io/badge/Sponsor-❤-ea4aaa)](https://github.com/sponsors/DayuanJiang) [![Sponsor](https://img.shields.io/badge/Sponsor-❤-ea4aaa)](https://github.com/sponsors/DayuanJiang)
[🚀 Live Demo](https://next-ai-drawio.jiang.jp/) [![Live Demo](./public/live-demo-button.svg)](https://next-ai-drawio.jiang.jp/)
</div> </div>
A Next.js web application that integrates AI capabilities with draw.io diagrams. Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization. A Next.js web application that integrates AI capabilities with draw.io diagrams. Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979 > Note: Thanks to <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) sponsorship, the demo site now uses the powerful K2-thinking model!
## Features
- **LLM-Powered Diagram Creation**: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
- **Image-Based Diagram Replication**: Upload existing diagrams or images and have the AI replicate and enhance them automatically
- **Diagram History**: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing.
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
- **AWS Architecture Diagram Support**: Specialized support for generating AWS architecture diagrams
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
## **Examples**
## Table of Contents
- [Next AI Draw.io](#next-ai-drawio)
- [Table of Contents](#table-of-contents)
- [Examples](#examples)
- [Features](#features)
- [MCP Server (Preview)](#mcp-server-preview)
- [Claude Code CLI](#claude-code-cli)
- [Getting Started](#getting-started)
- [Try it Online](#try-it-online)
- [Desktop Application](#desktop-application)
- [Run with Docker](#run-with-docker)
- [Installation](#installation)
- [Deployment](#deployment)
- [Deploy to EdgeOne Pages](#deploy-to-edgeone-pages)
- [Deploy on Vercel (Recommended)](#deploy-on-vercel-recommended)
- [Deploy on Cloudflare Workers](#deploy-on-cloudflare-workers)
- [Multi-Provider Support](#multi-provider-support)
- [How It Works](#how-it-works)
- [Support \& Contact](#support--contact)
- [Star History](#star-history)
## Examples
Here are some example prompts and their generated diagrams: Here are some example prompts and their generated diagrams:
@@ -68,6 +87,143 @@ Here are some example prompts and their generated diagrams:
</table> </table>
</div> </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:
[![Live Demo](./public/live-demo-button.svg)](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:
[![Deploy to EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
Check out the [Tencent EdgeOne Pages documentation](https://pages.edgeone.ai/document/deployment-overview) for more details.
Additionally, deploying through Tencent EdgeOne Pages will also grant you a [daily free quota for DeepSeek models](https://pages.edgeone.ai/document/edge-ai).
### Deploy on Vercel (Recommended)
[![Deploy with Vercel](https://vercel.com/button)](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://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
- AWS Bedrock (default)
- OpenAI
- Anthropic
- Google AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
- SiliconFlow
- SGLang
- Vercel AI Gateway
All providers except AWS Bedrock and OpenRouter support custom endpoints.
📖 **[Detailed Provider Configuration Guide](./docs/en/ai-providers.md)** - See setup instructions for each provider.
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
Note that the `claude` series has been trained on draw.io diagrams with cloud architecture logos like AWS, Azure, GCP. So if you want to create cloud architecture diagrams, this is the best choice.
## How It Works ## How It Works
The application uses the following technologies: The application uses the following technologies:
@@ -78,122 +234,11 @@ The application uses the following technologies:
Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly. Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.
## Multi-Provider Support
- AWS Bedrock (default)
- OpenAI / OpenAI-compatible APIs (via `OPENAI_BASE_URL`)
- Anthropic
- Google AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
Note that `claude-sonnet-4-5` has trained on draw.io diagrams with AWS logos, so if you want to create AWS architecture diagrams, this is the best choice.
## Getting Started
### Run with Docker (Recommended)
If you just want to run it locally, the best way is to use Docker.
First, install Docker if you haven't already: [Get Docker](https://docs.docker.com/get-docker/)
Then run:
```bash
docker run -d -p 3000:3000 \
-e AI_PROVIDER=openai \
-e AI_MODEL=gpt-4o \
-e OPENAI_API_KEY=your_api_key \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
Open [http://localhost:3000](http://localhost:3000) in your browser.
Replace the environment variables with your preferred AI provider configuration. See [Multi-Provider Support](#multi-provider-support) for available options.
### Installation
1. Clone the repository:
```bash
git clone https://github.com/DayuanJiang/next-ai-draw-io
cd next-ai-draw-io
```
2. Install dependencies:
```bash
npm install
# or
yarn install
```
3. Configure your AI provider:
Create a `.env.local` file in the root directory:
```bash
cp env.example .env.local
```
Edit `.env.local` and configure your chosen provider:
- Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek)
- Set `AI_MODEL` to the specific model you want to use
- Add the required API keys for your provider
See the [Multi-Provider Support](#multi-provider-support) section above for provider-specific configuration examples.
4. Run the development server:
```bash
npm run dev
```
5. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.
## Deployment
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js.
Check out the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Or you can deploy by this button.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
Be sure to **set the environment variables** in the Vercel dashboard as you did in your local `.env.local` file.
## Project Structure
```
app/ # Next.js App Router
api/chat/ # Chat API endpoint with AI tools
page.tsx # Main page with DrawIO embed
components/ # React components
chat-panel.tsx # Chat interface with diagram control
chat-input.tsx # User input component with file upload
history-dialog.tsx # Diagram version history viewer
ui/ # UI components (buttons, cards, etc.)
contexts/ # React context providers
diagram-context.tsx # Global diagram state management
lib/ # Utility functions and helpers
ai-providers.ts # Multi-provider AI configuration
utils.ts # XML processing and conversion utilities
public/ # Static assets including example images
```
## TODOs
- [x] Allow the LLM to modify the XML instead of generating it from scratch everytime.
- [x] Improve the smoothness of shape streaming updates.
- [x] Add multiple AI provider support (OpenAI, Anthropic, Google, Azure, Ollama)
- [x] Solve the bug that generation will fail for session that longer than 60s.
- [ ] Add API config on the UI.
## Support & Contact ## Support & Contact
**Special thanks to [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) for sponsoring the API token usage of the demo site!** Register on the ARK platform to get 500K free tokens for all models!
If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site! If you find this project useful, please consider [sponsoring](https://github.com/sponsors/DayuanJiang) to help me host the live demo site!
For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at: For support or inquiries, please open an issue on the GitHub repository or contact the maintainer at:

View File

@@ -1,207 +0,0 @@
# Next AI Draw.io
<div align="center">
**AI驱动的图表创建工具 - 对话、绘制、可视化**
[English](./README.md) | 中文 | [日本語](./README_JA.md)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Next.js](https://img.shields.io/badge/Next.js-15.x-black)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
[![Sponsor](https://img.shields.io/badge/Sponsor-❤-ea4aaa)](https://github.com/sponsors/DayuanJiang)
[🚀 在线演示](https://next-ai-drawio.jiang.jp/)
</div>
一个集成了AI功能的Next.js网页应用与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
## 功能特性
- **LLM驱动的图表创建**利用大语言模型通过自然语言命令直接创建和操作draw.io图表
- **基于图像的图表复制**上传现有图表或图像让AI自动复制和增强
- **图表历史记录**全面的版本控制跟踪所有更改允许您查看和恢复AI编辑前的图表版本
- **交互式聊天界面**与AI实时对话来完善您的图表
- **AWS架构图支持**专门支持生成AWS架构图
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
## **示例**
以下是一些示例提示词及其生成的图表:
<div align="center">
<table width="100%">
<tr>
<td colspan="2" valign="top" align="center">
<strong>动画Transformer连接器</strong><br />
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
<img src="./public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCP架构图</strong><br />
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="./public/gcp_demo.svg" alt="GCP架构图" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWS架构图</strong><br />
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="./public/aws_demo.svg" alt="AWS架构图" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azure架构图</strong><br />
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="./public/azure_demo.svg" alt="Azure架构图" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫咪素描</strong><br />
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
<img src="./public/cat_demo.svg" alt="猫咪绘图" width="240" />
</td>
</tr>
</table>
</div>
## 工作原理
本应用使用以下技术:
- **Next.js**:用于前端框架和路由
- **Vercel AI SDK**`ai` + `@ai-sdk/*`用于流式AI响应和多提供商支持
- **react-drawio**:用于图表表示和操作
图表以XML格式表示可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
## 多提供商支持
- AWS Bedrock默认
- OpenAI / OpenAI兼容API通过 `OPENAI_BASE_URL`
- Anthropic
- Google AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
注意:`claude-sonnet-4-5` 已在带有AWS标志的draw.io图表上进行训练因此如果您想创建AWS架构图这是最佳选择。
## 快速开始
### 使用Docker运行推荐
如果您只想在本地运行最好的方式是使用Docker。
首先如果您还没有安装Docker请先安装[获取Docker](https://docs.docker.com/get-docker/)
然后运行:
```bash
docker run -d -p 3000:3000 \
-e AI_PROVIDER=openai \
-e AI_MODEL=gpt-4o \
-e OPENAI_API_KEY=your_api_key \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
请根据您首选的AI提供商配置替换环境变量。可用选项请参阅[多提供商支持](#多提供商支持)。
### 安装
1. 克隆仓库:
```bash
git clone https://github.com/DayuanJiang/next-ai-draw-io
cd next-ai-draw-io
```
2. 安装依赖:
```bash
npm install
# 或
yarn install
```
3. 配置您的AI提供商
在根目录创建 `.env.local` 文件:
```bash
cp env.example .env.local
```
编辑 `.env.local` 并配置您选择的提供商:
-`AI_PROVIDER` 设置为您选择的提供商bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek
-`AI_MODEL` 设置为您要使用的特定模型
- 添加您的提供商所需的API密钥
请参阅上面的[多提供商支持](#多提供商支持)部分了解特定提供商的配置示例。
4. 运行开发服务器:
```bash
npm run dev
```
5. 在浏览器中打开 [http://localhost:3000](http://localhost:3000) 查看应用。
## 部署
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
或者您可以通过此按钮部署:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
请确保在Vercel控制台中**设置环境变量**,就像您在本地 `.env.local` 文件中所做的那样。
## 项目结构
```
app/ # Next.js App Router
api/chat/ # 带AI工具的聊天API端点
page.tsx # 带DrawIO嵌入的主页面
components/ # React组件
chat-panel.tsx # 带图表控制的聊天界面
chat-input.tsx # 带文件上传的用户输入组件
history-dialog.tsx # 图表版本历史查看器
ui/ # UI组件按钮、卡片等
contexts/ # React上下文提供者
diagram-context.tsx # 全局图表状态管理
lib/ # 工具函数和辅助程序
ai-providers.ts # 多提供商AI配置
utils.ts # XML处理和转换工具
public/ # 静态资源包括示例图片
```
## 待办事项
- [x] 允许LLM修改XML而不是每次从头生成
- [x] 提高形状流式更新的流畅度
- [x] 添加多AI提供商支持OpenAI, Anthropic, Google, Azure, Ollama
- [x] 解决超过60秒的会话生成失败的bug
- [ ] 在UI上添加API配置
## 支持与联系
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
如需支持或咨询请在GitHub仓库上提交issue或联系维护者
- 邮箱me[at]jiang.jp
## Star历史
[![Star History Chart](https://api.star-history.com/svg?repos=DayuanJiang/next-ai-draw-io&type=date&legend=top-left)](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
---

View File

@@ -1,207 +0,0 @@
# Next AI Draw.io
<div align="center">
**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**
[English](./README.md) | [中文](./README_CN.md) | 日本語
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Next.js](https://img.shields.io/badge/Next.js-15.x-black)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
[![Sponsor](https://img.shields.io/badge/Sponsor-❤-ea4aaa)](https://github.com/sponsors/DayuanJiang)
[🚀 ライブデモ](https://next-ai-drawio.jiang.jp/)
</div>
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
## 機能
- **LLM搭載のダイアグラム作成**大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
- **画像ベースのダイアグラム複製**既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
- **ダイアグラム履歴**すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
- **インタラクティブなチャットインターフェース**AIとリアルタイムでコミュニケーションしてダイアグラムを改善
- **AWSアーキテクチャダイアグラムサポート**AWSアーキテクチャダイアグラムの生成を専門的にサポート
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
## **例**
以下はいくつかのプロンプト例と生成されたダイアグラムです:
<div align="center">
<table width="100%">
<tr>
<td colspan="2" valign="top" align="center">
<strong>アニメーションTransformerコネクタ</strong><br />
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
<img src="./public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCPアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="./public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWSアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="./public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azureアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="./public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫のスケッチ</strong><br />
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
<img src="./public/cat_demo.svg" alt="猫の絵" width="240" />
</td>
</tr>
</table>
</div>
## 仕組み
本アプリケーションは以下の技術を使用しています:
- **Next.js**:フロントエンドフレームワークとルーティング
- **Vercel AI SDK**`ai` + `@ai-sdk/*`ストリーミングAIレスポンスとマルチプロバイダーサポート
- **react-drawio**:ダイアグラムの表現と操作
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
## マルチプロバイダーサポート
- AWS Bedrockデフォルト
- OpenAI / OpenAI互換API`OPENAI_BASE_URL`経由)
- Anthropic
- Google AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
注:`claude-sonnet-4-5`はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。
## はじめに
### Dockerで実行推奨
ローカルで実行したいだけなら、Dockerを使用するのが最も簡単です。
まず、Dockerをインストールしていない場合はインストールしてください[Dockerを入手](https://docs.docker.com/get-docker/)
次に実行:
```bash
docker run -d -p 3000:3000 \
-e AI_PROVIDER=openai \
-e AI_MODEL=gpt-4o \
-e OPENAI_API_KEY=your_api_key \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
ブラウザで [http://localhost:3000](http://localhost:3000) を開いてください。
環境変数はお好みのAIプロバイダー設定に置き換えてください。利用可能なオプションについては[マルチプロバイダーサポート](#マルチプロバイダーサポート)を参照してください。
### インストール
1. リポジトリをクローン:
```bash
git clone https://github.com/DayuanJiang/next-ai-draw-io
cd next-ai-draw-io
```
2. 依存関係をインストール:
```bash
npm install
# または
yarn install
```
3. AIプロバイダーを設定
ルートディレクトリに`.env.local`ファイルを作成:
```bash
cp env.example .env.local
```
`.env.local`を編集して選択したプロバイダーを設定:
- `AI_PROVIDER`を選択したプロバイダーに設定bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek
- `AI_MODEL`を使用する特定のモデルに設定
- プロバイダーに必要なAPIキーを追加
プロバイダー固有の設定例については、上記の[マルチプロバイダーサポート](#マルチプロバイダーサポート)セクションを参照してください。
4. 開発サーバーを起動:
```bash
npm run dev
```
5. ブラウザで[http://localhost:3000](http://localhost:3000)を開いてアプリケーションを確認。
## デプロイ
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
または、このボタンでデプロイできます:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
ローカルの`.env.local`ファイルと同様に、Vercelダッシュボードで**環境変数を設定**してください。
## プロジェクト構造
```
app/ # Next.js App Router
api/chat/ # AIツール付きチャットAPIエンドポイント
page.tsx # DrawIO埋め込み付きメインページ
components/ # Reactコンポーネント
chat-panel.tsx # ダイアグラム制御付きチャットインターフェース
chat-input.tsx # ファイルアップロード付きユーザー入力コンポーネント
history-dialog.tsx # ダイアグラムバージョン履歴ビューア
ui/ # UIコンポーネントボタン、カードなど
contexts/ # Reactコンテキストプロバイダー
diagram-context.tsx # グローバルダイアグラム状態管理
lib/ # ユーティリティ関数とヘルパー
ai-providers.ts # マルチプロバイダーAI設定
utils.ts # XML処理と変換ユーティリティ
public/ # サンプル画像を含む静的アセット
```
## TODO
- [x] LLMが毎回ゼロから生成する代わりにXMLを修正できるようにする
- [x] シェイプストリーミング更新の滑らかさを改善
- [x] 複数のAIプロバイダーサポートを追加OpenAI, Anthropic, Google, Azure, Ollama
- [x] 60秒以上のセッションで生成が失敗するバグを解決
- [ ] UIにAPI設定を追加
## サポート&お問い合わせ
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
サポートやお問い合わせについては、GitHubリポジトリでissueを開くか、メンテナーにご連絡ください
- メールme[at]jiang.jp
## スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=DayuanJiang/next-ai-draw-io&type=date&legend=top-left)](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
---

View File

@@ -0,0 +1,416 @@
import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { FaGithub } from "react-icons/fa"
export const metadata: Metadata = {
title: "关于 - Next AI Draw.io",
description:
"AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
}
function formatNumber(num: number): string {
if (num >= 1000) {
return `${num / 1000}k`
}
return num.toString()
}
export default function AboutCN() {
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-xl font-bold text-gray-900 hover:text-gray-700"
>
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
</Link>
<Link
href="/about/cn"
className="text-blue-600 font-semibold"
>
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="在GitHub上查看"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Next AI Draw.io
</h1>
<p className="text-xl text-gray-600 font-medium">
AI驱动的图表创建工具 -
</p>
</div>
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
{/* Header */}
<div className="mb-4">
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
</h3>
</div>
{/* Story */}
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
<p>
{" "}
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-blue-600 hover:underline"
>
</a>
{" "}
<span className="font-semibold text-amber-700">
K2-thinking
</span>{" "}
{" "}
<span className="font-semibold text-amber-700">
50Token
</span>
</p>
</div>
{/* Usage Limits */}
<p className="text-sm text-gray-600 mb-3">
使
</p>
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyRequestLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyTokenLimit)}
</p>
<p className="text-xs text-gray-500">
Token/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(tpmLimit)}
</p>
<p className="text-xs text-gray-500">
Token/
</p>
</div>
</div>
{/* Divider */}
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
</div>
{/* Bring Your Own Key */}
<div className="text-center">
<h4 className="text-base font-bold text-gray-900 mb-2">
使 API Key
</h4>
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
使 API
Key
</p>
<p className="text-xs text-gray-500 max-w-md mx-auto">
Key
</p>
</div>
</div>
</div>
<p className="text-gray-700">
AI功能的Next.js网页应用draw.io图表无缝结合AI辅助可视化来创建
</p>
{/* Features */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li>
<strong>LLM驱动的图表创建</strong>
draw.io图表
</li>
<li>
<strong></strong>
AI自动复制和增强
</li>
<li>
<strong></strong>
AI编辑前的图表版本
</li>
<li>
<strong></strong>
AI实时对话来完善您的图表
</li>
<li>
<strong>AWS架构图支持</strong>
AWS架构图
</li>
<li>
<strong></strong>
</li>
</ul>
{/* Examples */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<p className="text-gray-700 mb-6">
</p>
<div className="space-y-8">
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Transformer连接器
</h3>
<p className="text-gray-600 mb-4">
<strong></strong>
<strong></strong>Transformer架构图
</p>
<Image
src="/animated_connectors.svg"
alt="带动画连接器的Transformer架构"
width={480}
height={360}
className="mx-auto"
/>
</div>
{/* Cloud Architecture Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
GCP架构图
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使
<strong>GCP图标</strong>
GCP架构图
</p>
<Image
src="/gcp_demo.svg"
alt="GCP架构图"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
AWS架构图
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使
<strong>AWS图标</strong>
AWS架构图
</p>
<Image
src="/aws_demo.svg"
alt="AWS架构图"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Azure架构图
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使
<strong>Azure图标</strong>
Azure架构图
</p>
<Image
src="/azure_demo.svg"
alt="Azure架构图"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
</p>
<Image
src="/cat_demo.svg"
alt="猫咪绘图"
width={240}
height={240}
className="mx-auto"
/>
</div>
</div>
</div>
{/* How It Works */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<p className="text-gray-700 mb-4">使</p>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li>
<strong>Next.js</strong>
</li>
<li>
<strong>Vercel AI SDK</strong><code>ai</code> +{" "}
<code>@ai-sdk/*</code>
用于流式AI响应和多提供商支持
</li>
<li>
<strong>react-drawio</strong>:用于图表表示和操作
</li>
</ul>
<p className="text-gray-700 mt-4">
图表以XML格式表示可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
</p>
{/* Multi-Provider Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-1">
<li>
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
</a>
</li>
<li>AWS Bedrock</li>
<li>
OpenAI / OpenAI兼容API{" "}
<code>OPENAI_BASE_URL</code>
</li>
<li>Anthropic</li>
<li>Google AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
<li>SiliconFlow</li>
</ul>
<p className="text-gray-700 mt-4">
<code>claude-sonnet-4-5</code>{" "}
AWS标志的draw.io图表上进行训练AWS架构图
</p>
{/* Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<p className="text-gray-700 mb-4 font-semibold">
{" "}
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
</a>{" "}
API Token
</p>
<p className="text-gray-700">
{" "}
<a
href="https://github.com/sponsors/DayuanJiang"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
</a>{" "}
线
</p>
<p className="text-gray-700 mt-2">
{" "}
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
GitHub仓库
</a>{" "}
issue或联系me[at]jiang.jp
</p>
{/* CTA */}
<div className="mt-12 text-center">
<Link
href="/"
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
</Link>
</div>
</article>
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<p className="text-center text-gray-600 text-sm">
Next AI Draw.io - AI驱动的图表生成器
</p>
</div>
</footer>
</div>
)
}

View File

@@ -0,0 +1,432 @@
import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { FaGithub } from "react-icons/fa"
export const metadata: Metadata = {
title: "概要 - Next AI Draw.io",
description:
"AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
keywords: [
"AIダイアグラム",
"draw.io",
"AWSアーキテクチャ",
"GCPダイアグラム",
"Azureダイアグラム",
"LLM",
],
}
function formatNumber(num: number): string {
if (num >= 1000) {
return `${num / 1000}k`
}
return num.toString()
}
export default function AboutJA() {
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-xl font-bold text-gray-900 hover:text-gray-700"
>
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
</Link>
<Link
href="/about/ja"
className="text-blue-600 font-semibold"
>
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="GitHubで見る"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Next AI Draw.io
</h1>
<p className="text-xl text-gray-600 font-medium">
AI搭載のダイアグラム作成ツール -
</p>
</div>
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
{/* Header */}
<div className="mb-4">
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
ByteDance Doubao提供
</h3>
</div>
{/* Story */}
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
<p>
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-blue-600 hover:underline"
>
ByteDance Doubao
</a>
{" "}
<span className="font-semibold text-amber-700">
K2-thinking
</span>{" "}
使{" "}
<span className="font-semibold text-amber-700">
50
</span>
</p>
</div>
{/* Usage Limits */}
<p className="text-sm text-gray-600 mb-3">
使
</p>
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyRequestLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyTokenLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(tpmLimit)}
</p>
<p className="text-xs text-gray-500">
/
</p>
</div>
</div>
{/* Divider */}
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
</div>
{/* Bring Your Own Key */}
<div className="text-center">
<h4 className="text-base font-bold text-gray-900 mb-2">
APIキーを使用
</h4>
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
APIキーを使用することもできます
</p>
<p className="text-xs text-gray-500 max-w-md mx-auto">
</p>
</div>
</div>
</div>
<p className="text-gray-700">
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションですAI支援の可視化により
</p>
{/* Features */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li>
<strong>LLM搭載のダイアグラム作成</strong>
draw.ioダイアグラムを作成
</li>
<li>
<strong></strong>
AIが自動的に複製
</li>
<li>
<strong></strong>
AI編集前のダイアグラムの以前のバージョンを表示
</li>
<li>
<strong>
</strong>
AIとリアルタイムでコミュニケーションしてダイアグラムを改善
</li>
<li>
<strong>
AWSアーキテクチャダイアグラムサポート
</strong>
AWSアーキテクチャダイアグラムの生成を専門的にサポート
</li>
<li>
<strong></strong>
</li>
</ul>
{/* Examples */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<p className="text-gray-700 mb-6">
</p>
<div className="space-y-8">
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Transformerコネクタ
</h3>
<p className="text-gray-600 mb-4">
<strong></strong>{" "}
<strong></strong>
Transformerアーキテクチャ図を作成してください
</p>
<Image
src="/animated_connectors.svg"
alt="アニメーションコネクタ付きTransformerアーキテクチャ"
width={480}
height={360}
className="mx-auto"
/>
</div>
{/* Cloud Architecture Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
GCPアーキテクチャ図
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>GCPアイコン</strong>
使GCPアーキテクチャ図を生成してください
</p>
<Image
src="/gcp_demo.svg"
alt="GCPアーキテクチャ図"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
AWSアーキテクチャ図
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>AWSアイコン</strong>
使AWSアーキテクチャ図を生成してください
</p>
<Image
src="/aws_demo.svg"
alt="AWSアーキテクチャ図"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Azureアーキテクチャ図
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
<strong>Azureアイコン</strong>
使Azureアーキテクチャ図を生成してください
</p>
<Image
src="/azure_demo.svg"
alt="Azureアーキテクチャ図"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>{" "}
</p>
<Image
src="/cat_demo.svg"
alt="猫の絵"
width={240}
height={240}
className="mx-auto"
/>
</div>
</div>
</div>
{/* How It Works */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<p className="text-gray-700 mb-4">
使
</p>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li>
<strong>Next.js</strong>
</li>
<li>
<strong>Vercel AI SDK</strong><code>ai</code> +{" "}
<code>@ai-sdk/*</code>
ストリーミングAIレスポンスとマルチプロバイダーサポート
</li>
<li>
<strong>react-drawio</strong>
:ダイアグラムの表現と操作
</li>
</ul>
<p className="text-gray-700 mt-4">
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
</p>
{/* Multi-Provider Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-1">
<li>
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
ByteDance Doubao
</a>
</li>
<li>AWS Bedrock</li>
<li>
OpenAI / OpenAI互換API<code>OPENAI_BASE_URL</code>
</li>
<li>Anthropic</li>
<li>Google AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
<li>SiliconFlow</li>
</ul>
<p className="text-gray-700 mt-4">
<code>claude-sonnet-4-5</code>
AWSロゴ付きのdraw.ioダイアグラムで学習されているためAWSアーキテクチャダイアグラムを作成したい場合は最適な選択です
</p>
{/* Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
</h2>
<p className="text-gray-700 mb-4 font-semibold">
APIトークン使用を支援してくださった{" "}
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
ByteDance Doubao
</a>{" "}
</p>
<p className="text-gray-700">
{" "}
<a
href="https://github.com/sponsors/DayuanJiang"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
</a>{" "}
</p>
<p className="text-gray-700 mt-2">
{" "}
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
GitHubリポジトリ
</a>{" "}
issueを開くかme[at]jiang.jp
</p>
{/* CTA */}
<div className="mt-12 text-center">
<Link
href="/"
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
</Link>
</div>
</article>
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<p className="text-center text-gray-600 text-sm">
Next AI Draw.io -
AI搭載ダイアグラムジェネレーター
</p>
</div>
</footer>
</div>
)
}

453
app/[lang]/about/page.tsx Normal file
View File

@@ -0,0 +1,453 @@
import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { FaGithub } from "react-icons/fa"
export const metadata: Metadata = {
title: "About - Next AI Draw.io",
description:
"AI-Powered Diagram Creation Tool - Chat, Draw, Visualize. Create AWS, GCP, and Azure architecture diagrams with natural language.",
keywords: [
"AI diagram",
"draw.io",
"AWS architecture",
"GCP diagram",
"Azure diagram",
"LLM",
],
}
function formatNumber(num: number): string {
if (num >= 1000) {
return `${num / 1000}k`
}
return num.toString()
}
export default function About() {
const dailyRequestLimit = Number(process.env.DAILY_REQUEST_LIMIT) || 20
const dailyTokenLimit = Number(process.env.DAILY_TOKEN_LIMIT) || 500000
const tpmLimit = Number(process.env.TPM_LIMIT) || 50000
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link
href="/"
className="text-xl font-bold text-gray-900 hover:text-gray-700"
>
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
>
Editor
</Link>
<Link
href="/about"
className="text-blue-600 font-semibold"
>
About
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="View on GitHub"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Next AI Draw.io
</h1>
<p className="text-xl text-gray-600 font-medium">
AI-Powered Diagram Creation Tool - Chat, Draw,
Visualize
</p>
</div>
<div className="relative mb-8 rounded-2xl bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 p-[1px] shadow-lg">
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-amber-400 via-orange-400 to-yellow-400 opacity-20" />
<div className="relative rounded-2xl bg-white/80 backdrop-blur-sm p-6">
{/* Header */}
<div className="mb-4">
<h3 className="text-lg font-bold text-gray-900 tracking-tight">
Sponsored by ByteDance Doubao
</h3>
</div>
{/* Story */}
<div className="space-y-3 text-sm text-gray-700 leading-relaxed mb-5">
<p>
Great news! Thanks to the generous
sponsorship from{" "}
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-blue-600 hover:underline"
>
ByteDance Doubao
</a>
, the demo site now uses the powerful{" "}
<span className="font-semibold text-amber-700">
K2-thinking
</span>{" "}
model for better diagram generation! Sign up
via the link to get{" "}
<span className="font-semibold text-amber-700">
500K free tokens
</span>{" "}
for all models!
</p>
</div>
{/* Usage Limits */}
<p className="text-sm text-gray-600 mb-3">
Please note the current usage limits:
</p>
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyRequestLimit)}
</p>
<p className="text-xs text-gray-500">
requests/day
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(dailyTokenLimit)}
</p>
<p className="text-xs text-gray-500">
tokens/day
</p>
</div>
<div className="text-center p-3 bg-white/60 rounded-lg">
<p className="text-lg font-bold text-amber-600">
{formatNumber(tpmLimit)}
</p>
<p className="text-xs text-gray-500">
tokens/min
</p>
</div>
</div>
{/* Divider */}
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-amber-300 to-transparent" />
</div>
{/* Bring Your Own Key */}
<div className="text-center">
<h4 className="text-base font-bold text-gray-900 mb-2">
Bring Your Own API Key
</h4>
<p className="text-sm text-gray-600 mb-2 max-w-md mx-auto">
You can also use your own API key with any
supported provider. Click the Settings icon
in the chat panel to configure your provider
and API key.
</p>
<p className="text-xs text-gray-500 max-w-md mx-auto">
Your key is stored locally in your browser
and is never stored on the server.
</p>
</div>
</div>
</div>
<p className="text-gray-700">
A Next.js web application that integrates AI
capabilities with draw.io diagrams. Create, modify, and
enhance diagrams through natural language commands and
AI-assisted visualization.
</p>
{/* Features */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
Features
</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li>
<strong>LLM-Powered Diagram Creation</strong>:
Leverage Large Language Models to create and
manipulate draw.io diagrams directly through natural
language commands
</li>
<li>
<strong>Image-Based Diagram Replication</strong>:
Upload existing diagrams or images and have the AI
replicate and enhance them automatically
</li>
<li>
<strong>Diagram History</strong>: Comprehensive
version control that tracks all changes, allowing
you to view and restore previous versions of your
diagrams before the AI editing
</li>
<li>
<strong>Interactive Chat Interface</strong>:
Communicate with AI to refine your diagrams in
real-time
</li>
<li>
<strong>AWS Architecture Diagram Support</strong>:
Specialized support for generating AWS architecture
diagrams
</li>
<li>
<strong>Animated Connectors</strong>: Create dynamic
and animated connectors between diagram elements for
better visualization
</li>
</ul>
{/* Examples */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
Examples
</h2>
<p className="text-gray-700 mb-6">
Here are some example prompts and their generated
diagrams:
</p>
<div className="space-y-8">
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Animated Transformer Connectors
</h3>
<p className="text-gray-600 mb-4">
<strong>Prompt:</strong> Give me an{" "}
<strong>animated connector</strong> diagram of
transformer&apos;s architecture.
</p>
<Image
src="/animated_connectors.svg"
alt="Transformer Architecture with Animated Connectors"
width={480}
height={360}
className="mx-auto"
/>
</div>
{/* Cloud Architecture Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
GCP Architecture Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate a GCP
architecture diagram with{" "}
<strong>GCP icons</strong>. Users connect to
a frontend hosted on an instance.
</p>
<Image
src="/gcp_demo.svg"
alt="GCP Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
AWS Architecture Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate an AWS
architecture diagram with{" "}
<strong>AWS icons</strong>. Users connect to
a frontend hosted on an instance.
</p>
<Image
src="/aws_demo.svg"
alt="AWS Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Azure Architecture Diagram
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Generate an Azure
architecture diagram with{" "}
<strong>Azure icons</strong>. Users connect
to a frontend hosted on an instance.
</p>
<Image
src="/azure_demo.svg"
alt="Azure Architecture Diagram"
width={400}
height={300}
className="mx-auto"
/>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Cat Sketch
</h3>
<p className="text-gray-600 text-sm mb-4">
<strong>Prompt:</strong> Draw a cute cat for
me.
</p>
<Image
src="/cat_demo.svg"
alt="Cat Drawing"
width={240}
height={240}
className="mx-auto"
/>
</div>
</div>
</div>
{/* How It Works */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
How It Works
</h2>
<p className="text-gray-700 mb-4">
The application uses the following technologies:
</p>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li>
<strong>Next.js</strong>: For the frontend framework
and routing
</li>
<li>
<strong>Vercel AI SDK</strong> (<code>ai</code> +{" "}
<code>@ai-sdk/*</code>): For streaming AI responses
and multi-provider support
</li>
<li>
<strong>react-drawio</strong>: For diagram
representation and manipulation
</li>
</ul>
<p className="text-gray-700 mt-4">
Diagrams are represented as XML that can be rendered in
draw.io. The AI processes your commands and generates or
modifies this XML accordingly.
</p>
{/* Multi-Provider Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
Multi-Provider Support
</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-1">
<li>
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
ByteDance Doubao
</a>
</li>
<li>AWS Bedrock (default)</li>
<li>
OpenAI / OpenAI-compatible APIs (via{" "}
<code>OPENAI_BASE_URL</code>)
</li>
<li>Anthropic</li>
<li>Google AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
<li>SiliconFlow</li>
</ul>
<p className="text-gray-700 mt-4">
Note that <code>claude-sonnet-4-5</code> has trained on
draw.io diagrams with AWS logos, so if you want to
create AWS architecture diagrams, this is the best
choice.
</p>
{/* Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">
Support &amp; Contact
</h2>
<p className="text-gray-700 mb-4 font-semibold">
Special thanks to{" "}
<a
href="https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
ByteDance Doubao
</a>{" "}
for sponsoring the API token usage of the demo site!
</p>
<p className="text-gray-700">
If you find this project useful, please consider{" "}
<a
href="https://github.com/sponsors/DayuanJiang"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
sponsoring
</a>{" "}
to help host the live demo site!
</p>
<p className="text-gray-700 mt-2">
For support or inquiries, please open an issue on the{" "}
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
GitHub repository
</a>{" "}
or contact: me[at]jiang.jp
</p>
{/* CTA */}
<div className="mt-12 text-center">
<Link
href="/"
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
Open Editor
</Link>
</div>
</article>
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<p className="text-center text-gray-600 text-sm">
Next AI Draw.io - Open Source AI-Powered Diagram
Generator
</p>
</div>
</footer>
</div>
)
}

172
app/[lang]/layout.tsx Normal file
View File

@@ -0,0 +1,172 @@
import { GoogleAnalytics } from "@next/third-parties/google"
import type { Metadata, Viewport } from "next"
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google"
import { notFound } from "next/navigation"
import { DiagramProvider } from "@/contexts/diagram-context"
import { DictionaryProvider } from "@/hooks/use-dictionary"
import type { Locale } from "@/lib/i18n/config"
import { i18n } from "@/lib/i18n/config"
import { getDictionary, hasLocale } from "@/lib/i18n/dictionaries"
import "../globals.css"
const plusJakarta = Plus_Jakarta_Sans({
variable: "--font-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
})
const jetbrainsMono = JetBrains_Mono({
variable: "--font-mono",
subsets: ["latin"],
weight: ["400", "500"],
})
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
}
// Generate static params for all locales
export async function generateStaticParams() {
return i18n.locales.map((locale) => ({ lang: locale }))
}
// Generate metadata per locale
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>
}): Promise<Metadata> {
const { lang: rawLang } = await params
const lang = (rawLang in { en: 1, zh: 1, ja: 1 } ? rawLang : "en") as Locale
// Default to English metadata
const titles: Record<Locale, string> = {
en: "Next AI Draw.io - AI-Powered Diagram Generator",
zh: "Next AI Draw.io - AI powered diagram generator",
ja: "Next AI Draw.io - AI-powered diagram generator",
}
const descriptions: Record<Locale, string> = {
en: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
zh: "Use AI to create AWS architecture diagrams, flowcharts, and technical diagrams. Free online tool integrated with draw.io and AI assistance for professional diagram creation.",
ja: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Create professional diagrams with a free online tool that integrates draw.io with an AI assistant.",
}
return {
title: titles[lang],
description: descriptions[lang],
keywords: [
"AI diagram generator",
"AWS architecture",
"flowchart creator",
"draw.io",
"AI drawing tool",
"technical diagrams",
"diagram automation",
"free diagram generator",
"online diagram maker",
],
authors: [{ name: "Next AI Draw.io" }],
creator: "Next AI Draw.io",
publisher: "Next AI Draw.io",
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
openGraph: {
title: titles[lang],
description: descriptions[lang],
type: "website",
url: "https://next-ai-drawio.jiang.jp",
siteName: "Next AI Draw.io",
locale: lang === "zh" ? "zh_CN" : lang === "ja" ? "ja_JP" : "en_US",
images: [
{
url: "/architecture.png",
width: 1200,
height: 630,
alt: "Next AI Draw.io - AI-powered diagram creation tool",
},
],
},
twitter: {
card: "summary_large_image",
title: titles[lang],
description: descriptions[lang],
images: ["/architecture.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: {
icon: "/favicon.ico",
},
alternates: {
languages: {
en: "/en",
zh: "/zh",
ja: "/ja",
},
},
}
}
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode
params: Promise<{ lang: string }>
}>) {
const { lang } = await params
if (!hasLocale(lang)) notFound()
const validLang = lang as Locale
const dictionary = await getDictionary(validLang)
const jsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "Next AI Draw.io",
applicationCategory: "DesignApplication",
operatingSystem: "Web Browser",
description:
"AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.",
url: "https://next-ai-drawio.jiang.jp",
inLanguage: validLang,
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
}
return (
<html lang={validLang} suppressHydrationWarning>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
>
<DictionaryProvider dictionary={dictionary}>
<DiagramProvider>{children}</DiagramProvider>
</DictionaryProvider>
</body>
{process.env.NEXT_PUBLIC_GA_ID && (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
)}
</html>
)
}

268
app/[lang]/page.tsx Normal file
View File

@@ -0,0 +1,268 @@
"use client"
import { usePathname, useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { DrawIoEmbed } from "react-drawio"
import type { ImperativePanelHandle } from "react-resizable-panels"
import ChatPanel from "@/components/chat-panel"
import { STORAGE_CLOSE_PROTECTION_KEY } from "@/components/settings-dialog"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context"
import { i18n, type Locale } from "@/lib/i18n/config"
const drawioBaseUrl =
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
export default function Home() {
const {
drawioRef,
handleDiagramExport,
onDrawioLoad,
resetDrawioReady,
saveDiagramToStorage,
showSaveDialog,
setShowSaveDialog,
} = useDiagram()
const router = useRouter()
const pathname = usePathname()
// Extract current language from pathname (e.g., "/zh/about" → "zh")
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
const [isMobile, setIsMobile] = useState(false)
const [isChatVisible, setIsChatVisible] = useState(true)
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
const [darkMode, setDarkMode] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
const [closeProtection, setCloseProtection] = useState(false)
const chatPanelRef = useRef<ImperativePanelHandle>(null)
const isSavingRef = useRef(false)
const mouseOverDrawioRef = useRef(false)
const isMobileRef = useRef(false)
// Reset saving flag when dialog closes (with delay to ignore lingering save events from draw.io)
useEffect(() => {
if (!showSaveDialog) {
const timeout = setTimeout(() => {
isSavingRef.current = false
}, 1000)
return () => clearTimeout(timeout)
}
}, [showSaveDialog])
// Handle save from draw.io's built-in save button
// Note: draw.io sends save events for various reasons (focus changes, etc.)
// We use mouse position to determine if the user is interacting with draw.io
const handleDrawioSave = useCallback(() => {
if (!mouseOverDrawioRef.current) return
if (isSavingRef.current) return
isSavingRef.current = true
setShowSaveDialog(true)
}, [setShowSaveDialog])
// Load preferences from localStorage after mount
useEffect(() => {
// Restore saved locale and redirect if needed
const savedLocale = localStorage.getItem("next-ai-draw-io-locale")
if (savedLocale && i18n.locales.includes(savedLocale as Locale)) {
const pathParts = pathname.split("/").filter(Boolean)
const currentLocale = pathParts[0]
if (currentLocale !== savedLocale) {
pathParts[0] = savedLocale
router.replace(`/${pathParts.join("/")}`)
return // Wait for redirect
}
}
const savedUi = localStorage.getItem("drawio-theme")
if (savedUi === "min" || savedUi === "sketch") {
setDrawioUi(savedUi)
}
const savedDarkMode = localStorage.getItem("next-ai-draw-io-dark-mode")
if (savedDarkMode !== null) {
const isDark = savedDarkMode === "true"
setDarkMode(isDark)
document.documentElement.classList.toggle("dark", isDark)
} else {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches
setDarkMode(prefersDark)
document.documentElement.classList.toggle("dark", prefersDark)
}
const savedCloseProtection = localStorage.getItem(
STORAGE_CLOSE_PROTECTION_KEY,
)
if (savedCloseProtection === "true") {
setCloseProtection(true)
}
setIsLoaded(true)
}, [pathname, router])
const handleDarkModeChange = async () => {
await saveDiagramToStorage()
const newValue = !darkMode
setDarkMode(newValue)
localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue))
document.documentElement.classList.toggle("dark", newValue)
resetDrawioReady()
}
const handleDrawioUiChange = async () => {
await saveDiagramToStorage()
const newUi = drawioUi === "min" ? "sketch" : "min"
localStorage.setItem("drawio-theme", newUi)
setDrawioUi(newUi)
resetDrawioReady()
}
// Check mobile - save diagram and reset draw.io before crossing breakpoint
const isInitialRenderRef = useRef(true)
useEffect(() => {
const checkMobile = () => {
const newIsMobile = window.innerWidth < 768
if (
!isInitialRenderRef.current &&
newIsMobile !== isMobileRef.current
) {
saveDiagramToStorage().catch(() => {})
resetDrawioReady()
}
isMobileRef.current = newIsMobile
isInitialRenderRef.current = false
setIsMobile(newIsMobile)
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [saveDiagramToStorage, resetDrawioReady])
const toggleChatPanel = () => {
const panel = chatPanelRef.current
if (panel) {
if (panel.isCollapsed()) {
panel.expand()
setIsChatVisible(true)
} else {
panel.collapse()
setIsChatVisible(false)
}
}
}
// Keyboard shortcut for toggling chat panel
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
event.preventDefault()
toggleChatPanel()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [])
// Show confirmation dialog when user tries to leave the page
useEffect(() => {
if (!closeProtection) return
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault()
return ""
}
window.addEventListener("beforeunload", handleBeforeUnload)
return () =>
window.removeEventListener("beforeunload", handleBeforeUnload)
}, [closeProtection])
return (
<div className="h-screen bg-background relative overflow-hidden">
<ResizablePanelGroup
id="main-panel-group"
direction={isMobile ? "vertical" : "horizontal"}
className="h-full"
>
<ResizablePanel
id="drawio-panel"
defaultSize={isMobile ? 50 : 67}
minSize={20}
>
<div
className={`h-full relative ${
isMobile ? "p-1" : "p-2"
}`}
onMouseEnter={() => {
mouseOverDrawioRef.current = true
}}
onMouseLeave={() => {
mouseOverDrawioRef.current = false
}}
>
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30">
{isLoaded ? (
<DrawIoEmbed
key={`${drawioUi}-${darkMode}-${currentLang}`}
ref={drawioRef}
onExport={handleDiagramExport}
onLoad={onDrawioLoad}
onSave={handleDrawioSave}
baseUrl={drawioBaseUrl}
urlParameters={{
ui: drawioUi,
spin: true,
libraries: false,
saveAndExit: false,
noExitBtn: true,
dark: darkMode,
lang: currentLang,
}}
/>
) : (
<div className="h-full w-full flex items-center justify-center bg-background">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
</div>
)}
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Chat Panel */}
<ResizablePanel
key={isMobile ? "mobile" : "desktop"}
id="chat-panel"
ref={chatPanelRef}
defaultSize={isMobile ? 50 : 33}
minSize={isMobile ? 20 : 15}
maxSize={isMobile ? 80 : 50}
collapsible={!isMobile}
collapsedSize={isMobile ? 0 : 3}
onCollapse={() => setIsChatVisible(false)}
onExpand={() => setIsChatVisible(true)}
>
<div className={`h-full ${isMobile ? "p-1" : "py-2 pr-2"}`}>
<ChatPanel
isVisible={isChatVisible}
onToggleVisibility={toggleChatPanel}
drawioUi={drawioUi}
onToggleDrawioUi={handleDrawioUiChange}
darkMode={darkMode}
onToggleDarkMode={handleDarkModeChange}
isMobile={isMobile}
onCloseProtectionChange={setCloseProtection}
/>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
)
}

View File

@@ -1,205 +0,0 @@
import type { Metadata } from "next";
import Link from "next/link";
import { FaGithub } from "react-icons/fa";
import Image from "next/image";
export const metadata: Metadata = {
title: "关于 - Next AI Draw.io",
description: "AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。",
keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"],
};
export default function AboutCN() {
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
</Link>
<Link href="/about/cn" className="text-blue-600 font-semibold">
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="在GitHub上查看"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
<p className="text-xl text-gray-600 font-medium">
AI驱动的图表创建工具 -
</p>
<div className="flex justify-center gap-4 mt-4 text-sm">
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
<span className="text-gray-400">|</span>
<Link href="/about/cn" className="text-blue-600 font-semibold"></Link>
<span className="text-gray-400">|</span>
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600"></Link>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p className="text-amber-800">
Claude Opus 4.5 Claude Haiku 4.5
</p>
</div>
<p className="text-gray-700">
AI功能的Next.js网页应用draw.io图表无缝结合AI辅助可视化来创建
</p>
{/* Features */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li><strong>LLM驱动的图表创建</strong>draw.io图表</li>
<li><strong></strong>AI自动复制和增强</li>
<li><strong></strong>AI编辑前的图表版本</li>
<li><strong></strong>AI实时对话来完善您的图表</li>
<li><strong>AWS架构图支持</strong>AWS架构图</li>
<li><strong></strong></li>
</ul>
{/* Examples */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<p className="text-gray-700 mb-6"></p>
<div className="space-y-8">
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Transformer连接器</h3>
<p className="text-gray-600 mb-4">
<strong></strong> <strong></strong>Transformer架构图
</p>
<Image src="/animated_connectors.svg" alt="带动画连接器的Transformer架构" width={480} height={360} className="mx-auto" />
</div>
{/* Cloud Architecture Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCP架构图</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使<strong>GCP图标</strong>GCP架构图
</p>
<Image src="/gcp_demo.svg" alt="GCP架构图" width={400} height={300} className="mx-auto" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWS架构图</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使<strong>AWS图标</strong>AWS架构图
</p>
<Image src="/aws_demo.svg" alt="AWS架构图" width={400} height={300} className="mx-auto" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azure架构图</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> 使<strong>Azure图标</strong>Azure架构图
</p>
<Image src="/azure_demo.svg" alt="Azure架构图" width={400} height={300} className="mx-auto" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2"></h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>
</p>
<Image src="/cat_demo.svg" alt="猫咪绘图" width={240} height={240} className="mx-auto" />
</div>
</div>
</div>
{/* How It Works */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<p className="text-gray-700 mb-4">使</p>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li><strong>Next.js</strong></li>
<li><strong>Vercel AI SDK</strong><code>ai</code> + <code>@ai-sdk/*</code>用于流式AI响应和多提供商支持</li>
<li><strong>react-drawio</strong>:用于图表表示和操作</li>
</ul>
<p className="text-gray-700 mt-4">
图表以XML格式表示可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
</p>
{/* Multi-Provider Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<ul className="list-disc pl-6 text-gray-700 space-y-1">
<li>AWS Bedrock</li>
<li>OpenAI / OpenAI兼容API <code>OPENAI_BASE_URL</code></li>
<li>Anthropic</li>
<li>Google AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
</ul>
<p className="text-gray-700 mt-4">
<code>claude-sonnet-4-5</code> AWS标志的draw.io图表上进行训练AWS架构图
</p>
{/* Support */}
<div className="flex items-center gap-4 mt-10 mb-4">
<h2 className="text-2xl font-semibold text-gray-900"></h2>
<iframe
src="https://github.com/sponsors/DayuanJiang/button"
title="Sponsor DayuanJiang"
height="32"
width="114"
style={{ border: 0, borderRadius: 6 }}
/>
</div>
<p className="text-gray-700">
{" "}
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
</a>{" "}
线
</p>
<p className="text-gray-700 mt-2">
{" "}
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
GitHub仓库
</a>{" "}
issue或联系me[at]jiang.jp
</p>
{/* CTA */}
<div className="mt-12 text-center">
<Link
href="/"
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
</Link>
</div>
</article>
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<p className="text-center text-gray-600 text-sm">
Next AI Draw.io - AI驱动的图表生成器
</p>
</div>
</footer>
</div>
);
}

View File

@@ -1,205 +0,0 @@
import type { Metadata } from "next";
import Link from "next/link";
import { FaGithub } from "react-icons/fa";
import Image from "next/image";
export const metadata: Metadata = {
title: "概要 - Next AI Draw.io",
description: "AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。",
keywords: ["AIダイアグラム", "draw.io", "AWSアーキテクチャ", "GCPダイアグラム", "Azureダイアグラム", "LLM"],
};
export default function AboutJA() {
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<header className="bg-white border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link href="/" className="text-xl font-bold text-gray-900 hover:text-gray-700">
Next AI Draw.io
</Link>
<nav className="flex items-center gap-6 text-sm">
<Link href="/" className="text-gray-600 hover:text-gray-900 transition-colors">
</Link>
<Link href="/about/ja" className="text-blue-600 font-semibold">
</Link>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 transition-colors"
aria-label="GitHubで見る"
>
<FaGithub className="w-5 h-5" />
</a>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article className="prose prose-lg max-w-none">
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">Next AI Draw.io</h1>
<p className="text-xl text-gray-600 font-medium">
AI搭載のダイアグラム作成ツール -
</p>
<div className="flex justify-center gap-4 mt-4 text-sm">
<Link href="/about" className="text-gray-600 hover:text-blue-600">English</Link>
<span className="text-gray-400">|</span>
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600"></Link>
<span className="text-gray-400">|</span>
<Link href="/about/ja" className="text-blue-600 font-semibold"></Link>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p className="text-amber-800">
Claude Opus 4.5 Claude Haiku 4.5
</p>
</div>
<p className="text-gray-700">
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションですAI支援の可視化により
</p>
{/* Features */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li><strong>LLM搭載のダイアグラム作成</strong>draw.ioダイアグラムを作成</li>
<li><strong></strong>AIが自動的に複製</li>
<li><strong></strong>AI編集前のダイアグラムの以前のバージョンを表示</li>
<li><strong></strong>AIとリアルタイムでコミュニケーションしてダイアグラムを改善</li>
<li><strong>AWSアーキテクチャダイアグラムサポート</strong>AWSアーキテクチャダイアグラムの生成を専門的にサポート</li>
<li><strong></strong></li>
</ul>
{/* Examples */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<p className="text-gray-700 mb-6"></p>
<div className="space-y-8">
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Transformerコネクタ</h3>
<p className="text-gray-600 mb-4">
<strong></strong> <strong></strong>Transformerアーキテクチャ図を作成してください
</p>
<Image src="/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width={480} height={360} className="mx-auto" />
</div>
{/* Cloud Architecture Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">GCPアーキテクチャ図</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> <strong>GCPアイコン</strong>使GCPアーキテクチャ図を生成してください
</p>
<Image src="/gcp_demo.svg" alt="GCPアーキテクチャ図" width={400} height={300} className="mx-auto" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">AWSアーキテクチャ図</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> <strong>AWSアイコン</strong>使AWSアーキテクチャ図を生成してください
</p>
<Image src="/aws_demo.svg" alt="AWSアーキテクチャ図" width={400} height={300} className="mx-auto" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Azureアーキテクチャ図</h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong> <strong>Azureアイコン</strong>使Azureアーキテクチャ図を生成してください
</p>
<Image src="/azure_demo.svg" alt="Azureアーキテクチャ図" width={400} height={300} className="mx-auto" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2"></h3>
<p className="text-gray-600 text-sm mb-4">
<strong></strong>
</p>
<Image src="/cat_demo.svg" alt="猫の絵" width={240} height={240} className="mx-auto" />
</div>
</div>
</div>
{/* How It Works */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<p className="text-gray-700 mb-4">使</p>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li><strong>Next.js</strong></li>
<li><strong>Vercel AI SDK</strong><code>ai</code> + <code>@ai-sdk/*</code>ストリーミングAIレスポンスとマルチプロバイダーサポート</li>
<li><strong>react-drawio</strong>:ダイアグラムの表現と操作</li>
</ul>
<p className="text-gray-700 mt-4">
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
</p>
{/* Multi-Provider Support */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4"></h2>
<ul className="list-disc pl-6 text-gray-700 space-y-1">
<li>AWS Bedrock</li>
<li>OpenAI / OpenAI互換API<code>OPENAI_BASE_URL</code></li>
<li>Anthropic</li>
<li>Google AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
</ul>
<p className="text-gray-700 mt-4">
<code>claude-sonnet-4-5</code>AWSロゴ付きのdraw.ioダイアグラムで学習されているためAWSアーキテクチャダイアグラムを作成したい場合は最適な選択です
</p>
{/* Support */}
<div className="flex items-center gap-4 mt-10 mb-4">
<h2 className="text-2xl font-semibold text-gray-900"></h2>
<iframe
src="https://github.com/sponsors/DayuanJiang/button"
title="Sponsor DayuanJiang"
height="32"
width="114"
style={{ border: 0, borderRadius: 6 }}
/>
</div>
<p className="text-gray-700">
{" "}
<a href="https://github.com/sponsors/DayuanJiang" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
</a>{" "}
</p>
<p className="text-gray-700 mt-2">
{" "}
<a href="https://github.com/DayuanJiang/next-ai-draw-io" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
GitHubリポジトリ
</a>{" "}
issueを開くかme[at]jiang.jp
</p>
{/* CTA */}
<div className="mt-12 text-center">
<Link
href="/"
className="inline-block bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
</Link>
</div>
</article>
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<p className="text-center text-gray-600 text-sm">
Next AI Draw.io - AI搭載ダイアグラムジェネレーター
</p>
</div>
</footer>
</div>
);
}

View File

@@ -1,206 +0,0 @@
import type { Metadata } from "next";
import Link from "next/link";
import { FaGithub } from "react-icons/fa";
import Image from "next/image";
export const metadata: Metadata = {
title: "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 className="flex justify-center gap-4 mt-4 text-sm">
<Link href="/about" className="text-blue-600 font-semibold">English</Link>
<span className="text-gray-400">|</span>
<Link href="/about/cn" className="text-gray-600 hover:text-blue-600"></Link>
<span className="text-gray-400">|</span>
<Link href="/about/ja" className="text-gray-600 hover:text-blue-600"></Link>
</div>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p className="text-amber-800">
This app is designed to run on Claude Opus 4.5 for best performance. However, due to higher-than-expected traffic, running the top-tier model has become cost-prohibitive. To avoid service interruptions and manage costs, I have switched the backend to Claude Haiku 4.5.
</p>
</div>
<p className="text-gray-700">
A Next.js web application that integrates AI capabilities with draw.io diagrams.
Create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.
</p>
{/* Features */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Features</h2>
<ul className="list-disc pl-6 text-gray-700 space-y-2">
<li><strong>LLM-Powered Diagram Creation</strong>: Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands</li>
<li><strong>Image-Based Diagram Replication</strong>: Upload existing diagrams or images and have the AI replicate and enhance them automatically</li>
<li><strong>Diagram History</strong>: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing</li>
<li><strong>Interactive Chat Interface</strong>: Communicate with AI to refine your diagrams in real-time</li>
<li><strong>AWS Architecture Diagram Support</strong>: Specialized support for generating AWS architecture diagrams</li>
<li><strong>Animated Connectors</strong>: Create dynamic and animated connectors between diagram elements for better visualization</li>
</ul>
{/* Examples */}
<h2 className="text-2xl font-semibold text-gray-900 mt-10 mb-4">Examples</h2>
<p className="text-gray-700 mb-6">Here are some example prompts and their generated diagrams:</p>
<div className="space-y-8">
{/* Animated Transformer */}
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Animated Transformer Connectors</h3>
<p className="text-gray-600 mb-4">
<strong>Prompt:</strong> Give me an <strong>animated connector</strong> diagram of transformer&apos;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>AWS Bedrock (default)</li>
<li>OpenAI / OpenAI-compatible APIs (via <code>OPENAI_BASE_URL</code>)</li>
<li>Anthropic</li>
<li>Google AI</li>
<li>Azure OpenAI</li>
<li>Ollama</li>
<li>OpenRouter</li>
<li>DeepSeek</li>
</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 */}
<div className="flex items-center gap-4 mt-10 mb-4">
<h2 className="text-2xl font-semibold text-gray-900">Support &amp; Contact</h2>
<iframe
src="https://github.com/sponsors/DayuanJiang/button"
title="Sponsor DayuanJiang"
height="32"
width="114"
style={{ border: 0, borderRadius: 6 }}
/>
</div>
<p className="text-gray-700">
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 diff suppressed because it is too large Load Diff

View File

@@ -81,16 +81,15 @@ Contains the actual diagram data.
## Root Cell Container: `<root>` ## Root Cell Container: `<root>`
Contains all the cells in the diagram. Contains all the cells in the diagram. **Note:** When generating diagrams, you only need to provide the mxCell elements - the root container and root cells (id="0", id="1") are added automatically.
**Example:** **Internal structure (auto-generated):**
```xml ```xml
<root> <root>
<mxCell id="0"/> <mxCell id="0"/> <!-- Auto-added -->
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/> <!-- Auto-added -->
<!-- Your mxCell elements go here (start from id="2") -->
<!-- Other cells go here -->
</root> </root>
``` ```
@@ -203,15 +202,15 @@ Draw.io files contain two special cells that are always present:
1. **Root Cell** (id = "0"): The parent of all cells 1. **Root Cell** (id = "0"): The parent of all cells
2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells 2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells
## Tips for Manually Creating Draw.io XML ## Tips for Creating Draw.io XML
1. Start with the basic structure (`mxfile`, `diagram`, `mxGraphModel`, `root`) 1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically
2. Always include the two special cells (id = "0" and id = "1") 2. Start IDs from "2" (id="0" and id="1" are reserved for root cells)
3. Assign unique and sequential IDs to all cells 3. Assign unique and sequential IDs to all cells
4. Define parent relationships correctly 4. Define parent relationships correctly (use parent="1" for top-level shapes)
5. Use `mxGeometry` elements to position shapes 5. Use `mxGeometry` elements to position shapes
6. For connectors, specify `source` and `target` attributes 6. For connectors, specify `source` and `target` attributes
7. **CRITICAL: All mxCell elements must be DIRECT children of `<root>`. NEVER nest mxCell inside another mxCell.** 7. **CRITICAL: All mxCell elements must be siblings. NEVER nest mxCell inside another mxCell.**
## Common Patterns ## Common Patterns

10
app/api/config/route.ts Normal file
View File

@@ -0,0 +1,10 @@
import { NextResponse } from "next/server"
export async function GET() {
return NextResponse.json({
accessCodeRequired: !!process.env.ACCESS_CODE_LIST,
dailyRequestLimit: Number(process.env.DAILY_REQUEST_LIMIT) || 0,
dailyTokenLimit: Number(process.env.DAILY_TOKEN_LIMIT) || 0,
tpmLimit: Number(process.env.TPM_LIMIT) || 0,
})
}

View File

@@ -0,0 +1,117 @@
import { randomUUID } from "crypto"
import { z } from "zod"
import { getLangfuseClient } from "@/lib/langfuse"
import { getUserIdFromRequest } from "@/lib/user-id"
const feedbackSchema = z.object({
messageId: z.string().min(1).max(200),
feedback: z.enum(["good", "bad"]),
sessionId: z.string().min(1).max(200).optional(),
})
export async function POST(req: Request) {
const langfuse = getLangfuseClient()
if (!langfuse) {
return Response.json({ success: true, logged: false })
}
// Validate input
let data
try {
data = feedbackSchema.parse(await req.json())
} catch {
return Response.json(
{ success: false, error: "Invalid input" },
{ status: 400 },
)
}
const { messageId, feedback, sessionId } = data
// Skip logging if no sessionId - prevents attaching to wrong user's trace
if (!sessionId) {
return Response.json({ success: true, logged: false })
}
// Get user ID for tracking
const userId = getUserIdFromRequest(req)
try {
// Find the most recent chat trace for this session to attach the score to
const tracesResponse = await langfuse.api.trace.list({
sessionId,
limit: 1,
})
const traces = tracesResponse.data || []
const latestTrace = traces[0]
if (!latestTrace) {
// No trace found for this session - create a standalone feedback trace
const traceId = randomUUID()
const timestamp = new Date().toISOString()
await langfuse.api.ingestion.batch({
batch: [
{
type: "trace-create",
id: randomUUID(),
timestamp,
body: {
id: traceId,
name: "user-feedback",
sessionId,
userId,
input: { messageId, feedback },
metadata: {
source: "feedback-button",
note: "standalone - no chat trace found",
},
timestamp,
},
},
{
type: "score-create",
id: randomUUID(),
timestamp,
body: {
id: randomUUID(),
traceId,
name: "user-feedback",
value: feedback === "good" ? 1 : 0,
comment: `User gave ${feedback} feedback`,
},
},
],
})
} else {
// Attach score to the existing chat trace
const timestamp = new Date().toISOString()
await langfuse.api.ingestion.batch({
batch: [
{
type: "score-create",
id: randomUUID(),
timestamp,
body: {
id: randomUUID(),
traceId: latestTrace.id,
name: "user-feedback",
value: feedback === "good" ? 1 : 0,
comment: `User gave ${feedback} feedback`,
},
},
],
})
}
return Response.json({ success: true, logged: true })
} catch (error) {
console.error("Langfuse feedback error:", error)
return Response.json(
{ success: false, error: "Failed to log feedback" },
{ status: 500 },
)
}
}

76
app/api/log-save/route.ts Normal file
View File

@@ -0,0 +1,76 @@
import { randomUUID } from "crypto"
import { z } from "zod"
import { getLangfuseClient } from "@/lib/langfuse"
const saveSchema = z.object({
filename: z.string().min(1).max(255),
format: z.enum(["drawio", "png", "svg"]),
sessionId: z.string().min(1).max(200).optional(),
})
export async function POST(req: Request) {
const langfuse = getLangfuseClient()
if (!langfuse) {
return Response.json({ success: true, logged: false })
}
// Validate input
let data
try {
data = saveSchema.parse(await req.json())
} catch {
return Response.json(
{ success: false, error: "Invalid input" },
{ status: 400 },
)
}
const { filename, format, sessionId } = data
// Skip logging if no sessionId - prevents attaching to wrong user's trace
if (!sessionId) {
return Response.json({ success: true, logged: false })
}
try {
const timestamp = new Date().toISOString()
// Find the most recent chat trace for this session to attach the save flag
const tracesResponse = await langfuse.api.trace.list({
sessionId,
limit: 1,
})
const traces = tracesResponse.data || []
const latestTrace = traces[0]
if (latestTrace) {
// Add a score to the existing trace to flag that user saved
await langfuse.api.ingestion.batch({
batch: [
{
type: "score-create",
id: randomUUID(),
timestamp,
body: {
id: randomUUID(),
traceId: latestTrace.id,
name: "diagram-saved",
value: 1,
comment: `User saved diagram as ${filename}.${format}`,
},
},
],
})
}
// If no trace found, skip logging (user hasn't chatted yet)
return Response.json({ success: true, logged: !!latestTrace })
} catch (error) {
console.error("Langfuse save error:", error)
return Response.json(
{ success: false, error: "Failed to log save" },
{ status: 500 },
)
}
}

View File

@@ -0,0 +1,317 @@
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
import { createGateway } from "@ai-sdk/gateway"
import { createGoogleGenerativeAI } from "@ai-sdk/google"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { generateText } from "ai"
import { NextResponse } from "next/server"
import { createOllama } from "ollama-ai-provider-v2"
export const runtime = "nodejs"
/**
* SECURITY: Check if URL points to private/internal network (SSRF protection)
* Blocks: localhost, private IPs, link-local, AWS metadata service
*/
function isPrivateUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
const hostname = url.hostname.toLowerCase()
// Block localhost
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1"
) {
return true
}
// Block AWS/cloud metadata endpoints
if (
hostname === "169.254.169.254" ||
hostname === "metadata.google.internal"
) {
return true
}
// Check for private IPv4 ranges
const ipv4Match = hostname.match(
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
)
if (ipv4Match) {
const [, a, b] = ipv4Match.map(Number)
// 10.0.0.0/8
if (a === 10) return true
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true
// 192.168.0.0/16
if (a === 192 && b === 168) return true
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254) return true
// 127.0.0.0/8 (loopback)
if (a === 127) return true
}
// Block common internal hostnames
if (
hostname.endsWith(".local") ||
hostname.endsWith(".internal") ||
hostname.endsWith(".localhost")
) {
return true
}
return false
} catch {
// Invalid URL - block it
return true
}
}
interface ValidateRequest {
provider: string
apiKey: string
baseUrl?: string
modelId: string
// AWS Bedrock specific
awsAccessKeyId?: string
awsSecretAccessKey?: string
awsRegion?: string
}
export async function POST(req: Request) {
try {
const body: ValidateRequest = await req.json()
const {
provider,
apiKey,
baseUrl,
modelId,
awsAccessKeyId,
awsSecretAccessKey,
awsRegion,
} = body
if (!provider || !modelId) {
return NextResponse.json(
{ valid: false, error: "Provider and model ID are required" },
{ status: 400 },
)
}
// SECURITY: Block SSRF attacks via custom baseUrl
if (baseUrl && isPrivateUrl(baseUrl)) {
return NextResponse.json(
{ valid: false, error: "Invalid base URL" },
{ status: 400 },
)
}
// Validate credentials based on provider
if (provider === "bedrock") {
if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {
return NextResponse.json(
{
valid: false,
error: "AWS credentials (Access Key ID, Secret Access Key, Region) are required",
},
{ status: 400 },
)
}
} else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) {
return NextResponse.json(
{ valid: false, error: "API key is required" },
{ status: 400 },
)
}
let model: any
switch (provider) {
case "openai": {
const openai = createOpenAI({
apiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = openai.chat(modelId)
break
}
case "anthropic": {
const anthropic = createAnthropic({
apiKey,
baseURL: baseUrl || "https://api.anthropic.com/v1",
})
model = anthropic(modelId)
break
}
case "google": {
const google = createGoogleGenerativeAI({
apiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = google(modelId)
break
}
case "azure": {
const azure = createOpenAI({
apiKey,
baseURL: baseUrl,
})
model = azure.chat(modelId)
break
}
case "bedrock": {
const bedrock = createAmazonBedrock({
accessKeyId: awsAccessKeyId,
secretAccessKey: awsSecretAccessKey,
region: awsRegion,
})
model = bedrock(modelId)
break
}
case "openrouter": {
const openrouter = createOpenRouter({
apiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = openrouter(modelId)
break
}
case "deepseek": {
if (baseUrl || apiKey) {
const ds = createDeepSeek({
apiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = ds(modelId)
} else {
model = deepseek(modelId)
}
break
}
case "siliconflow": {
const sf = createOpenAI({
apiKey,
baseURL: baseUrl || "https://api.siliconflow.com/v1",
})
model = sf.chat(modelId)
break
}
case "ollama": {
const ollama = createOllama({
baseURL: baseUrl || "http://localhost:11434",
})
model = ollama(modelId)
break
}
case "gateway": {
const gw = createGateway({
apiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = gw(modelId)
break
}
case "edgeone": {
// EdgeOne uses OpenAI-compatible API via Edge Functions
// Need to pass cookies for EdgeOne Pages authentication
const cookieHeader = req.headers.get("cookie") || ""
const edgeone = createOpenAI({
apiKey: "edgeone", // EdgeOne doesn't require API key
baseURL: baseUrl || "/api/edgeai",
headers: {
cookie: cookieHeader,
},
})
model = edgeone.chat(modelId)
break
}
case "sglang": {
// SGLang is OpenAI-compatible
const sglang = createOpenAI({
apiKey: apiKey || "not-needed",
baseURL: baseUrl || "http://127.0.0.1:8000/v1",
})
model = sglang.chat(modelId)
break
}
case "doubao": {
// ByteDance Doubao uses DeepSeek-compatible API
const doubao = createDeepSeek({
apiKey,
baseURL:
baseUrl || "https://ark.cn-beijing.volces.com/api/v3",
})
model = doubao(modelId)
break
}
default:
return NextResponse.json(
{ valid: false, error: `Unknown provider: ${provider}` },
{ status: 400 },
)
}
// Make a minimal test request
const startTime = Date.now()
await generateText({
model,
prompt: "Say 'OK'",
maxOutputTokens: 20,
})
const responseTime = Date.now() - startTime
return NextResponse.json({
valid: true,
responseTime,
})
} catch (error) {
console.error("[validate-model] Error:", error)
let errorMessage = "Validation failed"
if (error instanceof Error) {
// Extract meaningful error message
if (
error.message.includes("401") ||
error.message.includes("Unauthorized")
) {
errorMessage = "Invalid API key"
} else if (
error.message.includes("404") ||
error.message.includes("not found")
) {
errorMessage = "Model not found"
} else if (
error.message.includes("429") ||
error.message.includes("rate limit")
) {
errorMessage = "Rate limited - try again later"
} else if (error.message.includes("ECONNREFUSED")) {
errorMessage = "Cannot connect to server"
} else {
errorMessage = error.message.slice(0, 100)
}
}
return NextResponse.json(
{ valid: false, error: errorMessage },
{ status: 200 }, // Return 200 so client can read error message
)
}
}

View File

@@ -0,0 +1,32 @@
export async function POST(req: Request) {
const accessCodes =
process.env.ACCESS_CODE_LIST?.split(",")
.map((code) => code.trim())
.filter(Boolean) || []
// If no access codes configured, verification always passes
if (accessCodes.length === 0) {
return Response.json({
valid: true,
message: "No access code required",
})
}
const accessCodeHeader = req.headers.get("x-access-code")
if (!accessCodeHeader) {
return Response.json(
{ valid: false, message: "Access code is required" },
{ status: 401 },
)
}
if (!accessCodes.includes(accessCodeHeader)) {
return Response.json(
{ valid: false, message: "Invalid access code" },
{ status: 401 },
)
}
return Response.json({ valid: true, message: "Access code is valid" })
}

View File

@@ -1,248 +1,401 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "tailwindcss-animate"; @plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-sans); --font-sans: var(--font-sans);
--font-mono: var(--font-mono); --font-mono: var(--font-mono);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar); --color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2); --color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1); --color-chart-1: var(--chart-1);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-input: var(--input); --color-input: var(--input);
--color-border: var(--border); --color-border: var(--border);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent); --color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-card: var(--card); --color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
} }
:root { :root {
--radius: 0.75rem; --radius: 0.75rem;
/* Clean Light Modern Palette */ /* Clean Light Modern Palette */
--background: oklch(0.985 0.002 240); --background: oklch(0.985 0.002 240);
--foreground: oklch(0.23 0.02 260); --foreground: oklch(0.23 0.02 260);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.23 0.02 260); --card-foreground: oklch(0.23 0.02 260);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.23 0.02 260); --popover-foreground: oklch(0.23 0.02 260);
/* Dark primary - slightly lighter */ /* Dark primary - slightly lighter */
--primary: oklch(0.35 0.01 260); --primary: oklch(0.35 0.01 260);
--primary-foreground: oklch(0.99 0 0); --primary-foreground: oklch(0.99 0 0);
/* Warm gray secondary */ /* Warm gray secondary */
--secondary: oklch(0.96 0.005 260); --secondary: oklch(0.96 0.005 260);
--secondary-foreground: oklch(0.35 0.02 260); --secondary-foreground: oklch(0.35 0.02 260);
/* Light muted tones */ /* Light muted tones */
--muted: oklch(0.965 0.005 260); --muted: oklch(0.965 0.005 260);
--muted-foreground: oklch(0.50 0.02 260); --muted-foreground: oklch(0.5 0.02 260);
/* Soft lavender accent */ /* Soft lavender accent */
--accent: oklch(0.94 0.03 280); --accent: oklch(0.94 0.03 280);
--accent-foreground: oklch(0.35 0.08 270); --accent-foreground: oklch(0.35 0.08 270);
/* Coral destructive */ /* Coral destructive */
--destructive: oklch(0.60 0.20 25); --destructive: oklch(0.6 0.2 25);
/* Subtle borders */ /* Subtle borders */
--border: oklch(0.92 0.01 260); --border: oklch(0.92 0.01 260);
--input: oklch(0.94 0.01 260); --input: oklch(0.94 0.01 260);
--ring: oklch(0.25 0.01 260); --ring: oklch(0.25 0.01 260);
/* Chart colors - harmonious palette */ /* Chart colors - harmonious palette */
--chart-1: oklch(0.55 0.18 265); --chart-1: oklch(0.55 0.18 265);
--chart-2: oklch(0.65 0.15 170); --chart-2: oklch(0.65 0.15 170);
--chart-3: oklch(0.70 0.18 45); --chart-3: oklch(0.7 0.18 45);
--chart-4: oklch(0.60 0.20 330); --chart-4: oklch(0.6 0.2 330);
--chart-5: oklch(0.50 0.15 200); --chart-5: oklch(0.5 0.15 200);
/* Sidebar */ /* Sidebar */
--sidebar: oklch(0.99 0.002 260); --sidebar: oklch(0.99 0.002 260);
--sidebar-foreground: oklch(0.23 0.02 260); --sidebar-foreground: oklch(0.23 0.02 260);
--sidebar-primary: oklch(0.55 0.18 265); --sidebar-primary: oklch(0.55 0.18 265);
--sidebar-primary-foreground: oklch(0.99 0 0); --sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.96 0.02 270); --sidebar-accent: oklch(0.96 0.02 270);
--sidebar-accent-foreground: oklch(0.35 0.05 265); --sidebar-accent-foreground: oklch(0.35 0.05 265);
--sidebar-border: oklch(0.93 0.01 260); --sidebar-border: oklch(0.93 0.01 260);
--sidebar-ring: oklch(0.55 0.18 265); --sidebar-ring: oklch(0.55 0.18 265);
} }
.dark { .dark {
--background: oklch(0.15 0.015 260); --background: oklch(0.15 0.015 260);
--foreground: oklch(0.95 0.01 260); --foreground: oklch(0.95 0.01 260);
--card: oklch(0.20 0.015 260); --card: oklch(0.2 0.015 260);
--card-foreground: oklch(0.95 0.01 260); --card-foreground: oklch(0.95 0.01 260);
--popover: oklch(0.20 0.015 260); --popover: oklch(0.2 0.015 260);
--popover-foreground: oklch(0.95 0.01 260); --popover-foreground: oklch(0.95 0.01 260);
--primary: oklch(0.70 0.16 265); --primary: oklch(0.7 0.16 265);
--primary-foreground: oklch(0.15 0.02 260); --primary-foreground: oklch(0.15 0.02 260);
--secondary: oklch(0.25 0.015 260); --secondary: oklch(0.25 0.015 260);
--secondary-foreground: oklch(0.90 0.01 260); --secondary-foreground: oklch(0.9 0.01 260);
--muted: oklch(0.25 0.015 260); --muted: oklch(0.25 0.015 260);
--muted-foreground: oklch(0.65 0.02 260); --muted-foreground: oklch(0.65 0.02 260);
--accent: oklch(0.30 0.04 280); --accent: oklch(0.3 0.04 280);
--accent-foreground: oklch(0.90 0.03 270); --accent-foreground: oklch(0.9 0.03 270);
--destructive: oklch(0.65 0.22 25); --destructive: oklch(0.65 0.22 25);
--border: oklch(0.28 0.015 260); --border: oklch(0.28 0.015 260);
--input: oklch(0.25 0.015 260); --input: oklch(0.25 0.015 260);
--ring: oklch(0.70 0.16 265); --ring: oklch(0.7 0.16 265);
--chart-1: oklch(0.70 0.16 265); --chart-1: oklch(0.7 0.16 265);
--chart-2: oklch(0.70 0.13 170); --chart-2: oklch(0.7 0.13 170);
--chart-3: oklch(0.75 0.16 45); --chart-3: oklch(0.75 0.16 45);
--chart-4: oklch(0.70 0.18 330); --chart-4: oklch(0.7 0.18 330);
--chart-5: oklch(0.60 0.13 200); --chart-5: oklch(0.6 0.13 200);
--sidebar: oklch(0.18 0.015 260); --sidebar: oklch(0.18 0.015 260);
--sidebar-foreground: oklch(0.95 0.01 260); --sidebar-foreground: oklch(0.95 0.01 260);
--sidebar-primary: oklch(0.70 0.16 265); --sidebar-primary: oklch(0.7 0.16 265);
--sidebar-primary-foreground: oklch(0.15 0.02 260); --sidebar-primary-foreground: oklch(0.15 0.02 260);
--sidebar-accent: oklch(0.25 0.03 270); --sidebar-accent: oklch(0.25 0.03 270);
--sidebar-accent-foreground: oklch(0.90 0.02 265); --sidebar-accent-foreground: oklch(0.9 0.02 265);
--sidebar-border: oklch(0.28 0.015 260); --sidebar-border: oklch(0.28 0.015 260);
--sidebar-ring: oklch(0.70 0.16 265); --sidebar-ring: oklch(0.7 0.16 265);
}
/* ============================================
REFINED MINIMAL DESIGN SYSTEM
============================================ */
:root {
/* Surface layers for depth */
--surface-0: oklch(1 0 0);
--surface-1: oklch(0.985 0.002 240);
--surface-2: oklch(0.97 0.004 240);
--surface-elevated: oklch(1 0 0);
/* Subtle borders */
--border-subtle: oklch(0.94 0.008 260);
--border-default: oklch(0.91 0.012 260);
/* Interactive states */
--interactive-hover: oklch(0.96 0.015 260);
--interactive-active: oklch(0.93 0.02 265);
/* Success state */
--success: oklch(0.65 0.18 145);
--success-muted: oklch(0.95 0.03 145);
/* Animation timing */
--duration-fast: 120ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dark {
--surface-0: oklch(0.15 0.015 260);
--surface-1: oklch(0.18 0.015 260);
--surface-2: oklch(0.22 0.015 260);
--surface-elevated: oklch(0.25 0.015 260);
--border-subtle: oklch(0.25 0.012 260);
--border-default: oklch(0.3 0.015 260);
--interactive-hover: oklch(0.25 0.02 265);
--interactive-active: oklch(0.3 0.025 270);
--success: oklch(0.7 0.16 145);
--success-muted: oklch(0.25 0.04 145);
}
/* Expose surface colors to Tailwind */
@theme inline {
--color-surface-0: var(--surface-0);
--color-surface-1: var(--surface-1);
--color-surface-2: var(--surface-2);
--color-surface-elevated: var(--surface-elevated);
--color-border-subtle: var(--border-subtle);
--color-border-default: var(--border-default);
--color-interactive-hover: var(--interactive-hover);
--color-interactive-active: var(--interactive-active);
--color-success: var(--success);
--color-success-muted: var(--success-muted);
} }
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground font-sans; @apply bg-background text-foreground font-sans;
} }
}
/* Fix for Radix ScrollArea viewport horizontal overflow */
[data-slot="scroll-area-viewport"] > div {
display: block !important;
width: 100% !important;
} }
/* Custom scrollbar */ /* Custom scrollbar */
@layer utilities { @layer utilities {
.scrollbar-thin { .scrollbar-thin {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: oklch(0.85 0.01 260) transparent; scrollbar-color: oklch(0.85 0.01 260) transparent;
} }
.scrollbar-thin::-webkit-scrollbar { .scrollbar-thin::-webkit-scrollbar {
width: 6px; width: 6px;
} }
.scrollbar-thin::-webkit-scrollbar-track { .scrollbar-thin::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.scrollbar-thin::-webkit-scrollbar-thumb { .scrollbar-thin::-webkit-scrollbar-thumb {
background-color: oklch(0.85 0.01 260); background-color: oklch(0.85 0.01 260);
border-radius: 3px; border-radius: 3px;
} }
.scrollbar-thin::-webkit-scrollbar-thumb:hover { .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: oklch(0.75 0.01 260); background-color: oklch(0.75 0.01 260);
} }
} }
/* Smooth page transitions */ /* Smooth page transitions */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(8px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes slideInRight { @keyframes slideInRight {
from { from {
opacity: 0; opacity: 0;
transform: translateX(16px); transform: translateX(16px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
} }
} }
.animate-fade-in { .animate-fade-in {
animation: fadeIn 0.3s ease-out forwards; animation: fadeIn 0.3s ease-out forwards;
} }
.animate-slide-in-right { .animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards; animation: slideInRight 0.3s ease-out forwards;
} }
/* Message bubble animations */ /* Message bubble animations */
@keyframes messageIn { @keyframes messageIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(12px) scale(0.98); transform: translateY(12px) scale(0.98);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
} }
} }
.animate-message-in { .animate-message-in {
animation: messageIn 0.25s ease-out forwards; animation: messageIn 0.25s ease-out forwards;
} }
/* Subtle floating shadow for cards */ /* Subtle floating shadow for cards */
.shadow-soft { .shadow-soft {
box-shadow: box-shadow:
0 1px 2px oklch(0.23 0.02 260 / 0.04), 0 1px 2px oklch(0.23 0.02 260 / 0.04),
0 4px 12px oklch(0.23 0.02 260 / 0.06), 0 4px 12px oklch(0.23 0.02 260 / 0.06),
0 8px 24px oklch(0.23 0.02 260 / 0.04); 0 8px 24px oklch(0.23 0.02 260 / 0.04);
} }
.shadow-soft-lg { .shadow-soft-lg {
box-shadow: box-shadow:
0 2px 4px oklch(0.23 0.02 260 / 0.04), 0 2px 4px oklch(0.23 0.02 260 / 0.04),
0 8px 20px oklch(0.23 0.02 260 / 0.08), 0 8px 20px oklch(0.23 0.02 260 / 0.08),
0 16px 40px oklch(0.23 0.02 260 / 0.06); 0 16px 40px oklch(0.23 0.02 260 / 0.06);
} }
/* Gradient text utility */ /* Gradient text utility */
.text-gradient-primary { .text-gradient-primary {
background: linear-gradient(135deg, oklch(0.55 0.18 265), oklch(0.60 0.20 290)); background: linear-gradient(
-webkit-background-clip: text; 135deg,
-webkit-text-fill-color: transparent; oklch(0.55 0.18 265),
background-clip: text; oklch(0.6 0.2 290)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ============================================
REFINED DIALOG STYLES
============================================ */
/* Refined dialog shadow - multi-layer soft shadow */
.shadow-dialog {
box-shadow:
0 0 0 1px oklch(0 0 0 / 0.03),
0 2px 4px oklch(0 0 0 / 0.02),
0 12px 24px oklch(0 0 0 / 0.06),
0 24px 48px oklch(0 0 0 / 0.04);
}
.dark .shadow-dialog {
box-shadow:
0 0 0 1px oklch(1 0 0 / 0.05),
0 2px 4px oklch(0 0 0 / 0.2),
0 12px 24px oklch(0 0 0 / 0.3),
0 24px 48px oklch(0 0 0 / 0.2);
}
/* Dialog animations */
@keyframes dialog-in {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes dialog-out {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
}
.animate-dialog-in {
animation: dialog-in var(--duration-normal) var(--ease-out) forwards;
}
.animate-dialog-out {
animation: dialog-out 150ms var(--ease-out) forwards;
}
/* Check pop animation for validation success */
@keyframes check-pop {
0% {
transform: scale(0.8);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.animate-check-pop {
animation: check-pop 0.25s var(--ease-spring) forwards;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.animate-dialog-in,
.animate-dialog-out,
.animate-check-pop {
animation: none;
}
} }

View File

@@ -1,107 +0,0 @@
import type { Metadata } from "next";
import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/react";
import { GoogleAnalytics } from "@next/third-parties/google";
import { DiagramProvider } from "@/contexts/diagram-context";
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 metadata: Metadata = {
title: "Next AI Draw.io - AI-Powered Diagram Generator",
description: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
keywords: ["AI diagram generator", "AWS architecture", "flowchart creator", "draw.io", "AI drawing tool", "technical diagrams", "diagram automation", "free diagram generator", "online diagram maker"],
authors: [{ name: "Next AI Draw.io" }],
creator: "Next AI Draw.io",
publisher: "Next AI Draw.io",
metadataBase: new URL("https://next-ai-drawio.jiang.jp"),
openGraph: {
title: "Next AI Draw.io - AI Diagram Generator",
description: "Create professional diagrams with AI assistance. Supports AWS architecture, flowcharts, and more.",
type: "website",
url: "https://next-ai-drawio.jiang.jp",
siteName: "Next AI Draw.io",
locale: "en_US",
images: [
{
url: "/architecture.png",
width: 1200,
height: 630,
alt: "Next AI Draw.io - AI-powered diagram creation tool",
},
],
},
twitter: {
card: "summary_large_image",
title: "Next AI Draw.io - AI Diagram Generator",
description: "Create professional diagrams with AI assistance. Free, no login required.",
images: ["/architecture.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: {
icon: "/favicon.ico",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Next AI Draw.io',
applicationCategory: 'DesignApplication',
operatingSystem: 'Web Browser',
description: 'AI-powered diagram generator with targeted XML editing capabilities that integrates with draw.io for creating AWS architecture diagrams, flowcharts, and technical diagrams. Features diagram history, multi-provider AI support, and real-time collaboration.',
url: 'https://next-ai-drawio.jiang.jp',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
};
return (
<html lang="en">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body
className={`${plusJakarta.variable} ${jetbrainsMono.variable} antialiased`}
>
<DiagramProvider>{children}</DiagramProvider>
<Analytics />
</body>
{process.env.NEXT_PUBLIC_GA_ID && (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
)}
</html>
);
}

28
app/manifest.ts Normal file
View File

@@ -0,0 +1,28 @@
import type { MetadataRoute } from "next"
import { getAssetUrl } from "@/lib/base-path"
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Next AI Draw.io",
short_name: "AIDraw.io",
description:
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
start_url: getAssetUrl("/"),
display: "standalone",
background_color: "#f9fafb",
theme_color: "#171d26",
icons: [
{
src: getAssetUrl("/favicon-192x192.png"),
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: getAssetUrl("/favicon-512x512.png"),
sizes: "512x512",
type: "image/png",
purpose: "any",
},
],
}
}

View File

@@ -1,85 +0,0 @@
"use client";
import React, { useState, useEffect } from "react";
import { DrawIoEmbed } from "react-drawio";
import ChatPanel from "@/components/chat-panel";
import { useDiagram } from "@/contexts/diagram-context";
import { Monitor } from "lucide-react";
export default function Home() {
const { drawioRef, handleDiagramExport } = useDiagram();
const [isMobile, setIsMobile] = useState(false);
const [isChatVisible, setIsChatVisible] = useState(true);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
event.preventDefault();
setIsChatVisible((prev) => !prev);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<div className="flex h-screen bg-background relative overflow-hidden">
{/* Mobile warning overlay */}
{isMobile && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background">
<div className="text-center p-8 max-w-sm mx-auto animate-fade-in">
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-6">
<Monitor className="w-8 h-8 text-primary" />
</div>
<h1 className="text-xl font-semibold text-foreground mb-3">
Desktop Required
</h1>
<p className="text-sm text-muted-foreground leading-relaxed">
This application works best on desktop or laptop devices. Please open it on a larger screen for the full experience.
</p>
</div>
</div>
)}
{/* Draw.io Canvas */}
<div
className={`${isChatVisible ? 'w-2/3' : 'w-full'} h-full relative transition-all duration-300 ease-out`}
>
<div className="absolute inset-2 rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
<DrawIoEmbed
ref={drawioRef}
onExport={handleDiagramExport}
urlParameters={{
spin: true,
libraries: false,
saveAndExit: false,
noExitBtn: true,
}}
/>
</div>
</div>
{/* Chat Panel */}
<div
className={`${isChatVisible ? 'w-1/3' : 'w-12'} h-full transition-all duration-300 ease-out`}
>
<div className="h-full py-2 pr-2">
<ChatPanel
isVisible={isChatVisible}
onToggleVisibility={() => setIsChatVisible(!isChatVisible)}
/>
</div>
</div>
</div>
);
}

View File

@@ -1,12 +1,12 @@
import { MetadataRoute } from 'next' import type { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
return { return {
rules: { rules: {
userAgent: '*', userAgent: "*",
allow: '/', allow: "/",
disallow: '/api/', disallow: "/api/",
}, },
sitemap: 'https://next-ai-drawio.jiang.jp/sitemap.xml', sitemap: "https://next-ai-drawio.jiang.jp/sitemap.xml",
} }
} }

View File

@@ -1,17 +1,17 @@
import { MetadataRoute } from 'next' import type { MetadataRoute } from "next"
export default function sitemap(): MetadataRoute.Sitemap { export default function sitemap(): MetadataRoute.Sitemap {
return [ return [
{ {
url: 'https://next-ai-drawio.jiang.jp', url: "https://next-ai-drawio.jiang.jp",
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'weekly', changeFrequency: "weekly",
priority: 1, priority: 1,
}, },
{ {
url: 'https://next-ai-drawio.jiang.jp/about', url: "https://next-ai-drawio.jiang.jp/about",
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 0.8, priority: 0.8,
}, },
] ]

83
biome.json Normal file
View File

@@ -0,0 +1,83 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noImportantStyles": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noArrayIndexKey": "off",
"noImplicitAnyLet": "off",
"noAssignInExpressions": "off"
},
"a11y": {
"useButtonType": "off",
"noAutofocus": "off",
"noStaticElementInteractions": "off",
"useKeyWithClickEvents": "off",
"noLabelWithoutControl": "off",
"noNoninteractiveTabindex": "off"
},
"correctness": {
"useExhaustiveDependencies": "off"
},
"style": {
"useNodejsImportProtocol": "off",
"useTemplate": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded"
}
},
"css": {
"parser": {
"cssModules": true,
"tailwindDirectives": true
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"overrides": [
{
"includes": ["components/ui/**"],
"formatter": {
"enabled": false
},
"linter": {
"enabled": false
},
"assist": {
"enabled": false
}
}
]
}

View File

@@ -1,21 +1,21 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "style": "new-york",
"rsc": true, "rsc": true,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "", "config": "",
"css": "app/globals.css", "css": "app/globals.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "hooks": "@/hooks"
}, },
"iconLibrary": "lucide" "iconLibrary": "lucide"
} }

View File

@@ -0,0 +1,170 @@
import { Cloud } from "lucide-react"
import type { ComponentProps, ReactNode } from "react"
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command"
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
export type ModelSelectorProps = ComponentProps<typeof Dialog>
export const ModelSelector = (props: ModelSelectorProps) => (
<Dialog {...props} />
)
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
<DialogTrigger {...props} />
)
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
title?: ReactNode
}
export const ModelSelectorContent = ({
className,
children,
title = "Model Selector",
...props
}: ModelSelectorContentProps) => (
<DialogContent className={cn("p-0", className)} {...props}>
<DialogTitle className="sr-only">{title}</DialogTitle>
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
{children}
</Command>
</DialogContent>
)
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
<CommandDialog {...props} />
)
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>
export const ModelSelectorInput = ({
className,
...props
}: ModelSelectorInputProps) => (
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
)
export type ModelSelectorListProps = ComponentProps<typeof CommandList>
export const ModelSelectorList = ({
className,
...props
}: ModelSelectorListProps) => (
<div className="relative">
<CommandList
className={cn(
// Hide scrollbar on all platforms
"[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]",
className,
)}
{...props}
/>
{/* Bottom shadow indicator for scrollable content */}
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-muted/80 via-muted/40 to-transparent" />
</div>
)
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
<CommandEmpty {...props} />
)
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
<CommandGroup {...props} />
)
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
<CommandItem {...props} />
)
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
<CommandShortcut {...props} />
)
export type ModelSelectorSeparatorProps = ComponentProps<
typeof CommandSeparator
>
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
<CommandSeparator {...props} />
)
export type ModelSelectorLogoProps = Omit<
ComponentProps<"img">,
"src" | "alt"
> & {
provider: string
}
export const ModelSelectorLogo = ({
provider,
className,
...props
}: ModelSelectorLogoProps) => {
// Use Lucide icon for bedrock since models.dev doesn't have a good AWS icon
if (provider === "amazon-bedrock") {
return <Cloud className={cn("size-4", className)} />
}
return (
<img
{...props}
alt={`${provider} logo`}
className={cn("size-4 dark:invert", className)}
height={16}
src={`https://models.dev/logos/${provider}.svg`}
width={16}
/>
)
}
export type ModelSelectorLogoGroupProps = ComponentProps<"div">
export const ModelSelectorLogoGroup = ({
className,
...props
}: ModelSelectorLogoGroupProps) => (
<div
className={cn(
"-space-x-1 flex shrink-0 items-center [&>img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
className,
)}
{...props}
/>
)
export type ModelSelectorNameProps = ComponentProps<"span">
export const ModelSelectorName = ({
className,
...props
}: ModelSelectorNameProps) => (
<span className={cn("flex-1 truncate text-left", className)} {...props} />
)

View File

@@ -0,0 +1,186 @@
"use client"
import { useControllableState } from "@radix-ui/react-use-controllable-state"
import { BrainIcon, ChevronDownIcon } from "lucide-react"
import type { ComponentProps, ReactNode } from "react"
import { createContext, memo, useContext, useEffect, useState } from "react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { cn } from "@/lib/utils"
import { Shimmer } from "./shimmer"
type ReasoningContextValue = {
isStreaming: boolean
isOpen: boolean
setIsOpen: (open: boolean) => void
duration: number | undefined
}
const ReasoningContext = createContext<ReasoningContextValue | null>(null)
export const useReasoning = () => {
const context = useContext(ReasoningContext)
if (!context) {
throw new Error("Reasoning components must be used within Reasoning")
}
return context
}
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
isStreaming?: boolean
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
duration?: number
}
const AUTO_CLOSE_DELAY = 1000
const MS_IN_S = 1000
export const Reasoning = memo(
({
className,
isStreaming = false,
open,
defaultOpen = true,
onOpenChange,
duration: durationProp,
children,
...props
}: ReasoningProps) => {
const [isOpen, setIsOpen] = useControllableState({
prop: open,
defaultProp: defaultOpen,
onChange: onOpenChange,
})
const [duration, setDuration] = useControllableState({
prop: durationProp,
defaultProp: undefined,
})
const [hasAutoClosed, setHasAutoClosed] = useState(false)
const [startTime, setStartTime] = useState<number | null>(null)
// Track duration when streaming starts and ends
useEffect(() => {
if (isStreaming) {
if (startTime === null) {
setStartTime(Date.now())
}
} else if (startTime !== null) {
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
setStartTime(null)
}
}, [isStreaming, startTime, setDuration])
// Auto-open when streaming starts, auto-close when streaming ends (once only)
useEffect(() => {
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
// Add a small delay before closing to allow user to see the content
const timer = setTimeout(() => {
setIsOpen(false)
setHasAutoClosed(true)
}, AUTO_CLOSE_DELAY)
return () => clearTimeout(timer)
}
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed])
const handleOpenChange = (newOpen: boolean) => {
setIsOpen(newOpen)
}
return (
<ReasoningContext.Provider
value={{ isStreaming, isOpen, setIsOpen, duration }}
>
<Collapsible
className={cn("not-prose mb-4", className)}
onOpenChange={handleOpenChange}
open={isOpen}
{...props}
>
{children}
</Collapsible>
</ReasoningContext.Provider>
)
},
)
export type ReasoningTriggerProps = ComponentProps<
typeof CollapsibleTrigger
> & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
}
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
if (isStreaming || duration === 0) {
return <Shimmer duration={1}>Thinking...</Shimmer>
}
if (duration === undefined) {
return <p>Thought for a few seconds</p>
}
return <p>Thought for {duration} seconds</p>
}
export const ReasoningTrigger = memo(
({
className,
children,
getThinkingMessage = defaultGetThinkingMessage,
...props
}: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning()
return (
<CollapsibleTrigger
className={cn(
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
className,
)}
{...props}
>
{children ?? (
<>
<BrainIcon className="size-4" />
{getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
)}
/>
</>
)}
</CollapsibleTrigger>
)
},
)
export type ReasoningContentProps = ComponentProps<
typeof CollapsibleContent
> & {
children: string
}
export const ReasoningContent = memo(
({ className, children, ...props }: ReasoningContentProps) => (
<CollapsibleContent
className={cn(
"mt-4 text-sm",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
>
<div className="whitespace-pre-wrap">{children}</div>
</CollapsibleContent>
),
)
Reasoning.displayName = "Reasoning"
ReasoningTrigger.displayName = "ReasoningTrigger"
ReasoningContent.displayName = "ReasoningContent"

View File

@@ -0,0 +1,64 @@
"use client"
import { motion } from "motion/react"
import {
type CSSProperties,
type ElementType,
type JSX,
memo,
useMemo,
} from "react"
import { cn } from "@/lib/utils"
export type TextShimmerProps = {
children: string
as?: ElementType
className?: string
duration?: number
spread?: number
}
const ShimmerComponent = ({
children,
as: Component = "p",
className,
duration = 2,
spread = 2,
}: TextShimmerProps) => {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements,
)
const dynamicSpread = useMemo(
() => (children?.length ?? 0) * spread,
[children, spread],
)
return (
<MotionComponent
animate={{ backgroundPosition: "0% center" }}
className={cn(
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
className,
)}
initial={{ backgroundPosition: "100% center" }}
style={
{
"--spread": `${dynamicSpread}px`,
backgroundImage:
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
} as CSSProperties
}
transition={{
repeat: Number.POSITIVE_INFINITY,
duration,
ease: "linear",
}}
>
{children}
</MotionComponent>
)
}
export const Shimmer = memo(ShimmerComponent)

View File

@@ -1,19 +1,19 @@
import React from "react"; import type { VariantProps } from "class-variance-authority"
import { Button, buttonVariants } from "@/components/ui/button"; import type React from "react"
import { Button, type buttonVariants } from "@/components/ui/button"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip"
import { type VariantProps } from "class-variance-authority";
interface ButtonWithTooltipProps interface ButtonWithTooltipProps
extends React.ComponentProps<"button">, extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
tooltipContent: string; tooltipContent: string
children: React.ReactNode; children: React.ReactNode
asChild?: boolean; asChild?: boolean
} }
export function ButtonWithTooltip({ export function ButtonWithTooltip({
@@ -27,8 +27,10 @@ export function ButtonWithTooltip({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button {...buttonProps}>{children}</Button> <Button {...buttonProps}>{children}</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{tooltipContent}</TooltipContent> <TooltipContent className="max-w-xs text-wrap">
{tooltipContent}
</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
); )
} }

View File

@@ -1,124 +1,219 @@
"use client"; "use client"
import { Zap, Cloud, GitBranch, Palette } from "lucide-react"; import {
Cloud,
FileText,
GitBranch,
Palette,
Terminal,
Zap,
} from "lucide-react"
import { useDictionary } from "@/hooks/use-dictionary"
import { getAssetUrl } from "@/lib/base-path"
interface ExampleCardProps { interface ExampleCardProps {
icon: React.ReactNode; icon: React.ReactNode
title: string; title: string
description: string; description: string
onClick: () => void; onClick: () => void
isNew?: boolean
} }
function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) { function ExampleCard({
icon,
title,
description,
onClick,
isNew,
}: ExampleCardProps) {
const dict = useDictionary()
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="group w-full text-left p-4 rounded-xl border border-border/60 bg-card hover:bg-accent/50 hover:border-primary/30 transition-all duration-200 hover:shadow-sm" className={`group w-full text-left p-4 rounded-xl border bg-card hover:bg-accent/50 hover:border-primary/30 transition-all duration-200 hover:shadow-sm ${
isNew
? "border-primary/40 ring-1 ring-primary/20"
: "border-border/60"
}`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary/15 transition-colors"> <div
className={`w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors ${
isNew
? "bg-primary/20 group-hover:bg-primary/25"
: "bg-primary/10 group-hover:bg-primary/15"
}`}
>
{icon} {icon}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-medium text-foreground group-hover:text-primary transition-colors"> <div className="flex items-center gap-2">
{title} <h3 className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
</h3> {title}
</h3>
{isNew && (
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-primary text-primary-foreground rounded">
{dict.common.new}
</span>
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2"> <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
{description} {description}
</p> </p>
</div> </div>
</div> </div>
</button> </button>
); )
} }
export default function ExamplePanel({ export default function ExamplePanel({
setInput, setInput,
setFiles, setFiles,
}: { }: {
setInput: (input: string) => void; setInput: (input: string) => void
setFiles: (files: File[]) => void; setFiles: (files: File[]) => void
}) { }) {
const dict = useDictionary()
const handleReplicateFlowchart = async () => { const handleReplicateFlowchart = async () => {
setInput("Replicate this flowchart."); setInput("Replicate this flowchart.")
try { try {
const response = await fetch("/example.png"); const response = await fetch(getAssetUrl("/example.png"))
const blob = await response.blob(); const blob = await response.blob()
const file = new File([blob], "example.png", { type: "image/png" }); const file = new File([blob], "example.png", { type: "image/png" })
setFiles([file]); setFiles([file])
} catch (error) { } catch (error) {
console.error("Error loading example image:", error); console.error(dict.errors.failedToLoadExample, error)
} }
}; }
const handleReplicateArchitecture = async () => { const handleReplicateArchitecture = async () => {
setInput("Replicate this in aws style"); setInput("Replicate this in aws style")
try { try {
const response = await fetch("/architecture.png"); const response = await fetch(getAssetUrl("/architecture.png"))
const blob = await response.blob(); const blob = await response.blob()
const file = new File([blob], "architecture.png", { const file = new File([blob], "architecture.png", {
type: "image/png", type: "image/png",
}); })
setFiles([file]); setFiles([file])
} catch (error) { } catch (error) {
console.error("Error loading architecture image:", error); console.error(dict.errors.failedToLoadExample, error)
} }
}; }
const handlePdfExample = async () => {
setInput("Summarize this paper as a diagram")
try {
const response = await fetch(getAssetUrl("/chain-of-thought.txt"))
const blob = await response.blob()
const file = new File([blob], "chain-of-thought.txt", {
type: "text/plain",
})
setFiles([file])
} catch (error) {
console.error(dict.errors.failedToLoadExample, error)
}
}
return ( return (
<div className="py-6 px-2 animate-fade-in"> <div className="py-6 px-2 animate-fade-in">
{/* MCP Server Notice */}
<a
href="https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
target="_blank"
rel="noopener noreferrer"
className="block mb-4 p-3 rounded-xl bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-500/20 hover:border-purple-500/40 transition-colors group"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center shrink-0">
<Terminal className="w-4 h-4 text-purple-500" />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
{dict.examples.mcpServer}
</span>
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
{dict.examples.preview}
</span>
</div>
<p className="text-xs text-muted-foreground">
{dict.examples.mcpDescription}
</p>
</div>
</div>
</a>
{/* Welcome section */} {/* Welcome section */}
<div className="text-center mb-6"> <div className="text-center mb-6">
<h2 className="text-lg font-semibold text-foreground mb-2"> <h2 className="text-lg font-semibold text-foreground mb-2">
Create diagrams with AI {dict.examples.title}
</h2> </h2>
<p className="text-sm text-muted-foreground max-w-xs mx-auto"> <p className="text-sm text-muted-foreground max-w-xs mx-auto">
Describe what you want to create or upload an image to replicate {dict.examples.subtitle}
</p> </p>
</div> </div>
{/* Examples grid */} {/* Examples grid */}
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1"> <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-1">
Quick Examples {dict.examples.quickExamples}
</p> </p>
<div className="grid gap-2"> <div className="grid gap-2">
<ExampleCard
icon={<FileText className="w-4 h-4 text-primary" />}
title={dict.examples.paperToDiagram}
description={dict.examples.paperDescription}
onClick={handlePdfExample}
isNew
/>
<ExampleCard <ExampleCard
icon={<Zap className="w-4 h-4 text-primary" />} icon={<Zap className="w-4 h-4 text-primary" />}
title="Animated Diagram" title={dict.examples.animatedDiagram}
description="Draw a transformer architecture with animated connectors" description={dict.examples.animatedDescription}
onClick={() => setInput("Give me a **animated connector** diagram of transformer's architecture")} onClick={() => {
setInput(
"Give me a **animated connector** diagram of transformer's architecture",
)
setFiles([])
}}
/> />
<ExampleCard <ExampleCard
icon={<Cloud className="w-4 h-4 text-primary" />} icon={<Cloud className="w-4 h-4 text-primary" />}
title="AWS Architecture" title={dict.examples.awsArchitecture}
description="Create a cloud architecture diagram with AWS icons" description={dict.examples.awsDescription}
onClick={handleReplicateArchitecture} onClick={handleReplicateArchitecture}
/> />
<ExampleCard <ExampleCard
icon={<GitBranch className="w-4 h-4 text-primary" />} icon={<GitBranch className="w-4 h-4 text-primary" />}
title="Replicate Flowchart" title={dict.examples.replicateFlowchart}
description="Upload and replicate an existing flowchart" description={dict.examples.replicateDescription}
onClick={handleReplicateFlowchart} onClick={handleReplicateFlowchart}
/> />
<ExampleCard <ExampleCard
icon={<Palette className="w-4 h-4 text-primary" />} icon={<Palette className="w-4 h-4 text-primary" />}
title="Creative Drawing" title={dict.examples.creativeDrawing}
description="Draw something fun and creative" description={dict.examples.creativeDescription}
onClick={() => setInput("Draw a cat for me")} onClick={() => {
setInput("Draw a cat for me")
setFiles([])
}}
/> />
</div> </div>
<p className="text-[11px] text-muted-foreground/60 text-center mt-4"> <p className="text-[11px] text-muted-foreground/60 text-center mt-4">
Examples are cached for instant response {dict.examples.cachedNote}
</p> </p>
</div> </div>
</div> </div>
); )
} }

View File

@@ -1,34 +1,161 @@
"use client"; "use client"
import React, { useCallback, useRef, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { ResetWarningModal } from "@/components/reset-warning-modal";
import { SaveDialog } from "@/components/save-dialog";
import { import {
Download,
History,
Image as ImageIcon,
Loader2, Loader2,
Send, Send,
Trash2, Trash2,
Image as ImageIcon, } from "lucide-react"
History, import type React from "react"
Download, import { useCallback, useEffect, useRef, useState } from "react"
Paperclip, import { toast } from "sonner"
} from "lucide-react"; import { ButtonWithTooltip } from "@/components/button-with-tooltip"
import { ButtonWithTooltip } from "@/components/button-with-tooltip"; import { ErrorToast } from "@/components/error-toast"
import { FilePreviewList } from "./file-preview-list"; import { HistoryDialog } from "@/components/history-dialog"
import { useDiagram } from "@/contexts/diagram-context"; import { ModelSelector } from "@/components/model-selector"
import { HistoryDialog } from "@/components/history-dialog"; import { ResetWarningModal } from "@/components/reset-warning-modal"
import { SaveDialog } from "@/components/save-dialog"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary"
import { formatMessage } from "@/lib/i18n/utils"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import type { FlattenedModel } from "@/lib/types/model-config"
import { FilePreviewList } from "./file-preview-list"
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
const MAX_FILES = 5
function isValidFileType(file: File): boolean {
return file.type.startsWith("image/") || isPdfFile(file) || isTextFile(file)
}
function formatFileSize(bytes: number): string {
const mb = bytes / 1024 / 1024
if (mb < 0.01) return `${(bytes / 1024).toFixed(0)}KB`
return `${mb.toFixed(2)}MB`
}
function showErrorToast(message: React.ReactNode) {
toast.custom(
(t) => (
<ErrorToast message={message} onDismiss={() => toast.dismiss(t)} />
),
{ duration: 5000 },
)
}
interface ValidationResult {
validFiles: File[]
errors: string[]
}
function validateFiles(
newFiles: File[],
existingCount: number,
dict: any,
): ValidationResult {
const errors: string[] = []
const validFiles: File[] = []
const availableSlots = MAX_FILES - existingCount
if (availableSlots <= 0) {
errors.push(formatMessage(dict.errors.maxFiles, { max: MAX_FILES }))
return { validFiles, errors }
}
for (const file of newFiles) {
if (validFiles.length >= availableSlots) {
errors.push(
formatMessage(dict.errors.onlyMoreAllowed, {
slots: availableSlots,
}),
)
break
}
if (!isValidFileType(file)) {
errors.push(
formatMessage(dict.errors.unsupportedType, { name: file.name }),
)
continue
}
// Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter)
const isExtractedFile = isPdfFile(file) || isTextFile(file)
if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) {
const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024
errors.push(
formatMessage(dict.errors.fileExceeds, {
name: file.name,
size: formatFileSize(file.size),
max: maxSizeMB,
}),
)
} else {
validFiles.push(file)
}
}
return { validFiles, errors }
}
function showValidationErrors(errors: string[], dict: any) {
if (errors.length === 0) return
if (errors.length === 1) {
showErrorToast(
<span className="text-muted-foreground">{errors[0]}</span>,
)
} else {
showErrorToast(
<div className="flex flex-col gap-1">
<span className="font-medium">
{formatMessage(dict.errors.filesRejected, {
count: errors.length,
})}
</span>
<ul className="text-muted-foreground text-xs list-disc list-inside">
{errors.slice(0, 3).map((err) => (
<li key={err}>{err}</li>
))}
{errors.length > 3 && (
<li>
{formatMessage(dict.errors.andMore, {
count: errors.length - 3,
})}
</li>
)}
</ul>
</div>,
)
}
}
interface ChatInputProps { interface ChatInputProps {
input: string; input: string
status: "submitted" | "streaming" | "ready" | "error"; status: "submitted" | "streaming" | "ready" | "error"
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void; onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
onClearChat: () => void; onClearChat: () => void
files?: File[]; files?: File[]
onFileChange?: (files: File[]) => void; onFileChange?: (files: File[]) => void
showHistory?: boolean; pdfData?: Map<
onToggleHistory?: (show: boolean) => void; File,
{ text: string; charCount: number; isExtracting: boolean }
>
sessionId?: string
error?: Error | null
// Model selector props
models?: FlattenedModel[]
selectedModelId?: string
onModelSelect?: (modelId: string | undefined) => void
showUnvalidatedModels?: boolean
onConfigureModels?: () => void
} }
export function ChatInput({ export function ChatInput({
@@ -39,126 +166,157 @@ export function ChatInput({
onClearChat, onClearChat,
files = [], files = [],
onFileChange = () => {}, onFileChange = () => {},
showHistory = false, pdfData = new Map(),
onToggleHistory = () => {}, sessionId,
error = null,
models = [],
selectedModelId,
onModelSelect = () => {},
showUnvalidatedModels = false,
onConfigureModels = () => {},
}: ChatInputProps) { }: ChatInputProps) {
const { diagramHistory, saveDiagramToFile } = useDiagram(); const dict = useDictionary()
const textareaRef = useRef<HTMLTextAreaElement>(null); const { diagramHistory, saveDiagramToFile } = useDiagram()
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [showClearDialog, setShowClearDialog] = useState(false);
const [showSaveDialog, setShowSaveDialog] = useState(false);
const isDisabled = status === "streaming" || status === "submitted"; const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
useEffect(() => { const [isDragging, setIsDragging] = useState(false)
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled); const [showClearDialog, setShowClearDialog] = useState(false)
}, [status, isDisabled]); const [showHistory, setShowHistory] = useState(false)
const [showSaveDialog, setShowSaveDialog] = useState(false)
// Allow retry when there's an error (even if status is still "streaming" or "submitted")
const isDisabled =
(status === "streaming" || status === "submitted") && !error
const adjustTextareaHeight = useCallback(() => { const adjustTextareaHeight = useCallback(() => {
const textarea = textareaRef.current; const textarea = textareaRef.current
if (textarea) { if (textarea) {
textarea.style.height = "auto"; textarea.style.height = "auto"
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`
} }
}, []); }, [])
// Handle programmatic input changes (e.g., setInput("") after form submission)
useEffect(() => { useEffect(() => {
adjustTextareaHeight(); adjustTextareaHeight()
}, [input, adjustTextareaHeight]); }, [input, adjustTextareaHeight])
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(e)
adjustTextareaHeight()
}
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault(); e.preventDefault()
const form = e.currentTarget.closest("form"); const form = e.currentTarget.closest("form")
if (form && input.trim() && !isDisabled) { if (form && input.trim() && !isDisabled) {
form.requestSubmit(); form.requestSubmit()
} }
} }
}; }
const handlePaste = async (e: React.ClipboardEvent) => { const handlePaste = async (e: React.ClipboardEvent) => {
if (isDisabled) return; if (isDisabled) return
const items = e.clipboardData.items; const items = e.clipboardData.items
const imageItems = Array.from(items).filter((item) => const imageItems = Array.from(items).filter((item) =>
item.type.startsWith("image/") item.type.startsWith("image/"),
); )
if (imageItems.length > 0) { if (imageItems.length > 0) {
const imageFiles = await Promise.all( const imageFiles = (
imageItems.map(async (item) => { await Promise.all(
const file = item.getAsFile(); imageItems.map(async (item, index) => {
if (!file) return null; const file = item.getAsFile()
return new File( if (!file) return null
[file], return new File(
`pasted-image-${Date.now()}.${file.type.split("/")[1]}`, [file],
{ `pasted-image-${Date.now()}-${index}.${file.type.split("/")[1]}`,
type: file.type, { type: file.type },
} )
); }),
}) )
); ).filter((f): f is File => f !== null)
const validFiles = imageFiles.filter( const { validFiles, errors } = validateFiles(
(file): file is File => file !== null imageFiles,
); files.length,
dict,
)
showValidationErrors(errors, dict)
if (validFiles.length > 0) { if (validFiles.length > 0) {
onFileChange([...files, ...validFiles]); onFileChange([...files, ...validFiles])
} }
} }
}; }
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = Array.from(e.target.files || []); const newFiles = Array.from(e.target.files || [])
onFileChange([...files, ...newFiles]); const { validFiles, errors } = validateFiles(
}; newFiles,
files.length,
dict,
)
showValidationErrors(errors, dict)
if (validFiles.length > 0) {
onFileChange([...files, ...validFiles])
}
if (fileInputRef.current) {
fileInputRef.current.value = ""
}
}
const handleRemoveFile = (fileToRemove: File) => { const handleRemoveFile = (fileToRemove: File) => {
onFileChange(files.filter((file) => file !== fileToRemove)); onFileChange(files.filter((file) => file !== fileToRemove))
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ""; fileInputRef.current.value = ""
} }
}; }
const triggerFileInput = () => { const triggerFileInput = () => {
fileInputRef.current?.click(); fileInputRef.current?.click()
}; }
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => { const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault()
e.stopPropagation(); e.stopPropagation()
setIsDragging(true); setIsDragging(true)
}; }
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => { const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault()
e.stopPropagation(); e.stopPropagation()
setIsDragging(false); setIsDragging(false)
}; }
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => { const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault()
e.stopPropagation(); e.stopPropagation()
setIsDragging(false); setIsDragging(false)
if (isDisabled) return; if (isDisabled) return
const droppedFiles = e.dataTransfer.files; const droppedFiles = e.dataTransfer.files
const supportedFiles = Array.from(droppedFiles).filter((file) =>
isValidFileType(file),
)
const imageFiles = Array.from(droppedFiles).filter((file) => const { validFiles, errors } = validateFiles(
file.type.startsWith("image/") supportedFiles,
); files.length,
dict,
if (imageFiles.length > 0) { )
onFileChange([...files, ...imageFiles]); showValidationErrors(errors, dict)
if (validFiles.length > 0) {
onFileChange([...files, ...validFiles])
} }
}; }
const handleClear = () => { const handleClear = () => {
onClearChat(); onClearChat()
setShowClearDialog(false); setShowClearDialog(false)
}; }
return ( return (
<form <form
@@ -175,34 +333,34 @@ export function ChatInput({
{/* File previews */} {/* File previews */}
{files.length > 0 && ( {files.length > 0 && (
<div className="mb-3"> <div className="mb-3">
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} /> <FilePreviewList
files={files}
onRemoveFile={handleRemoveFile}
pdfData={pdfData}
/>
</div> </div>
)} )}
{/* Input container */}
<div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200"> <div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200">
<Textarea <Textarea
ref={textareaRef} ref={textareaRef}
value={input} value={input}
onChange={onChange} onChange={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste} onPaste={handlePaste}
placeholder="Describe your diagram or paste an image..." placeholder={dict.chat.placeholder}
disabled={isDisabled} disabled={isDisabled}
aria-label="Chat input" aria-label="Chat input"
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60" className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
/> />
{/* Action bar */}
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50"> <div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
{/* Left actions */} <div className="flex items-center gap-1 overflow-x-hidden">
<div className="flex items-center gap-1">
<ButtonWithTooltip <ButtonWithTooltip
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setShowClearDialog(true)} onClick={() => setShowClearDialog(true)}
tooltipContent="Clear conversation" tooltipContent={dict.chat.clearConversation}
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10" className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
@@ -213,90 +371,102 @@ export function ChatInput({
onOpenChange={setShowClearDialog} onOpenChange={setShowClearDialog}
onClear={handleClear} onClear={handleClear}
/> />
<HistoryDialog
showHistory={showHistory}
onToggleHistory={onToggleHistory}
/>
</div> </div>
{/* Right actions */} <div className="flex items-center gap-1 overflow-hidden justify-end">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1 overflow-x-hidden">
<ButtonWithTooltip <ButtonWithTooltip
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onToggleHistory(true)} onClick={() => setShowHistory(true)}
disabled={isDisabled || diagramHistory.length === 0} disabled={
tooltipContent="Diagram history" isDisabled || diagramHistory.length === 0
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" }
> tooltipContent={dict.chat.diagramHistory}
<History className="h-4 w-4" /> className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
</ButtonWithTooltip> >
<History className="h-4 w-4" />
</ButtonWithTooltip>
<ButtonWithTooltip <ButtonWithTooltip
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setShowSaveDialog(true)} onClick={() => setShowSaveDialog(true)}
disabled={isDisabled}
tooltipContent={dict.chat.saveDiagram}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<Download className="h-4 w-4" />
</ButtonWithTooltip>
<ButtonWithTooltip
type="button"
variant="ghost"
size="sm"
onClick={triggerFileInput}
disabled={isDisabled}
tooltipContent={dict.chat.uploadFile}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<ImageIcon className="h-4 w-4" />
</ButtonWithTooltip>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
multiple
disabled={isDisabled}
/>
</div>
<ModelSelector
models={models}
selectedModelId={selectedModelId}
onSelect={onModelSelect}
onConfigure={onConfigureModels}
disabled={isDisabled} disabled={isDisabled}
tooltipContent="Save diagram" showUnvalidatedModels={showUnvalidatedModels}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<Download className="h-4 w-4" />
</ButtonWithTooltip>
<SaveDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={(filename, format) => saveDiagramToFile(filename, format)}
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
/> />
<ButtonWithTooltip
type="button"
variant="ghost"
size="sm"
onClick={triggerFileInput}
disabled={isDisabled}
tooltipContent="Upload image"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<ImageIcon className="h-4 w-4" />
</ButtonWithTooltip>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*"
multiple
disabled={isDisabled}
/>
<div className="w-px h-5 bg-border mx-1" /> <div className="w-px h-5 bg-border mx-1" />
<Button <Button
type="submit" type="submit"
disabled={isDisabled || !input.trim()} disabled={isDisabled || !input.trim()}
size="sm" size="sm"
className="h-8 px-4 rounded-xl font-medium shadow-sm" className="h-8 px-4 rounded-xl font-medium shadow-sm"
aria-label={isDisabled ? "Sending..." : "Send message"} aria-label={
isDisabled ? dict.chat.sending : dict.chat.send
}
> >
{isDisabled ? ( {isDisabled ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
<> <>
<Send className="h-4 w-4 mr-1.5" /> <Send className="h-4 w-4 mr-1.5" />
Send {dict.chat.send}
</> </>
)} )}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<HistoryDialog
showHistory={showHistory}
onToggleHistory={setShowHistory}
/>
<SaveDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={(filename, format) =>
saveDiagramToFile(filename, format, sessionId)
}
defaultFilename={`diagram-${new Date()
.toISOString()
.slice(0, 10)}`}
/>
</form> </form>
); )
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,29 @@
"use client"; "use client"
import { Highlight, themes } from "prism-react-renderer"; import { Highlight, themes } from "prism-react-renderer"
interface CodeBlockProps { interface CodeBlockProps {
code: string; code: string
language?: "xml" | "json"; language?: "xml" | "json"
} }
export function CodeBlock({ code, language = "xml" }: CodeBlockProps) { export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
return ( return (
<div className="overflow-hidden w-full"> <div className="overflow-hidden w-full">
<Highlight theme={themes.github} code={code} language={language}> <Highlight theme={themes.github} code={code} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => ( {({
className: _className,
style,
tokens,
getLineProps,
getTokenProps,
}) => (
<pre <pre
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all" className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all"
style={{ style={{
...style, ...style,
fontFamily: "var(--font-mono), ui-monospace, monospace", fontFamily:
"var(--font-mono), ui-monospace, monospace",
backgroundColor: "transparent", backgroundColor: "transparent",
margin: 0, margin: 0,
padding: 0, padding: 0,
@@ -25,9 +32,16 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
}} }}
> >
{tokens.map((line, i) => ( {tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })} style={{ wordBreak: "break-all" }}> <div
key={i}
{...getLineProps({ line })}
style={{ wordBreak: "break-all" }}
>
{line.map((token, key) => ( {line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} /> <span
key={key}
{...getTokenProps({ token })}
/>
))} ))}
</div> </div>
))} ))}
@@ -35,5 +49,5 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
)} )}
</Highlight> </Highlight>
</div> </div>
); )
} }

View File

@@ -0,0 +1,363 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { useDictionary } from "@/hooks/use-dictionary"
import { wrapWithMxFile } from "@/lib/utils"
// Dev XML presets for streaming simulator
const DEV_XML_PRESETS: Record<string, string> = {
"Simple Box": `<mxCell id="2" value="Hello World" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="120" y="100" width="120" height="60" as="geometry"/>
</mxCell>`,
"Two Boxes with Arrow": `<mxCell id="2" value="Start" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="100" height="50" as="geometry"/>
</mxCell>
<mxCell id="3" value="End" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="300" y="100" width="100" height="50" as="geometry"/>
</mxCell>
<mxCell id="4" value="" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`,
Flowchart: `<mxCell id="2" value="Start" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="160" y="40" width="80" height="40" as="geometry"/>
</mxCell>
<mxCell id="3" value="Process A" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="140" y="120" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="4" value="Decision" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="150" y="220" width="100" height="80" as="geometry"/>
</mxCell>
<mxCell id="5" value="Process B" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="300" y="230" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="6" value="End" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="160" y="340" width="80" height="40" as="geometry"/>
</mxCell>
<mxCell id="7" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="8" style="endArrow=classic;html=1;" edge="1" parent="1" source="3" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="9" value="Yes" style="endArrow=classic;html=1;" edge="1" parent="1" source="4" target="6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="10" value="No" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="4" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`,
"Truncated (Error Test)": `<mxCell id="2" value="This cell is truncated" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="120" y="100" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="Incomplete" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor`,
"HTML Escape + Cell Truncate": `<mxCell id="2" value="<b>Chain-of-Thought Prompting</b><br/><font size='12'>Eliciting Reasoning in Large Language Models</font>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=16;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="720" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="<b>Problem: LLM Reasoning Limitations</b><br/>• Scaling parameters alone insufficient for logical tasks<br/>• Arithmetic, commonsense, symbolic reasoning challenges<br/>• Standard prompting fails on multi-step problems" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="40" y="120" width="340" height="120" as="geometry"/>
</mxCell>
<mxCell id="4" value="<b>Traditional Approaches</b><br/>1. <b>Finetuning:</b> Expensive, task-specific<br/>2. <b>Standard Few-Shot:</b> Input→Output pairs<br/> (No explanation of reasoning)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="420" y="120" width="340" height="120" as="geometry"/>
</mxCell>
<mxCell id="5" value="<b>CoT Methodology</b><br/>• Add reasoning steps to few-shot examples<br/>• Natural language intermediate steps<br/>• No parameter updates needed<br/>• Model learns to generate own thought process" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="40" y="260" width="340" height="100" as="geometry"/>
</mxCell>
<mxCell id="6" value="<b>Example Comparison</b><br/><b>Standard:</b><br/>Q: Roger has 5 balls. He buys 2 cans of 3 balls. How many?<br/>A: 11.<br/><br/><b>CoT:</b><br/>Q: Roger has 5 balls. He buys 2 cans of 3 balls. How many?<br/>A: Roger started with 5 balls. 2 cans of 3 tennis balls each is 6 tennis balls. 5 + 6 = 11. The answer is 11." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="420" y="260" width="340" height="140" as="geometry"/>
</mxCell>
<mxCell id="7" value="<b>Experimental Models</b><br/>• GPT-3 (175B)<br/>• LaMDA (137B)<br/>• PaLM (540B)<br/>• UL2 (20B)<br/>• Codex" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="40" y="380" width="340" height="100" as="geometry"/>
</mxCell>
<mxCell id="8" value="<b>Reasoning Domains Tested</b><br/>1. <b>Arithmetic:</b> GSM8K, SVAMP, ASDiv, AQuA, MAWPS<br/>2. <b>Commonsense:</b> CSQA, StrategyQA, Date Understanding, Sports Understanding<br/>3. <b>Symbolic:</b> Last Letter Concatenation, Coin Flip" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1">
<mxGeometry x="420" y="420" width="340" height="100" as="geometry"/>
</mxCell>
<mxCell id="9" value="<b>Key Results: Arithmetic</b><br/>• PaLM 540B + CoT: <b>56.9%</b> on GSM8K<br/> (vs 17.9% standard)<br/>• Surpassed finetuned GPT-3 (55%)<br/>• With calculator: <b>58.6%</b>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="40" y="500" width="220" height="100" as="geometry"/>
</mxCell>
<mxCell id="10" value="<b>Key Results: Commonsense</b><br/>• StrategyQA: <b>75.6%</b><br/> (vs 69.4% SOTA)<br/>• Sports Understanding: <b>95.4%</b><br/> (vs 84% human)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="280" y="500" width="220" height="100" as="geometry"/>
</mxCell>
<mxCell id="11" value="<b>Key Results: Symbolic</b><br/>• OOD Generalization<br/>• Coin Flip: Trained on 2 flips<br/> Works on 3-4 flips with CoT<br/>• Standard prompting fails" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="540" y="500" width="220" height="100" as="geometry"/>
</mxCell>
<mxCell id="12" value="<b>Emergent Ability of Scale</b><br/>• Small models (&lt;10B): No benefit, often harmful<br/>• Large models (100B+): Reasoning emerges<br/>• CoT gains increase dramatically with scale" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
<mxGeometry x="40" y="620" width="340" height="80" as="geometry"/>
</mxCell>
<mxCell id="13" value="<b>Ablation Studies</b><br/>1. Equation only: Worse than CoT<br/>2. Variable compute (...): No improvement<br/>3. Answer first, then reasoning: Same as baseline<br/>→ Content matters, not just extra tokens" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="420" y="620" width="340" height="80" as="geometry"/>
</mxCell>
<mxCell id="14" value="<b>Error Analysis</b><br/>• Semantic understanding errors<br/>• One-step missing errors<br/>• Calculation errors<br/>• Larger models reduce semantic/missing-step errors" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="40" y="720" width="340" height="80" as="geometry"/>
</mxCell>
<mxCell id="15" value="<b>Conclusion</b><br/>• CoT unlocks reasoning potential<br/>• Simple paradigm: &quot;show your work&quot;<br/>• Emergent capability of large models<br/>• No specialized architecture needed" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="420" y="720" width="340" height="80" as="geometry"/>
</mxCell>
<mxCell id="16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="4" target="6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="5" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="6" target="8">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.25;entryY=0;" edge="1" parent="1" source="7" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="7" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.75;entryY=0;" edge="1" parent="1" source="7" target="11">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="9" target="12">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="10" target="13">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="11" target="14">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="12" target="15">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="13" target="15">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="14" target="15">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`,
}
interface DevXmlSimulatorProps {
setMessages: React.Dispatch<React.SetStateAction<any[]>>
onDisplayChart: (xml: string) => void
onShowQuotaToast?: () => void
}
export function DevXmlSimulator({
setMessages,
onDisplayChart,
onShowQuotaToast,
}: DevXmlSimulatorProps) {
const dict = useDictionary()
const [devXml, setDevXml] = useState("")
const [isSimulating, setIsSimulating] = useState(false)
const [devIntervalMs, setDevIntervalMs] = useState(1)
const [devChunkSize, setDevChunkSize] = useState(10)
const devStopRef = useRef(false)
const devXmlInitializedRef = useRef(false)
// Restore dev XML from localStorage on mount (after hydration)
useEffect(() => {
const saved = localStorage.getItem("dev-xml-simulator")
if (saved) setDevXml(saved)
devXmlInitializedRef.current = true
}, [])
// Save dev XML to localStorage (only after initial load)
useEffect(() => {
if (devXmlInitializedRef.current) {
localStorage.setItem("dev-xml-simulator", devXml)
}
}, [devXml])
const handleDevSimulate = async () => {
if (!devXml.trim() || isSimulating) return
setIsSimulating(true)
devStopRef.current = false
const toolCallId = `dev-sim-${Date.now()}`
const xml = devXml.trim()
// Add user message and initial assistant message with empty XML
const userMsg = {
id: `user-${Date.now()}`,
role: "user" as const,
parts: [
{
type: "text" as const,
text: dict.dev.simulatingMessage,
},
],
}
const assistantMsg = {
id: `assistant-${Date.now()}`,
role: "assistant" as const,
parts: [
{
type: "tool-display_diagram" as const,
toolCallId,
state: "input-streaming" as const,
input: { xml: "" },
},
],
}
setMessages((prev) => [...prev, userMsg, assistantMsg] as any)
// Stream characters progressively
for (let i = 0; i < xml.length; i += devChunkSize) {
if (devStopRef.current) {
setIsSimulating(false)
return
}
const chunk = xml.slice(0, i + devChunkSize)
setMessages((prev) => {
const updated = [...prev]
const lastMsg = updated[updated.length - 1] as any
if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) {
lastMsg.parts[0].input = { xml: chunk }
}
return updated
})
await new Promise((r) => setTimeout(r, devIntervalMs))
}
if (devStopRef.current) {
setIsSimulating(false)
return
}
// Finalize: set state to output-available
setMessages((prev) => {
const updated = [...prev]
const lastMsg = updated[updated.length - 1] as any
if (lastMsg?.role === "assistant" && lastMsg.parts?.[0]) {
lastMsg.parts[0].state = "output-available"
lastMsg.parts[0].output = dict.dev.successMessage
lastMsg.parts[0].input = { xml }
}
return updated
})
// Display the final diagram
const fullXml = wrapWithMxFile(xml)
onDisplayChart(fullXml)
setIsSimulating(false)
}
return (
<div className="border-t border-dashed border-orange-500/50 px-4 py-2 bg-orange-50/50 dark:bg-orange-950/30">
<details>
<summary className="text-xs text-orange-600 dark:text-orange-400 cursor-pointer font-medium">
{dict.dev.title}
</summary>
<div className="mt-2 space-y-2">
<div className="flex items-center gap-2">
<label className="text-xs text-muted-foreground whitespace-nowrap">
{dict.dev.preset}
</label>
<select
onChange={(e) => {
if (e.target.value) {
setDevXml(DEV_XML_PRESETS[e.target.value])
}
}}
className="flex-1 text-xs p-1 border rounded bg-background"
defaultValue=""
>
<option value="" disabled>
{dict.dev.selectPreset}
</option>
{Object.keys(DEV_XML_PRESETS).map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
<button
type="button"
onClick={() => setDevXml("")}
className="px-2 py-1 text-xs text-muted-foreground hover:text-foreground border rounded"
>
{dict.dev.clear}
</button>
</div>
<textarea
value={devXml}
onChange={(e) => setDevXml(e.target.value)}
placeholder={dict.dev.placeholder}
className="w-full h-24 text-xs font-mono p-2 border rounded bg-background"
/>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 flex-1">
<label className="text-xs text-muted-foreground whitespace-nowrap">
{dict.dev.interval}
</label>
<input
type="range"
min="1"
max="200"
step="1"
value={devIntervalMs}
onChange={(e) =>
setDevIntervalMs(Number(e.target.value))
}
className="flex-1 h-1 accent-orange-500"
/>
<span className="text-xs text-muted-foreground w-12">
{devIntervalMs}ms
</span>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-muted-foreground whitespace-nowrap">
{dict.dev.chars}
</label>
<input
type="number"
min="1"
max="100"
value={devChunkSize}
onChange={(e) =>
setDevChunkSize(
Math.max(1, Number(e.target.value)),
)
}
className="w-14 text-xs p-1 border rounded bg-background"
/>
</div>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={handleDevSimulate}
disabled={isSimulating || !devXml.trim()}
className="px-3 py-1 text-xs bg-orange-500 text-white rounded hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSimulating
? dict.dev.streaming
: `${dict.dev.simulate} (${devChunkSize} chars/${devIntervalMs}ms)`}
</button>
{isSimulating && (
<button
type="button"
onClick={() => {
devStopRef.current = true
}}
className="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
>
{dict.dev.stop}
</button>
)}
{onShowQuotaToast && (
<button
type="button"
onClick={onShowQuotaToast}
className="px-3 py-1 text-xs bg-purple-500 text-white rounded hover:bg-purple-600"
>
{dict.dev.testQuotaToast}
</button>
)}
</div>
</div>
</details>
</div>
)
}

View File

@@ -0,0 +1,44 @@
"use client"
import type React from "react"
interface ErrorToastProps {
message: React.ReactNode
onDismiss: () => void
}
export function ErrorToast({ message, onDismiss }: ErrorToastProps) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " " || e.key === "Escape") {
e.preventDefault()
onDismiss()
}
}
return (
<div
role="alert"
aria-live="polite"
tabIndex={0}
onClick={onDismiss}
onKeyDown={handleKeyDown}
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm cursor-pointer hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-destructive/10 flex-shrink-0">
<svg
className="w-4 h-4 text-destructive"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</div>
<span className="text-sm text-foreground">{message}</span>
</div>
)
}

View File

@@ -1,49 +1,140 @@
"use client"; "use client"
import React, { useEffect, useState } from "react"; import { FileCode, FileText, Loader2, X } from "lucide-react"
import Image from "next/image"; import Image from "next/image"
import { X } from "lucide-react"; import { useEffect, useRef, useState } from "react"
import { useDictionary } from "@/hooks/use-dictionary"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
interface FilePreviewListProps { function formatCharCount(count: number): string {
files: File[]; if (count >= 1000) {
onRemoveFile: (fileToRemove: File) => void; return `${(count / 1000).toFixed(1)}k`
}
return String(count)
} }
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) { interface FilePreviewListProps {
const [selectedImage, setSelectedImage] = useState<string | null>(null); files: File[]
onRemoveFile: (fileToRemove: File) => void
pdfData?: Map<
File,
{ text: string; charCount: number; isExtracting: boolean }
>
}
// Cleanup object URLs on unmount export function FilePreviewList({
files,
onRemoveFile,
pdfData = new Map(),
}: FilePreviewListProps) {
const dict = useDictionary()
const [selectedImage, setSelectedImage] = useState<string | null>(null)
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
const imageUrlsRef = useRef<Map<File, string>>(new Map())
// Create and cleanup object URLs when files change
useEffect(() => { useEffect(() => {
const objectUrls = files const currentUrls = imageUrlsRef.current
.filter((file) => file.type.startsWith("image/")) const newUrls = new Map<File, string>()
.map((file) => URL.createObjectURL(file));
files.forEach((file) => {
if (file.type.startsWith("image/")) {
// Reuse existing URL if file is already tracked
const existingUrl = currentUrls.get(file)
if (existingUrl) {
newUrls.set(file, existingUrl)
} else {
newUrls.set(file, URL.createObjectURL(file))
}
}
})
// Revoke URLs for files that are no longer in the list
currentUrls.forEach((url, file) => {
if (!newUrls.has(file)) {
URL.revokeObjectURL(url)
}
})
imageUrlsRef.current = newUrls
setImageUrls(newUrls)
}, [files])
// Cleanup all URLs on unmount only
useEffect(() => {
return () => { return () => {
objectUrls.forEach(URL.revokeObjectURL); imageUrlsRef.current.forEach((url) => {
}; URL.revokeObjectURL(url)
}, [files]); })
// Clear the ref so StrictMode remount creates fresh URLs
imageUrlsRef.current = new Map()
}
}, [])
// Clear selected image if its URL was revoked
useEffect(() => {
if (
selectedImage &&
!Array.from(imageUrls.values()).includes(selectedImage)
) {
setSelectedImage(null)
}
}, [imageUrls, selectedImage])
if (files.length === 0) return null; if (files.length === 0) return null
return ( return (
<> <>
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md"> <div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
{files.map((file, index) => { {files.map((file, index) => {
const imageUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null; const imageUrl = imageUrls.get(file) || null
const pdfInfo = pdfData.get(file)
return ( return (
<div key={file.name + index} className="relative group"> <div key={file.name + index} className="relative group">
<div <div
className="w-20 h-20 border rounded-md overflow-hidden bg-muted cursor-pointer" className={`w-20 h-20 border rounded-md overflow-hidden bg-muted ${
onClick={() => imageUrl && setSelectedImage(imageUrl)} file.type.startsWith("image/") && imageUrl
? "cursor-pointer"
: ""
}`}
onClick={() =>
file.type.startsWith("image/") &&
imageUrl &&
setSelectedImage(imageUrl)
}
> >
{file.type.startsWith("image/") ? ( {file.type.startsWith("image/") && imageUrl ? (
<Image <Image
src={imageUrl!} src={imageUrl}
alt={file.name} alt={file.name}
width={80} width={80}
height={80} height={80}
className="object-cover w-full h-full" className="object-cover w-full h-full"
unoptimized
/> />
) : isPdfFile(file) || isTextFile(file) ? (
<div className="flex flex-col items-center justify-center h-full p-1">
{pdfInfo?.isExtracting ? (
<Loader2 className="h-6 w-6 text-blue-500 mb-1 animate-spin" />
) : isPdfFile(file) ? (
<FileText className="h-6 w-6 text-red-500 mb-1" />
) : (
<FileCode className="h-6 w-6 text-blue-500 mb-1" />
)}
<span className="text-xs text-center truncate w-full px-1">
{file.name.length > 10
? `${file.name.slice(0, 7)}...`
: file.name}
</span>
{pdfInfo?.isExtracting ? (
<span className="text-[10px] text-muted-foreground">
{dict.file.reading}
</span>
) : pdfInfo?.charCount ? (
<span className="text-[10px] text-green-600 font-medium">
{formatCharCount(
pdfInfo.charCount,
)}{" "}
{dict.file.chars}
</span>
) : null}
</div>
) : ( ) : (
<div className="flex items-center justify-center h-full text-xs text-center p-1"> <div className="flex items-center justify-center h-full text-xs text-center p-1">
{file.name} {file.name}
@@ -54,15 +145,14 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
type="button" type="button"
onClick={() => onRemoveFile(file)} onClick={() => onRemoveFile(file)}
className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity" className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Remove file" aria-label={dict.file.removeFile}
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</button> </button>
</div> </div>
); )
})} })}
</div> </div>
{/* Image Modal/Lightbox */} {/* Image Modal/Lightbox */}
{selectedImage && ( {selectedImage && (
<div <div
@@ -72,7 +162,7 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
<button <button
className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors" className="absolute top-4 right-4 z-10 bg-white rounded-full p-2 hover:bg-gray-200 transition-colors"
onClick={() => setSelectedImage(null)} onClick={() => setSelectedImage(null)}
aria-label="Close" aria-label={dict.common.close}
> >
<X className="h-6 w-6" /> <X className="h-6 w-6" />
</button> </button>
@@ -84,10 +174,11 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
height={900} height={900}
className="object-contain max-w-full max-h-[90vh] w-auto h-auto" className="object-contain max-w-full max-h-[90vh] w-auto h-auto"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
unoptimized
/> />
</div> </div>
</div> </div>
)} )}
</> </>
); )
} }

View File

@@ -1,6 +1,8 @@
"use client"; "use client"
import { useState } from "react"; import Image from "next/image"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -8,51 +10,50 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"; import { useDiagram } from "@/contexts/diagram-context"
import Image from "next/image"; import { useDictionary } from "@/hooks/use-dictionary"
import { useDiagram } from "@/contexts/diagram-context"; import { formatMessage } from "@/lib/i18n/utils"
interface HistoryDialogProps { interface HistoryDialogProps {
showHistory: boolean; showHistory: boolean
onToggleHistory: (show: boolean) => void; onToggleHistory: (show: boolean) => void
} }
export function HistoryDialog({ export function HistoryDialog({
showHistory, showHistory,
onToggleHistory, onToggleHistory,
}: HistoryDialogProps) { }: HistoryDialogProps) {
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram(); const dict = useDictionary()
const [selectedIndex, setSelectedIndex] = useState<number | null>(null); const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram()
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
const handleClose = () => { const handleClose = () => {
setSelectedIndex(null); setSelectedIndex(null)
onToggleHistory(false); onToggleHistory(false)
}; }
const handleConfirmRestore = () => { const handleConfirmRestore = () => {
if (selectedIndex !== null) { if (selectedIndex !== null) {
onDisplayChart(diagramHistory[selectedIndex].xml); // Skip validation for trusted history snapshots
handleClose(); onDisplayChart(diagramHistory[selectedIndex].xml, true)
handleClose()
} }
}; }
return ( return (
<Dialog open={showHistory} onOpenChange={onToggleHistory}> <Dialog open={showHistory} onOpenChange={onToggleHistory}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto"> <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Diagram History</DialogTitle> <DialogTitle>{dict.history.title}</DialogTitle>
<DialogDescription> <DialogDescription>
Here saved each diagram before AI modification. {dict.history.description}
<br />
Click on a diagram to restore it
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{diagramHistory.length === 0 ? ( {diagramHistory.length === 0 ? (
<div className="text-center p-4 text-gray-500"> <div className="text-center p-4 text-gray-500">
No history available yet. Send messages to create {dict.history.noHistory}
diagram history.
</div> </div>
) : ( ) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 py-4"> <div className="grid grid-cols-2 md:grid-cols-3 gap-4 py-4">
@@ -69,14 +70,14 @@ export function HistoryDialog({
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center"> <div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
<Image <Image
src={item.svg} src={item.svg}
alt={`Diagram version ${index + 1}`} alt={`${dict.history.version} ${index + 1}`}
width={200} width={200}
height={100} height={100}
className="object-contain w-full h-full p-1" className="object-contain w-full h-full p-1"
/> />
</div> </div>
<div className="text-xs text-center mt-1 text-gray-500"> <div className="text-xs text-center mt-1 text-gray-500">
Version {index + 1} {dict.history.version} {index + 1}
</div> </div>
</div> </div>
))} ))}
@@ -87,28 +88,27 @@ export function HistoryDialog({
{selectedIndex !== null ? ( {selectedIndex !== null ? (
<> <>
<div className="flex-1 text-sm text-muted-foreground"> <div className="flex-1 text-sm text-muted-foreground">
Restore to Version {selectedIndex + 1}? {formatMessage(dict.history.restoreTo, {
version: selectedIndex + 1,
})}
</div> </div>
<Button <Button
variant="outline" variant="outline"
onClick={() => setSelectedIndex(null)} onClick={() => setSelectedIndex(null)}
> >
Cancel {dict.common.cancel}
</Button> </Button>
<Button onClick={handleConfirmRestore}> <Button onClick={handleConfirmRestore}>
Confirm {dict.common.confirm}
</Button> </Button>
</> </>
) : ( ) : (
<Button <Button variant="outline" onClick={handleClose}>
variant="outline" {dict.common.close}
onClick={handleClose}
>
Close
</Button> </Button>
)} )}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,303 @@
"use client"
import {
AlertTriangle,
Bot,
Check,
ChevronDown,
Server,
Settings2,
} from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import {
ModelSelectorContent,
ModelSelectorEmpty,
ModelSelectorGroup,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorLogo,
ModelSelectorName,
ModelSelector as ModelSelectorRoot,
ModelSelectorSeparator,
ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector"
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
import { useDictionary } from "@/hooks/use-dictionary"
import type { FlattenedModel } from "@/lib/types/model-config"
import { cn } from "@/lib/utils"
interface ModelSelectorProps {
models: FlattenedModel[]
selectedModelId: string | undefined
onSelect: (modelId: string | undefined) => void
onConfigure: () => void
disabled?: boolean
showUnvalidatedModels?: boolean
}
// Map our provider names to models.dev logo names
const PROVIDER_LOGO_MAP: Record<string, string> = {
openai: "openai",
anthropic: "anthropic",
google: "google",
azure: "azure",
bedrock: "amazon-bedrock",
openrouter: "openrouter",
deepseek: "deepseek",
siliconflow: "siliconflow",
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
gateway: "vercel",
edgeone: "tencent-cloud",
doubao: "bytedance",
}
// Group models by providerLabel (handles duplicate providers)
function groupModelsByProvider(
models: FlattenedModel[],
): Map<string, { provider: string; models: FlattenedModel[] }> {
const groups = new Map<
string,
{ provider: string; models: FlattenedModel[] }
>()
for (const model of models) {
const key = model.providerLabel
const existing = groups.get(key)
if (existing) {
existing.models.push(model)
} else {
groups.set(key, { provider: model.provider, models: [model] })
}
}
return groups
}
export function ModelSelector({
models,
selectedModelId,
onSelect,
onConfigure,
disabled = false,
showUnvalidatedModels = false,
}: ModelSelectorProps) {
const dict = useDictionary()
const [open, setOpen] = useState(false)
// Filter models based on showUnvalidatedModels setting
const displayModels = useMemo(() => {
if (showUnvalidatedModels) {
return models
}
return models.filter((m) => m.validated === true)
}, [models, showUnvalidatedModels])
const groupedModels = useMemo(
() => groupModelsByProvider(displayModels),
[displayModels],
)
// Find selected model for display
const selectedModel = useMemo(
() => models.find((m) => m.id === selectedModelId),
[models, selectedModelId],
)
const handleSelect = (value: string) => {
if (value === "__configure__") {
onConfigure()
} else if (value === "__server_default__") {
onSelect(undefined)
} else {
onSelect(value)
}
setOpen(false)
}
const tooltipContent = selectedModel
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
const wrapperRef = useRef<HTMLDivElement | null>(null)
const [showLabel, setShowLabel] = useState(true)
// Threshold (px) under which we hide the label (tweak as needed)
const HIDE_THRESHOLD = 240
const SHOW_THRESHOLD = 260
useEffect(() => {
const el = wrapperRef.current
if (!el) return
const target = el.parentElement ?? el
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width
setShowLabel((prev) => {
// if currently showing and width dropped below hide threshold -> hide
if (prev && width <= HIDE_THRESHOLD) return false
// if currently hidden and width rose above show threshold -> show
if (!prev && width >= SHOW_THRESHOLD) return true
// otherwise keep previous state (hysteresis)
return prev
})
}
})
ro.observe(target)
const initialWidth = target.getBoundingClientRect().width
setShowLabel(initialWidth >= SHOW_THRESHOLD)
return () => ro.disconnect()
}, [])
return (
<div ref={wrapperRef} className="inline-block">
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
<ModelSelectorTrigger asChild>
<ButtonWithTooltip
tooltipContent={tooltipContent}
variant="ghost"
size="sm"
disabled={disabled}
className={cn(
"hover:bg-accent gap-1.5 h-8 px-2 transition-all duration-150 ease-in-out",
!showLabel && "px-1.5 justify-center",
)}
// accessibility: expose label to screen readers
aria-label={tooltipContent}
>
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
{/* show/hide visible label based on measured width */}
{showLabel ? (
<span className="text-xs truncate">
{selectedModel
? selectedModel.modelId
: dict.modelConfig.default}
</span>
) : (
// Keep an sr-only label for screen readers when hidden
<span className="sr-only">
{selectedModel
? selectedModel.modelId
: dict.modelConfig.default}
</span>
)}
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
</ButtonWithTooltip>
</ModelSelectorTrigger>
<ModelSelectorContent title={dict.modelConfig.selectModel}>
<ModelSelectorInput
placeholder={dict.modelConfig.searchModels}
/>
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
<ModelSelectorEmpty>
{displayModels.length === 0 && models.length > 0
? dict.modelConfig.noVerifiedModels
: dict.modelConfig.noModelsFound}
</ModelSelectorEmpty>
{/* Server Default Option */}
<ModelSelectorGroup heading={dict.modelConfig.default}>
<ModelSelectorItem
value="__server_default__"
onSelect={handleSelect}
className={cn(
"cursor-pointer",
!selectedModelId && "bg-accent",
)}
>
<Check
className={cn(
"mr-2 h-4 w-4",
!selectedModelId
? "opacity-100"
: "opacity-0",
)}
/>
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
<ModelSelectorName>
{dict.modelConfig.serverDefault}
</ModelSelectorName>
</ModelSelectorItem>
</ModelSelectorGroup>
{/* Configured Models by Provider */}
{Array.from(groupedModels.entries()).map(
([
providerLabel,
{ provider, models: providerModels },
]) => (
<ModelSelectorGroup
key={providerLabel}
heading={providerLabel}
>
{providerModels.map((model) => (
<ModelSelectorItem
key={model.id}
value={model.modelId}
onSelect={() =>
handleSelect(model.id)
}
className="cursor-pointer"
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedModelId === model.id
? "opacity-100"
: "opacity-0",
)}
/>
<ModelSelectorLogo
provider={
PROVIDER_LOGO_MAP[
provider
] || provider
}
className="mr-2"
/>
<ModelSelectorName>
{model.modelId}
</ModelSelectorName>
{model.validated !== true && (
<span
title={
dict.modelConfig
.unvalidatedModelWarning
}
>
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
</span>
)}
</ModelSelectorItem>
))}
</ModelSelectorGroup>
),
)}
{/* Configure Option */}
<ModelSelectorSeparator />
<ModelSelectorGroup>
<ModelSelectorItem
value="__configure__"
onSelect={handleSelect}
className="cursor-pointer"
>
<Settings2 className="mr-2 h-4 w-4" />
<ModelSelectorName>
{dict.modelConfig.configureModels}
</ModelSelectorName>
</ModelSelectorItem>
</ModelSelectorGroup>
{/* Info text */}
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
{showUnvalidatedModels
? dict.modelConfig.allModelsShown
: dict.modelConfig.onlyVerifiedShown}
</div>
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelectorRoot>
</div>
)
}

View File

@@ -0,0 +1,125 @@
"use client"
import { Coffee, Settings, X } from "lucide-react"
import type React from "react"
import { FaGithub } from "react-icons/fa"
import { useDictionary } from "@/hooks/use-dictionary"
import { formatMessage } from "@/lib/i18n/utils"
interface QuotaLimitToastProps {
type?: "request" | "token"
used: number
limit: number
onDismiss: () => void
onConfigModel?: () => void
}
export function QuotaLimitToast({
type = "request",
used,
limit,
onDismiss,
onConfigModel,
}: QuotaLimitToastProps) {
const dict = useDictionary()
const isTokenLimit = type === "token"
const formatNumber = (n: number) =>
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n.toString()
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault()
onDismiss()
}
}
return (
<div
role="alert"
aria-live="polite"
tabIndex={0}
onKeyDown={handleKeyDown}
className="relative w-[400px] overflow-hidden rounded-xl border border-border/50 bg-card p-5 shadow-soft animate-message-in"
>
{/* Close button */}
<button
onClick={onDismiss}
className="absolute right-3 top-3 p-1.5 rounded-full text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
aria-label="Dismiss"
>
<X className="w-4 h-4" />
</button>
{/* Title row with icon */}
<div className="flex items-center gap-2.5 mb-3 pr-6">
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-accent flex items-center justify-center">
<Coffee
className="w-4 h-4 text-accent-foreground"
strokeWidth={2}
/>
</div>
<h3 className="font-semibold text-foreground text-sm">
{isTokenLimit
? dict.quota.tokenLimit
: dict.quota.dailyLimit}
</h3>
<span className="px-2 py-0.5 text-xs font-medium rounded-md bg-muted text-muted-foreground">
{formatMessage(dict.quota.usedOf, {
used: formatNumber(used),
limit: formatNumber(limit),
})}
</span>
</div>
{/* Message */}
<div className="text-sm text-muted-foreground leading-relaxed mb-4 space-y-2">
<p>
{isTokenLimit
? dict.quota.messageToken
: dict.quota.messageApi}
</p>
<p
dangerouslySetInnerHTML={{
__html: formatMessage(dict.quota.doubaoSponsorship, {
link: "https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project",
}),
}}
/>
<p dangerouslySetInnerHTML={{ __html: dict.quota.tip }} />
<p>{dict.quota.reset}</p>
</div>{" "}
{/* Action buttons */}
<div className="flex items-center gap-2">
{onConfigModel && (
<button
type="button"
onClick={() => {
onConfigModel()
onDismiss()
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
<Settings className="w-3.5 h-3.5" />
{dict.quota.configModel}
</button>
)}
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
>
<FaGithub className="w-3.5 h-3.5" />
{dict.quota.selfHost}
</a>
<a
href="https://github.com/sponsors/DayuanJiang"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-foreground hover:bg-muted transition-colors"
>
<Coffee className="w-3.5 h-3.5" />
{dict.quota.sponsor}
</a>
</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
"use client"; "use client"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -8,12 +8,13 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog"
import { useDictionary } from "@/hooks/use-dictionary"
interface ResetWarningModalProps { interface ResetWarningModalProps {
open: boolean; open: boolean
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void
onClear: () => void; onClear: () => void
} }
export function ResetWarningModal({ export function ResetWarningModal({
@@ -21,14 +22,15 @@ export function ResetWarningModal({
onOpenChange, onOpenChange,
onClear, onClear,
}: ResetWarningModalProps) { }: ResetWarningModalProps) {
const dict = useDictionary()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Clear Everything?</DialogTitle> <DialogTitle>{dict.dialogs.clearTitle}</DialogTitle>
<DialogDescription> <DialogDescription>
This will clear the current conversation and reset the {dict.dialogs.clearDescription}
diagram. This action cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
@@ -36,13 +38,13 @@ export function ResetWarningModal({
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
Cancel {dict.common.cancel}
</Button> </Button>
<Button variant="destructive" onClick={onClear}> <Button variant="destructive" onClick={onClear}>
Clear Everything {dict.dialogs.clearEverything}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

View File

@@ -1,36 +1,32 @@
"use client"; "use client"
import { useState, useEffect } from "react"; import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter, } from "@/components/ui/dialog"
} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select"
import { useDictionary } from "@/hooks/use-dictionary"
export type ExportFormat = "drawio" | "png" | "svg"; export type ExportFormat = "drawio" | "png" | "svg"
const FORMAT_OPTIONS: { value: ExportFormat; label: string; extension: string }[] = [
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
{ value: "png", label: "PNG Image", extension: ".png" },
{ value: "svg", label: "SVG Image", extension: ".svg" },
];
interface SaveDialogProps { interface SaveDialogProps {
open: boolean; open: boolean
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void
onSave: (filename: string, format: ExportFormat) => void; onSave: (filename: string, format: ExportFormat) => void
defaultFilename: string; defaultFilename: string
} }
export function SaveDialog({ export function SaveDialog({
@@ -39,46 +35,76 @@ export function SaveDialog({
onSave, onSave,
defaultFilename, defaultFilename,
}: SaveDialogProps) { }: SaveDialogProps) {
const [filename, setFilename] = useState(defaultFilename); const dict = useDictionary()
const [format, setFormat] = useState<ExportFormat>("drawio"); const [filename, setFilename] = useState(defaultFilename)
const [format, setFormat] = useState<ExportFormat>("drawio")
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setFilename(defaultFilename); setFilename(defaultFilename)
} }
}, [open, defaultFilename]); }, [open, defaultFilename])
const handleSave = () => { const handleSave = () => {
const finalFilename = filename.trim() || defaultFilename; const finalFilename = filename.trim() || defaultFilename
onSave(finalFilename, format); onSave(finalFilename, format)
onOpenChange(false); onOpenChange(false)
}; }
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault()
handleSave(); handleSave()
} }
}; }
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format); const FORMAT_OPTIONS = [
{
value: "drawio" as const,
label: dict.save.formats.drawio,
extension: ".drawio",
},
{
value: "png" as const,
label: dict.save.formats.png,
extension: ".png",
},
{
value: "svg" as const,
label: dict.save.formats.svg,
extension: ".svg",
},
]
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format)
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Save Diagram</DialogTitle> <DialogTitle>{dict.save.title}</DialogTitle>
<DialogDescription>
{dict.save.description}
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Format</label> <label className="text-sm font-medium">
<Select value={format} onValueChange={(v) => setFormat(v as ExportFormat)}> {dict.save.format}
</label>
<Select
value={format}
onValueChange={(v) => setFormat(v as ExportFormat)}
>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{FORMAT_OPTIONS.map((opt) => ( {FORMAT_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}> <SelectItem
key={opt.value}
value={opt.value}
>
{opt.label} {opt.label}
</SelectItem> </SelectItem>
))} ))}
@@ -86,13 +112,15 @@ export function SaveDialog({
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Filename</label> <label className="text-sm font-medium">
{dict.save.filename}
</label>
<div className="flex items-stretch"> <div className="flex items-stretch">
<Input <Input
value={filename} value={filename}
onChange={(e) => setFilename(e.target.value)} onChange={(e) => setFilename(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Enter filename" placeholder={dict.save.filenamePlaceholder}
autoFocus autoFocus
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
className="rounded-r-none border-r-0 focus-visible:z-10" className="rounded-r-none border-r-0 focus-visible:z-10"
@@ -104,12 +132,15 @@ export function SaveDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button
Cancel variant="outline"
onClick={() => onOpenChange(false)}
>
{dict.common.cancel}
</Button> </Button>
<Button onClick={handleSave}>Save</Button> <Button onClick={handleSave}>{dict.common.save}</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

View File

@@ -0,0 +1,430 @@
"use client"
import { Github, Info, Moon, Sun, Tag } from "lucide-react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path"
import { i18n, type Locale } from "@/lib/i18n/config"
// Reusable setting item component for consistent layout
function SettingItem({
label,
description,
children,
}: {
label: string
description?: string
children: React.ReactNode
}) {
return (
<div className="flex items-center justify-between py-4 first:pt-0 last:pb-0">
<div className="space-y-0.5 pr-4">
<Label className="text-sm font-medium">{label}</Label>
{description && (
<p className="text-xs text-muted-foreground max-w-[260px]">
{description}
</p>
)}
</div>
<div className="shrink-0">{children}</div>
</div>
)
}
const LANGUAGE_LABELS: Record<Locale, string> = {
en: "English",
zh: "中文",
ja: "日本語",
}
interface SettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onCloseProtectionChange?: (enabled: boolean) => void
drawioUi: "min" | "sketch"
onToggleDrawioUi: () => void
darkMode: boolean
onToggleDarkMode: () => void
minimalStyle?: boolean
onMinimalStyleChange?: (value: boolean) => void
}
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required"
function getStoredAccessCodeRequired(): boolean | null {
if (typeof window === "undefined") return null
const stored = localStorage.getItem(STORAGE_ACCESS_CODE_REQUIRED_KEY)
if (stored === null) return null
return stored === "true"
}
function SettingsContent({
open,
onOpenChange,
onCloseProtectionChange,
drawioUi,
onToggleDrawioUi,
darkMode,
onToggleDarkMode,
minimalStyle = false,
onMinimalStyleChange = () => {},
}: SettingsDialogProps) {
const dict = useDictionary()
const router = useRouter()
const pathname = usePathname() || "/"
const search = useSearchParams()
const [accessCode, setAccessCode] = useState("")
const [closeProtection, setCloseProtection] = useState(true)
const [isVerifying, setIsVerifying] = useState(false)
const [error, setError] = useState("")
const [accessCodeRequired, setAccessCodeRequired] = useState(
() => getStoredAccessCodeRequired() ?? false,
)
const [currentLang, setCurrentLang] = useState("en")
useEffect(() => {
// Only fetch if not cached in localStorage
if (getStoredAccessCodeRequired() !== null) return
fetch(getApiEndpoint("/api/config"))
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data) => {
const required = data?.accessCodeRequired === true
localStorage.setItem(
STORAGE_ACCESS_CODE_REQUIRED_KEY,
String(required),
)
setAccessCodeRequired(required)
})
.catch(() => {
// Don't cache on error - allow retry on next mount
setAccessCodeRequired(false)
})
}, [])
// Detect current language from pathname
useEffect(() => {
const seg = pathname.split("/").filter(Boolean)
const first = seg[0]
if (first && i18n.locales.includes(first as Locale)) {
setCurrentLang(first)
} else {
setCurrentLang(i18n.defaultLocale)
}
}, [pathname])
useEffect(() => {
if (open) {
const storedCode =
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
setAccessCode(storedCode)
const storedCloseProtection = localStorage.getItem(
STORAGE_CLOSE_PROTECTION_KEY,
)
// Default to true if not set
setCloseProtection(storedCloseProtection !== "false")
setError("")
}
}, [open])
const changeLanguage = (lang: string) => {
// Save locale to localStorage for persistence across restarts
localStorage.setItem("next-ai-draw-io-locale", lang)
const parts = pathname.split("/")
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
parts[1] = lang
} else {
parts.splice(1, 0, lang)
}
const newPath = parts.join("/") || "/"
const searchStr = search?.toString() ? `?${search.toString()}` : ""
router.push(newPath + searchStr)
}
const handleSave = async () => {
if (!accessCodeRequired) return
setError("")
setIsVerifying(true)
try {
const response = await fetch(
getApiEndpoint("/api/verify-access-code"),
{
method: "POST",
headers: {
"x-access-code": accessCode.trim(),
},
},
)
const data = await response.json()
if (!data.valid) {
setError(data.message || dict.errors.invalidAccessCode)
return
}
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
onOpenChange(false)
} catch {
setError(dict.errors.networkError)
} finally {
setIsVerifying(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault()
handleSave()
}
}
return (
<DialogContent className="sm:max-w-lg p-0 gap-0">
{/* Header */}
<DialogHeader className="px-6 pt-6 pb-4">
<DialogTitle>{dict.settings.title}</DialogTitle>
<DialogDescription className="mt-1">
{dict.settings.description}
</DialogDescription>
</DialogHeader>
{/* Content */}
<div className="px-6 pb-6">
<div className="divide-y divide-border-subtle">
{/* Access Code (conditional) */}
{accessCodeRequired && (
<div className="py-4 first:pt-0 space-y-3">
<div className="space-y-0.5">
<Label
htmlFor="access-code"
className="text-sm font-medium"
>
{dict.settings.accessCode}
</Label>
<p className="text-xs text-muted-foreground">
{dict.settings.accessCodeDescription}
</p>
</div>
<div className="flex gap-2">
<Input
id="access-code"
type="password"
value={accessCode}
onChange={(e) =>
setAccessCode(e.target.value)
}
onKeyDown={handleKeyDown}
placeholder={
dict.settings.accessCodePlaceholder
}
autoComplete="off"
className="h-9"
/>
<Button
onClick={handleSave}
disabled={isVerifying || !accessCode.trim()}
className="h-9 px-4 rounded-xl"
>
{isVerifying ? "..." : dict.common.save}
</Button>
</div>
{error && (
<p className="text-xs text-destructive">
{error}
</p>
)}
</div>
)}
{/* Language */}
<SettingItem
label={dict.settings.language}
description={dict.settings.languageDescription}
>
<Select
value={currentLang}
onValueChange={changeLanguage}
>
<SelectTrigger
id="language-select"
className="w-[120px] h-9 rounded-xl"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{i18n.locales.map((locale) => (
<SelectItem key={locale} value={locale}>
{LANGUAGE_LABELS[locale]}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingItem>
{/* Theme */}
<SettingItem
label={dict.settings.theme}
description={dict.settings.themeDescription}
>
<Button
id="theme-toggle"
variant="outline"
size="icon"
onClick={onToggleDarkMode}
className="h-9 w-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
>
{darkMode ? (
<Sun className="h-4 w-4" />
) : (
<Moon className="h-4 w-4" />
)}
</Button>
</SettingItem>
{/* Draw.io Style */}
<SettingItem
label={dict.settings.drawioStyle}
description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
>
<Button
id="drawio-ui"
variant="outline"
onClick={onToggleDrawioUi}
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
>
{dict.settings.switchTo}{" "}
{drawioUi === "min"
? dict.settings.sketch
: dict.settings.minimal}
</Button>
</SettingItem>
{/* Close Protection */}
<SettingItem
label={dict.settings.closeProtection}
description={dict.settings.closeProtectionDescription}
>
<Switch
id="close-protection"
checked={closeProtection}
onCheckedChange={(checked) => {
setCloseProtection(checked)
localStorage.setItem(
STORAGE_CLOSE_PROTECTION_KEY,
checked.toString(),
)
onCloseProtectionChange?.(checked)
}}
/>
</SettingItem>
{/* Diagram Style */}
<SettingItem
label={dict.settings.diagramStyle}
description={dict.settings.diagramStyleDescription}
>
<div className="flex items-center gap-2">
<Switch
id="minimal-style"
checked={minimalStyle}
onCheckedChange={onMinimalStyleChange}
/>
<span className="text-sm text-muted-foreground">
{minimalStyle
? dict.chat.minimalStyle
: dict.chat.styledMode}
</span>
</div>
</SettingItem>
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
<div className="flex items-center justify-center gap-3">
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Tag className="h-3 w-3" />
{process.env.APP_VERSION}
</span>
<span className="text-muted-foreground">·</span>
<a
href="https://github.com/DayuanJiang/next-ai-draw-io"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
>
<Github className="h-3 w-3" />
GitHub
</a>
{process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
"true" && (
<>
<span className="text-muted-foreground">·</span>
<a
href={`/${currentLang}/about${currentLang === "zh" ? "/cn" : currentLang === "ja" ? "/ja" : ""}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
>
<Info className="h-3 w-3" />
{dict.nav.about}
</a>
</>
)}
</div>
</div>
</DialogContent>
)
}
export function SettingsDialog(props: SettingsDialogProps) {
return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<Suspense
fallback={
<DialogContent className="sm:max-w-lg p-0">
<div className="h-80 flex items-center justify-center">
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" />
</div>
</DialogContent>
}
>
<SettingsContent {...props} />
</Suspense>
</Dialog>
)
}

View File

@@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -10,7 +10,7 @@ const buttonVariants = cva(
variants: { variants: {
variant: { variant: {
default: default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", "bg-primary text-primary-foreground shadow-xs hover:brightness-75",
destructive: destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:

View File

@@ -0,0 +1,33 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

191
components/ui/command.tsx Normal file
View File

@@ -0,0 +1,191 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className={cn("overflow-hidden p-0", className)}>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
onMouseEnter={(e) => {
// Ensure hover updates selection for visual feedback
const item = e.currentTarget
item.setAttribute("data-selected", "true")
// Deselect siblings
const siblings = item.parentElement?.querySelectorAll("[cmdk-item]")
siblings?.forEach((sibling) => {
if (sibling !== item) {
sibling.setAttribute("data-selected", "false")
}
})
}}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -38,7 +38,10 @@ function DialogOverlay({
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"duration-200",
className className
)} )}
{...props} {...props}
@@ -57,13 +60,32 @@ function DialogContent({
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", // Base styles
"fixed top-[50%] left-[50%] z-50 w-full",
"max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%]",
"grid gap-4 p-6",
// Refined visual treatment
"bg-surface-0 rounded-2xl border border-border-subtle shadow-dialog",
// Entry/exit animations
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-[0.98] data-[state=open]:zoom-in-[0.98]",
"data-[state=closed]:slide-out-to-top-[2%] data-[state=open]:slide-in-from-top-[2%]",
"duration-200 sm:max-w-lg",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"> <DialogPrimitive.Close className={cn(
"absolute top-4 right-4 rounded-xl p-1.5",
"text-muted-foreground/60 hover:text-foreground",
"hover:bg-interactive-hover",
"transition-all duration-150",
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"disabled:pointer-events-none",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4"
)}>
<XIcon /> <XIcon />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
@@ -102,7 +124,10 @@ function DialogTitle({
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)} className={cn(
"text-xl font-semibold tracking-tight leading-tight",
className
)}
{...props} {...props}
/> />
) )
@@ -115,7 +140,10 @@ function DialogDescription({
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)} className={cn(
"text-sm text-muted-foreground leading-relaxed",
className
)}
{...props} {...props}
/> />
) )

View File

@@ -8,9 +8,30 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", // Base styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "flex h-10 w-full min-w-0 rounded-xl px-3.5 py-2",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "border border-border-subtle bg-surface-1",
"text-sm text-foreground",
// Placeholder
"placeholder:text-muted-foreground/60",
// Selection
"selection:bg-primary selection:text-primary-foreground",
// Transitions
"transition-all duration-150 ease-out",
// Hover state
"hover:border-border-default",
// Focus state - refined ring
"focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10",
// File input
"file:text-foreground file:inline-flex file:h-7 file:border-0",
"file:bg-transparent file:text-sm file:font-medium",
// Disabled
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
// Invalid state
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
"dark:aria-invalid:ring-destructive/40",
// Dark mode background
"dark:bg-surface-1",
className className
)} )}
{...props} {...props}

24
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

48
components/ui/popover.tsx Normal file
View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,56 @@
"use client"
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@@ -18,7 +18,7 @@ function ScrollArea({
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1" className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 !overflow-x-hidden"
> >
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>

31
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -1,178 +1,344 @@
"use client"; "use client"
import React, { createContext, useContext, useRef, useState } from "react"; import type React from "react"
import type { DrawIoEmbedRef } from "react-drawio"; import { createContext, useContext, useEffect, useRef, useState } from "react"
import { extractDiagramXML } from "../lib/utils"; import type { DrawIoEmbedRef } from "react-drawio"
import type { ExportFormat } from "@/components/save-dialog"; import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
import type { ExportFormat } from "@/components/save-dialog"
import { getApiEndpoint } from "@/lib/base-path"
import { extractDiagramXML, validateAndFixXml } from "../lib/utils"
interface DiagramContextType { interface DiagramContextType {
chartXML: string; chartXML: string
latestSvg: string; latestSvg: string
diagramHistory: { svg: string; xml: string }[]; diagramHistory: { svg: string; xml: string }[]
loadDiagram: (chart: string) => void; loadDiagram: (chart: string, skipValidation?: boolean) => string | null
handleExport: () => void; handleExport: () => void
handleExportWithoutHistory: () => void; handleExportWithoutHistory: () => void
resolverRef: React.Ref<((value: string) => void) | null>; resolverRef: React.Ref<((value: string) => void) | null>
drawioRef: React.Ref<DrawIoEmbedRef | null>; drawioRef: React.Ref<DrawIoEmbedRef | null>
handleDiagramExport: (data: any) => void; handleDiagramExport: (data: any) => void
clearDiagram: () => void; clearDiagram: () => void
saveDiagramToFile: (filename: string, format: ExportFormat) => void; saveDiagramToFile: (
filename: string,
format: ExportFormat,
sessionId?: string,
) => void
saveDiagramToStorage: () => Promise<void>
isDrawioReady: boolean
onDrawioLoad: () => void
resetDrawioReady: () => void
showSaveDialog: boolean
setShowSaveDialog: (show: boolean) => void
} }
const DiagramContext = createContext<DiagramContextType | undefined>(undefined); const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
export function DiagramProvider({ children }: { children: React.ReactNode }) { export function DiagramProvider({ children }: { children: React.ReactNode }) {
const [chartXML, setChartXML] = useState<string>(""); const [chartXML, setChartXML] = useState<string>("")
const [latestSvg, setLatestSvg] = useState<string>(""); const [latestSvg, setLatestSvg] = useState<string>("")
const [diagramHistory, setDiagramHistory] = useState< const [diagramHistory, setDiagramHistory] = useState<
{ svg: string; xml: string }[] { svg: string; xml: string }[]
>([]); >([])
const drawioRef = useRef<DrawIoEmbedRef | null>(null); const [isDrawioReady, setIsDrawioReady] = useState(false)
const resolverRef = useRef<((value: string) => void) | null>(null); const [canSaveDiagram, setCanSaveDiagram] = useState(false)
const [showSaveDialog, setShowSaveDialog] = useState(false)
const hasCalledOnLoadRef = useRef(false)
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
const resolverRef = useRef<((value: string) => void) | null>(null)
// Track if we're expecting an export for history (user-initiated) // Track if we're expecting an export for history (user-initiated)
const expectHistoryExportRef = useRef<boolean>(false); const expectHistoryExportRef = useRef<boolean>(false)
// Track if diagram has been restored from localStorage
const hasDiagramRestoredRef = useRef<boolean>(false)
const onDrawioLoad = () => {
// Only set ready state once to prevent infinite loops
if (hasCalledOnLoadRef.current) return
hasCalledOnLoadRef.current = true
// console.log("[DiagramContext] DrawIO loaded, setting ready state")
setIsDrawioReady(true)
}
const resetDrawioReady = () => {
// console.log("[DiagramContext] Resetting DrawIO ready state")
hasCalledOnLoadRef.current = false
setIsDrawioReady(false)
}
// Restore diagram XML when DrawIO becomes ready
// eslint-disable-next-line react-hooks/exhaustive-deps -- loadDiagram uses refs internally and is stable
useEffect(() => {
// Reset restore flag when DrawIO is not ready (e.g., theme/UI change remounts it)
if (!isDrawioReady) {
hasDiagramRestoredRef.current = false
setCanSaveDiagram(false)
return
}
if (hasDiagramRestoredRef.current) return
hasDiagramRestoredRef.current = true
try {
const savedDiagramXml = localStorage.getItem(
STORAGE_DIAGRAM_XML_KEY,
)
if (savedDiagramXml) {
// Skip validation for trusted saved diagrams
loadDiagram(savedDiagramXml, true)
}
} catch (error) {
console.error("Failed to restore diagram from localStorage:", error)
}
// Allow saving after restore is complete
setTimeout(() => {
setCanSaveDiagram(true)
}, 500)
}, [isDrawioReady])
// Save diagram XML to localStorage whenever it changes (debounced)
useEffect(() => {
if (!canSaveDiagram) return
if (!chartXML || chartXML.length <= 300) return
const timeoutId = setTimeout(() => {
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
}, 1000)
return () => clearTimeout(timeoutId)
}, [chartXML, canSaveDiagram])
// Track if we're expecting an export for file save (stores raw export data) // Track if we're expecting an export for file save (stores raw export data)
const saveResolverRef = useRef<{ const saveResolverRef = useRef<{
resolver: ((data: string) => void) | null; resolver: ((data: string) => void) | null
format: ExportFormat | null; format: ExportFormat | null
}>({ resolver: null, format: null }); }>({ resolver: null, format: null })
const handleExport = () => { const handleExport = () => {
if (drawioRef.current) { if (drawioRef.current) {
// Mark that this export should be saved to history // Mark that this export should be saved to history
expectHistoryExportRef.current = true; expectHistoryExportRef.current = true
drawioRef.current.exportDiagram({ drawioRef.current.exportDiagram({
format: "xmlsvg", format: "xmlsvg",
}); })
} }
}; }
const handleExportWithoutHistory = () => { const handleExportWithoutHistory = () => {
if (drawioRef.current) { if (drawioRef.current) {
// Export without saving to history (for edit_diagram fetching current state) // Export without saving to history (for edit_diagram fetching current state)
drawioRef.current.exportDiagram({ drawioRef.current.exportDiagram({
format: "xmlsvg", format: "xmlsvg",
}); })
} }
}; }
// Save current diagram to localStorage (used before theme/UI changes)
const saveDiagramToStorage = async (): Promise<void> => {
if (!drawioRef.current) return
try {
const currentXml = await Promise.race([
new Promise<string>((resolve) => {
resolverRef.current = resolve
drawioRef.current?.exportDiagram({ format: "xmlsvg" })
}),
new Promise<string>((_, reject) =>
setTimeout(() => reject(new Error("Export timeout")), 2000),
),
])
// Only save if diagram has meaningful content (not empty template)
if (currentXml && currentXml.length > 300) {
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, currentXml)
}
} catch (error) {
console.error("Failed to save diagram to storage:", error)
}
}
const loadDiagram = (
chart: string,
skipValidation?: boolean,
): string | null => {
let xmlToLoad = chart
// Validate XML structure before loading (unless skipped for internal use)
if (!skipValidation) {
const validation = validateAndFixXml(chart)
if (!validation.valid) {
console.warn(
"[loadDiagram] Validation error:",
validation.error,
)
return validation.error
}
// Use fixed XML if auto-fix was applied
if (validation.fixed) {
console.log(
"[loadDiagram] Auto-fixed XML issues:",
validation.fixes,
)
xmlToLoad = validation.fixed
}
}
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
setChartXML(xmlToLoad)
const loadDiagram = (chart: string) => {
if (drawioRef.current) { if (drawioRef.current) {
drawioRef.current.load({ drawioRef.current.load({
xml: chart, xml: xmlToLoad,
}); })
} }
};
return null
}
const handleDiagramExport = (data: any) => { const handleDiagramExport = (data: any) => {
// Handle save to file if requested (process raw data before extraction) // Handle save to file if requested (process raw data before extraction)
if (saveResolverRef.current.resolver) { if (saveResolverRef.current.resolver) {
const format = saveResolverRef.current.format; const format = saveResolverRef.current.format
saveResolverRef.current.resolver(data.data); saveResolverRef.current.resolver(data.data)
saveResolverRef.current = { resolver: null, format: null }; saveResolverRef.current = { resolver: null, format: null }
// For non-xmlsvg formats, skip XML extraction as it will fail // For non-xmlsvg formats, skip XML extraction as it will fail
// Only drawio (which uses xmlsvg internally) has the content attribute // Only drawio (which uses xmlsvg internally) has the content attribute
if (format === "png" || format === "svg") { if (format === "png" || format === "svg") {
return; return
} }
} }
const extractedXML = extractDiagramXML(data.data); const extractedXML = extractDiagramXML(data.data)
setChartXML(extractedXML); setChartXML(extractedXML)
setLatestSvg(data.data); setLatestSvg(data.data)
// Only add to history if this was a user-initiated export // Only add to history if this was a user-initiated export
// Limit to 20 entries to prevent memory leaks during long sessions
const MAX_HISTORY_SIZE = 20
if (expectHistoryExportRef.current) { if (expectHistoryExportRef.current) {
setDiagramHistory((prev) => [ setDiagramHistory((prev) => {
...prev, const newHistory = [
{ ...prev,
svg: data.data, {
xml: extractedXML, svg: data.data,
}, xml: extractedXML,
]); },
expectHistoryExportRef.current = false; ]
// Keep only the last MAX_HISTORY_SIZE entries (circular buffer)
return newHistory.slice(-MAX_HISTORY_SIZE)
})
expectHistoryExportRef.current = false
} }
if (resolverRef.current) { if (resolverRef.current) {
resolverRef.current(extractedXML); resolverRef.current(extractedXML)
resolverRef.current = null; resolverRef.current = null
} }
}; }
const clearDiagram = () => { const clearDiagram = () => {
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`; const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
loadDiagram(emptyDiagram); // Skip validation for trusted internal template (loadDiagram also sets chartXML)
setChartXML(emptyDiagram); loadDiagram(emptyDiagram, true)
setLatestSvg(""); setLatestSvg("")
setDiagramHistory([]); setDiagramHistory([])
}; }
const saveDiagramToFile = (filename: string, format: ExportFormat) => { const saveDiagramToFile = (
filename: string,
format: ExportFormat,
sessionId?: string,
) => {
if (!drawioRef.current) { if (!drawioRef.current) {
console.warn("Draw.io editor not ready"); console.warn("Draw.io editor not ready")
return; return
} }
// Map format to draw.io export format // Map format to draw.io export format
const drawioFormat = format === "drawio" ? "xmlsvg" : format; const drawioFormat = format === "drawio" ? "xmlsvg" : format
// Set up the resolver before triggering export // Set up the resolver before triggering export
saveResolverRef.current = { saveResolverRef.current = {
resolver: (exportData: string) => { resolver: (exportData: string) => {
let fileContent: string | Blob; let fileContent: string | Blob
let mimeType: string; let mimeType: string
let extension: string; let extension: string
if (format === "drawio") { if (format === "drawio") {
// Extract XML from SVG for .drawio format // Extract XML from SVG for .drawio format
const xml = extractDiagramXML(exportData); const xml = extractDiagramXML(exportData)
let xmlContent = xml; let xmlContent = xml
if (!xml.includes("<mxfile")) { if (!xml.includes("<mxfile")) {
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`; xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`
} }
fileContent = xmlContent; fileContent = xmlContent
mimeType = "application/xml"; mimeType = "application/xml"
extension = ".drawio"; extension = ".drawio"
// Save to localStorage when user manually saves
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xmlContent)
} else if (format === "png") { } else if (format === "png") {
// PNG data comes as base64 data URL // PNG data comes as base64 data URL
fileContent = exportData; fileContent = exportData
mimeType = "image/png"; mimeType = "image/png"
extension = ".png"; extension = ".png"
} else { } else {
// SVG format // SVG format
fileContent = exportData; fileContent = exportData
mimeType = "image/svg+xml"; mimeType = "image/svg+xml"
extension = ".svg"; extension = ".svg"
} }
// Log save event to Langfuse (flags the trace)
logSaveToLangfuse(filename, format, sessionId)
// Handle download // Handle download
let url: string; let url: string
if (typeof fileContent === "string" && fileContent.startsWith("data:")) { if (
typeof fileContent === "string" &&
fileContent.startsWith("data:")
) {
// Already a data URL (PNG) // Already a data URL (PNG)
url = fileContent; url = fileContent
} else { } else {
const blob = new Blob([fileContent], { type: mimeType }); const blob = new Blob([fileContent], { type: mimeType })
url = URL.createObjectURL(blob); url = URL.createObjectURL(blob)
} }
const a = document.createElement("a"); const a = document.createElement("a")
a.href = url; a.href = url
a.download = `${filename}${extension}`; a.download = `${filename}${extension}`
document.body.appendChild(a); document.body.appendChild(a)
a.click(); a.click()
document.body.removeChild(a); document.body.removeChild(a)
// Delay URL revocation to ensure download completes // Delay URL revocation to ensure download completes
if (!url.startsWith("data:")) { if (!url.startsWith("data:")) {
setTimeout(() => URL.revokeObjectURL(url), 100); setTimeout(() => URL.revokeObjectURL(url), 100)
} }
}, },
format, format,
}; }
// Export diagram - callback will be handled in handleDiagramExport // Export diagram - callback will be handled in handleDiagramExport
drawioRef.current.exportDiagram({ format: drawioFormat }); drawioRef.current.exportDiagram({ format: drawioFormat })
}; }
// Log save event to Langfuse (just flags the trace, doesn't send content)
const logSaveToLangfuse = async (
filename: string,
format: string,
sessionId?: string,
) => {
try {
await fetch(getApiEndpoint("/api/log-save"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename, format, sessionId }),
})
} catch (error) {
console.warn("Failed to log save to Langfuse:", error)
}
}
return ( return (
<DiagramContext.Provider <DiagramContext.Provider
@@ -188,17 +354,23 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
handleDiagramExport, handleDiagramExport,
clearDiagram, clearDiagram,
saveDiagramToFile, saveDiagramToFile,
saveDiagramToStorage,
isDrawioReady,
onDrawioLoad,
resetDrawioReady,
showSaveDialog,
setShowSaveDialog,
}} }}
> >
{children} {children}
</DiagramContext.Provider> </DiagramContext.Provider>
); )
} }
export function useDiagram() { export function useDiagram() {
const context = useContext(DiagramContext); const context = useContext(DiagramContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useDiagram must be used within a DiagramProvider"); throw new Error("useDiagram must be used within a DiagramProvider")
} }
return context; return context
} }

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
services:
drawio:
image: jgraph/drawio:latest
ports: ["8080:8080"]
next-ai-draw-io:
build:
context: .
args:
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
# Uncomment below for subdirectory deployment
# - NEXT_PUBLIC_BASE_PATH=/nextaidrawio
ports: ["3000:3000"]
env_file: .env
environment:
# For subdirectory deployment, uncomment and set your path:
# NEXT_PUBLIC_BASE_PATH: /nextaidrawio
depends_on: [drawio]

244
docs/cn/README_CN.md Normal file
View File

@@ -0,0 +1,244 @@
# Next AI Draw.io
<div align="center">
**AI驱动的图表创建工具 - 对话、绘制、可视化**
[English](../../README.md) | 中文 | [日本語](../ja/README_JA.md)
[![TrendShift](https://trendshift.io/api/badge/repositories/15449)](https://next-ai-drawio.jiang.jp/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Next.js](https://img.shields.io/badge/Next.js-16.x-black)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19.x-61dafb)](https://react.dev/)
[![Sponsor](https://img.shields.io/badge/Sponsor-❤-ea4aaa)](https://github.com/sponsors/DayuanJiang)
[![Live Demo](../../public/live-demo-button.svg)](https://next-ai-drawio.jiang.jp/)
</div>
一个集成了AI功能的Next.js网页应用与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。
> 注:感谢 <img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 的赞助支持,本项目的 Demo 现已接入强大的 K2-thinking 模型!
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
## 目录
- [Next AI Draw.io](#next-ai-drawio)
- [目录](#目录)
- [示例](#示例)
- [功能特性](#功能特性)
- [MCP服务器预览](#mcp服务器预览)
- [Claude Code CLI](#claude-code-cli)
- [快速开始](#快速开始)
- [在线试用](#在线试用)
- [桌面应用](#桌面应用)
- [使用Docker运行](#使用docker运行)
- [安装](#安装)
- [部署](#部署)
- [部署到腾讯云EdgeOne Pages](#部署到腾讯云edgeone-pages)
- [部署到Vercel推荐](#部署到vercel推荐)
- [部署到Cloudflare Workers](#部署到cloudflare-workers)
- [多提供商支持](#多提供商支持)
- [工作原理](#工作原理)
- [支持与联系](#支持与联系)
- [Star历史](#star历史)
## 示例
以下是一些示例提示词及其生成的图表:
<div align="center">
<table width="100%">
<tr>
<td colspan="2" valign="top" align="center">
<strong>动画Transformer连接器</strong><br />
<p><strong>提示词:</strong> 给我一个带有**动画连接器**的Transformer架构图。</p>
<img src="../../public/animated_connectors.svg" alt="带动画连接器的Transformer架构" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCP架构图</strong><br />
<p><strong>提示词:</strong> 使用**GCP图标**生成一个GCP架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/gcp_demo.svg" alt="GCP架构图" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWS架构图</strong><br />
<p><strong>提示词:</strong> 使用**AWS图标**生成一个AWS架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/aws_demo.svg" alt="AWS架构图" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azure架构图</strong><br />
<p><strong>提示词:</strong> 使用**Azure图标**生成一个Azure架构图。在这个图中用户连接到托管在实例上的前端。</p>
<img src="../../public/azure_demo.svg" alt="Azure架构图" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫咪素描</strong><br />
<p><strong>提示词:</strong> 给我画一只可爱的猫。</p>
<img src="../../public/cat_demo.svg" alt="猫咪绘图" width="240" />
</td>
</tr>
</table>
</div>
## 功能特性
- **LLM驱动的图表创建**利用大语言模型通过自然语言命令直接创建和操作draw.io图表
- **基于图像的图表复制**上传现有图表或图像让AI自动复制和增强
- **PDF和文本文件上传**上传PDF文档和文本文件提取内容并从现有文档生成图表
- **AI推理过程显示**查看支持模型的AI思考过程OpenAI o1/o3、Gemini、Claude等
- **图表历史记录**全面的版本控制跟踪所有更改允许您查看和恢复AI编辑前的图表版本
- **交互式聊天界面**与AI实时对话来完善您的图表
- **云架构图支持**专门支持生成云架构图AWS、GCP、Azure
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
## MCP服务器预览
> **预览功能**:此功能为实验性功能,可能不稳定。
通过MCP模型上下文协议在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。
```json
{
"mcpServers": {
"drawio": {
"command": "npx",
"args": ["@next-ai-drawio/mcp-server@latest"]
}
}
}
```
### Claude Code CLI
```bash
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
```
然后让Claude创建图表
> "创建一个展示用户认证流程的流程图包含登录、MFA和会话管理"
图表会实时显示在浏览器中!
详情请参阅[MCP服务器README](../../packages/mcp-server/README.md)了解VS Code、Cursor等客户端配置。
## 快速开始
### 在线试用
无需安装!直接在我们的演示站点试用:
[![Live Demo](../../public/live-demo-button.svg)](https://next-ai-drawio.jiang.jp/)
> **使用自己的 API Key**:您可以使用自己的 API Key 来绕过演示站点的用量限制。点击聊天面板中的设置图标即可配置您的 Provider 和 API Key。您的 Key 仅保存在浏览器本地,不会被存储在服务器上。
### 桌面应用
从 [Releases 页面](https://github.com/DayuanJiang/next-ai-draw-io/releases) 下载适用于您平台的原生桌面应用:
支持的平台Windows、macOS、Linux。
### 使用Docker运行
[查看 Docker 指南](./docker.md)
### 安装
1. 克隆仓库:
```bash
git clone https://github.com/DayuanJiang/next-ai-draw-io
cd next-ai-draw-io
npm install
cp env.example .env.local
```
详细设置说明请参阅[提供商配置指南](./ai-providers.md)。
2. 运行开发服务器:
```bash
npm run dev
```
3. 在浏览器中打开 [http://localhost:6002](http://localhost:6002) 查看应用。
## 部署
### 部署到腾讯云EdgeOne Pages
您可以通过[腾讯云EdgeOne Pages](https://pages.edgeone.ai/zh)一键部署。
直接点击此按钮一键部署:
[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
查看[腾讯云EdgeOne Pages文档](https://pages.edgeone.ai/zh/document/product-introduction)了解更多详情。
同时通过腾讯云EdgeOne Pages部署也会获得[每日免费的DeepSeek模型额度](https://edgeone.cloud.tencent.com/pages/document/169925463311781888)。
### 部署到Vercel推荐
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
部署Next.js应用最简单的方式是使用Next.js创建者提供的[Vercel平台](https://vercel.com/new)。请确保在Vercel控制台中**设置环境变量**,就像您在本地 `.env.local` 文件中所做的那样。
查看[Next.js部署文档](https://nextjs.org/docs/app/building-your-application/deploying)了解更多详情。
### 部署到Cloudflare Workers
[查看 Cloudflare 部署指南](./cloudflare-deploy.md)
## 多提供商支持
- [字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
- AWS Bedrock默认
- OpenAI
- Anthropic
- Google AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
- SiliconFlow
- SGLang
- Vercel AI Gateway
除AWS Bedrock和OpenRouter外所有提供商都支持自定义端点。
📖 **[详细的提供商配置指南](./ai-providers.md)** - 查看各提供商的设置说明。
**模型要求**此任务需要强大的模型能力因为它涉及生成具有严格格式约束的长文本draw.io XML。推荐使用 Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro 和 DeepSeek V3.2/R1。
注意:`claude` 系列已在带有 AWS、Azure、GCP 等云架构 Logo 的 draw.io 图表上进行训练,因此如果您想创建云架构图,这是最佳选择。
## 工作原理
本应用使用以下技术:
- **Next.js**:用于前端框架和路由
- **Vercel AI SDK**`ai` + `@ai-sdk/*`用于流式AI响应和多提供商支持
- **react-drawio**:用于图表表示和操作
图表以XML格式表示可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。
## 支持与联系
**特别感谢[字节跳动豆包](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)赞助演示站点的 API Token 使用!** 注册火山引擎 ARK 平台即可获得50万免费Token
如果您觉得这个项目有用,请考虑[赞助](https://github.com/sponsors/DayuanJiang)来帮助我托管在线演示站点!
如需支持或咨询请在GitHub仓库上提交issue或联系维护者
- 邮箱me[at]jiang.jp
## Star历史
[![Star History Chart](https://api.star-history.com/svg?repos=DayuanJiang/next-ai-draw-io&type=date&legend=top-left)](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
---

236
docs/cn/ai-providers.md Normal file
View File

@@ -0,0 +1,236 @@
# AI 提供商配置
本指南介绍如何为 next-ai-draw-io 配置不同的 AI 模型提供商。
## 快速开始
1.`.env.example` 复制为 `.env.local`
2. 设置所选提供商的 API 密钥
3.`AI_MODEL` 设置为所需的模型
4. 运行 `npm run dev`
## 支持的提供商
### 豆包 (字节跳动火山引擎)
> **免费 Token**:在 [火山引擎 ARK 平台](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) 注册,即可获得所有模型 50 万免费 Token
```bash
DOUBAO_API_KEY=your_api_key
AI_MODEL=doubao-seed-1-8-251215 # 或其他豆包模型
```
### Google Gemini
```bash
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
AI_MODEL=gemini-2.0-flash
```
可选的自定义端点:
```bash
GOOGLE_BASE_URL=https://your-custom-endpoint
```
### OpenAI
```bash
OPENAI_API_KEY=your_api_key
AI_MODEL=gpt-4o
```
可选的自定义端点(用于 OpenAI 兼容服务):
```bash
OPENAI_BASE_URL=https://your-custom-endpoint/v1
```
### Anthropic
```bash
ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514
```
可选的自定义端点:
```bash
ANTHROPIC_BASE_URL=https://your-custom-endpoint
```
### DeepSeek
```bash
DEEPSEEK_API_KEY=your_api_key
AI_MODEL=deepseek-chat
```
可选的自定义端点:
```bash
DEEPSEEK_BASE_URL=https://your-custom-endpoint
```
### SiliconFlow (OpenAI 兼容)
```bash
SILICONFLOW_API_KEY=your_api_key
AI_MODEL=deepseek-ai/DeepSeek-V3 # 示例;使用任何 SiliconFlow 模型 ID
```
可选的自定义端点(默认为推荐域名):
```bash
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # 或 https://api.siliconflow.cn/v1
```
### SGLang
```bash
SGLANG_API_KEY=your_api_key
AI_MODEL=your_model_id
```
可选的自定义端点:
```bash
SGLANG_BASE_URL=https://your-custom-endpoint/v1
```
### Azure OpenAI
```bash
AZURE_API_KEY=your_api_key
AZURE_RESOURCE_NAME=your-resource-name # 必填:您的 Azure 资源名称
AI_MODEL=your-deployment-name
```
或者使用自定义端点代替资源名称:
```bash
AZURE_API_KEY=your_api_key
AZURE_BASE_URL=https://your-resource.openai.azure.com # AZURE_RESOURCE_NAME 的替代方案
AI_MODEL=your-deployment-name
```
可选的推理配置:
```bash
AZURE_REASONING_EFFORT=low # 可选low, medium, high
AZURE_REASONING_SUMMARY=detailed # 可选none, brief, detailed
```
### AWS Bedrock
```bash
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
```
注意:在 AWS 环境Lambda、带有 IAM 角色的 EC2凭证会自动从 IAM 角色获取。
### OpenRouter
```bash
OPENROUTER_API_KEY=your_api_key
AI_MODEL=anthropic/claude-sonnet-4
```
可选的自定义端点:
```bash
OPENROUTER_BASE_URL=https://your-custom-endpoint
```
### Ollama (本地)
```bash
AI_PROVIDER=ollama
AI_MODEL=llama3.2
```
可选的自定义 URL
```bash
OLLAMA_BASE_URL=http://localhost:11434
```
### Vercel AI Gateway
Vercel AI Gateway 通过单个 API 密钥提供对多个 AI 提供商的统一访问。这简化了身份验证,让您无需管理多个 API 密钥即可在不同提供商之间切换。
**基本用法Vercel 托管网关):**
```bash
AI_GATEWAY_API_KEY=your_gateway_api_key
AI_MODEL=openai/gpt-4o
```
**自定义网关 URL用于本地开发或自托管网关**
```bash
AI_GATEWAY_API_KEY=your_custom_api_key
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
AI_MODEL=openai/gpt-4o
```
模型格式使用 `provider/model` 语法:
- `openai/gpt-4o` - OpenAI GPT-4o
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
**配置说明:**
- 如果未设置 `AI_GATEWAY_BASE_URL`,则使用默认的 Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`)
- 自定义基础 URL 适用于:
- 使用自定义网关实例进行本地开发
- 自托管 AI Gateway 部署
- 企业代理配置
- 当使用自定义基础 URL 时,必须同时提供 `AI_GATEWAY_API_KEY`
从 [Vercel AI Gateway 仪表板](https://vercel.com/ai-gateway) 获取您的 API 密钥。
## 自动检测
如果您只配置了**一个**提供商的 API 密钥,系统将自动检测并使用该提供商。无需设置 `AI_PROVIDER`
如果您配置了**多个** API 密钥,则必须显式设置 `AI_PROVIDER`
```bash
AI_PROVIDER=google # 或openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
```
## 模型能力要求
此任务对模型能力要求极高因为它涉及生成具有严格格式约束draw.io XML的长文本。
**推荐模型**
- Claude Sonnet 4.5 / Opus 4.5
**关于 Ollama 的说明**:虽然支持将 Ollama 作为提供商,但除非您在本地运行像 DeepSeek R1 或 Qwen3-235B 这样的高性能模型,否则对于此用例通常不太实用。
## 温度设置 (Temperature)
您可以通过环境变量选择性地配置温度:
```bash
TEMPERATURE=0 # 输出更具确定性(推荐用于图表)
```
**重要提示**:对于不支持温度设置的模型(例如以下模型),请勿设置 `TEMPERATURE`
- GPT-5.1 和其他推理模型
- 某些专用模型
未设置时,模型将使用其默认行为。
## 推荐
- **最佳体验**使用支持视觉的模型GPT-4o, Claude, Gemini以获得图像转图表功能
- **经济实惠**DeepSeek 提供具有竞争力的价格
- **隐私保护**:使用 Ollama 进行完全本地、离线的操作(需要强大的硬件支持)
- **灵活性**OpenRouter 通过单一 API 提供对众多模型的访问

View File

@@ -0,0 +1,267 @@
# 部署到 Cloudflare Workers
本项目可以通过 **OpenNext 适配器** 部署为 **Cloudflare Worker**,为您提供:
- 全球边缘部署
- 极低延迟
- 免费的 `workers.dev` 域名托管
- 通过 R2 实现完整的 Next.js ISR 支持(可选)
> **Windows 用户重要提示:** OpenNext 和 Wrangler 在 **原生 Windows 环境下并不完全可靠**。建议方案:
>
> - 使用 **GitHub Codespaces**(完美运行)
> - 或者使用 **WSL (Linux)**
>
> 纯 Windows 构建可能会因为 WASM 文件路径问题而失败。
---
## 前置条件
1. 一个 **Cloudflare 账户**(免费版即可满足基本部署需求)
2. **Node.js 18+**
3. 安装 **Wrangler CLI**(作为开发依赖安装即可):
```bash
npm install -D wrangler
```
4. 登录 Cloudflare
```bash
npx wrangler login
```
> **注意:** 只有在启用 R2 进行 ISR 缓存时才需要绑定支付方式。基本的 Workers 部署是免费的。
---
## 第一步 — 安装依赖
```bash
npm install
```
---
## 第二步 — 配置环境变量
Cloudflare 在本地测试时使用不同的文件。
### 1) 创建 `.dev.vars`(用于 Cloudflare 本地调试 + 部署)
```bash
cp env.example .dev.vars
```
填入您的 API 密钥和配置信息。
### 2) 确保 `.env.local` 也存在(用于常规 Next.js 开发)
```bash
cp env.example .env.local
```
在此处填入相同的值。
---
## 第三步 — 选择部署类型
### 选项 A不使用 R2 部署(简单,免费)
如果您不需要 ISR 缓存,可以选择不使用 R2 进行部署:
**1. 使用简单的 `open-next.config.ts`**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
export default defineCloudflareConfig({})
```
**2. 使用简单的 `wrangler.jsonc`(不包含 r2_buckets**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
直接跳至 **第四步**
---
### 选项 B使用 R2 部署(完整的 ISR 支持)
R2 开启了 **增量静态再生 (ISR)** 缓存功能。需要在您的 Cloudflare 账户中绑定支付方式。
**1. 在 Cloudflare 控制台中创建 R2 存储桶:**
- 进入 **Storage & Databases → R2**
- 点击 **Create bucket**
- 命名为:`next-inc-cache`
**2. 配置 `open-next.config.ts`**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
})
```
**3. 配置 `wrangler.jsonc`(包含 R2**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "next-inc-cache"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
> **重要提示:** `bucket_name` 必须与您在 Cloudflare 控制台中创建的名称完全一致。
---
## 第四步 — 注册 workers.dev 子域名(仅首次需要)
在首次部署之前,您需要一个 workers.dev 子域名。
**选项 1通过 Cloudflare 控制台(推荐)**
访问https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
**选项 2在部署过程中**
运行 `npm run deploy`Wrangler 可能会提示:
```
Would you like to register a workers.dev subdomain? (Y/n)
```
输入 `Y` 并选择一个子域名。
> **注意:** 在 CI/CD 或非交互式环境中,该提示不会出现。请先通过控制台进行注册。
---
## 第五步 — 部署到 Cloudflare
```bash
npm run deploy
```
该脚本执行的操作:
- 构建 Next.js 应用
- 通过 OpenNext 将其转换为 Cloudflare Worker
- 上传静态资源
- 发布 Worker
您的应用将可通过以下地址访问:
```
https://<worker-name>.<your-subdomain>.workers.dev
```
---
## 常见问题与修复
### `You need to register a workers.dev subdomain`
**原因:** 您的账户尚未注册 workers.dev 子域名。
**修复:** 前往 https://dash.cloudflare.com → Workers & Pages → Set up a subdomain。
---
### `Please enable R2 through the Cloudflare Dashboard`
**原因:** wrangler.jsonc 中配置了 R2但您的账户尚未启用该功能。
**修复:** 启用 R2需要支付方式或使用选项 A不使用 R2 部署)。
---
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
**原因:** `wrangler.jsonc` 中缺少 `r2_buckets` 配置。
**修复:** 添加 `r2_buckets` 部分或切换到选项 A不使用 R2
---
### `Can't set compatibility date in the future`
**原因:** wrangler 配置中的 `compatibility_date` 设置为了未来的日期。
**修复:**`compatibility_date` 修改为今天或更早的日期。
---
### Windows 错误:`resvg.wasm?module` (ENOENT)
**原因:** Windows 文件名不能包含 `?`,但某个 wasm 资源文件名中使用了 `?module`
**修复:** 在 Linux 环境WSL、Codespaces 或 CI上进行构建/部署。
---
## 可选:本地预览
部署前在本地预览 Worker
```bash
npm run preview
```
---
## 总结
| 功能 | 不使用 R2 | 使用 R2 |
|---------|------------|---------|
| 成本 | 免费 | 需要绑定支付方式 |
| ISR 缓存 | 无 | 有 |
| 静态页面 | 支持 | 支持 |
| API 路由 | 支持 | 支持 |
| 配置复杂度 | 简单 | 中等 |
测试或简单应用请选择 **不使用 R2**。需要 ISR 缓存的生产环境应用请选择 **使用 R2**

29
docs/cn/docker.md Normal file
View File

@@ -0,0 +1,29 @@
# 使用 Docker 运行
如果您只是想在本地运行,最好的方式是使用 Docker。
首先,如果您尚未安装 Docker请先安装[获取 Docker](https://docs.docker.com/get-docker/)
然后运行:
```bash
docker run -d -p 3000:3000 \
-e AI_PROVIDER=openai \
-e AI_MODEL=gpt-4o \
-e OPENAI_API_KEY=your_api_key \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
或者使用环境变量文件:
```bash
cp env.example .env
# 编辑 .env 文件并填入您的配置
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
请将环境变量替换为您首选的 AI 提供商配置。查看 [AI 提供商](./ai-providers.md) 了解可用选项。
> **离线部署:** 如果无法访问 `embed.diagrams.net`,请参阅 [离线部署](./offline-deployment.md) 了解配置选项。

View File

@@ -0,0 +1,38 @@
# 离线部署
通过自托管 draw.io 来替代 `embed.diagrams.net`,从而离线部署 Next AI Draw.io。
**注意:** `NEXT_PUBLIC_DRAWIO_BASE_URL` 是一个**构建时**变量。修改它需要重新构建 Docker 镜像。
## Docker Compose 设置
1. 克隆仓库并在 `.env` 文件中定义 API 密钥。
2. 创建 `docker-compose.yml`
```yaml
services:
drawio:
image: jgraph/drawio:latest
ports: ["8080:8080"]
next-ai-draw-io:
build:
context: .
args:
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
ports: ["3000:3000"]
env_file: .env
depends_on: [drawio]
```
3. 运行 `docker compose up -d` 并打开 `http://localhost:3000`
## 配置与重要警告
**`NEXT_PUBLIC_DRAWIO_BASE_URL` 必须是用户浏览器可访问的地址。**
| 场景 | URL 值 |
|----------|-----------|
| 本地主机 (Localhost) | `http://localhost:8080` |
| 远程/服务器 | `http://YOUR_SERVER_IP:8080` |
**切勿使用** Docker 内部别名(如 `http://drawio:8080`),因为浏览器无法解析它们。

236
docs/en/ai-providers.md Normal file
View File

@@ -0,0 +1,236 @@
# AI Provider Configuration
This guide explains how to configure different AI model providers for next-ai-draw-io.
## Quick Start
1. Copy `.env.example` to `.env.local`
2. Set your API key for your chosen provider
3. Set `AI_MODEL` to your desired model
4. Run `npm run dev`
## Supported Providers
### Doubao (ByteDance Volcengine)
> **Free tokens**: Register on the [Volcengine ARK platform](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) to get 500K free tokens for all models!
```bash
DOUBAO_API_KEY=your_api_key
AI_MODEL=doubao-seed-1-8-251215 # or other Doubao model
```
### Google Gemini
```bash
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
AI_MODEL=gemini-2.0-flash
```
Optional custom endpoint:
```bash
GOOGLE_BASE_URL=https://your-custom-endpoint
```
### OpenAI
```bash
OPENAI_API_KEY=your_api_key
AI_MODEL=gpt-4o
```
Optional custom endpoint (for OpenAI-compatible services):
```bash
OPENAI_BASE_URL=https://your-custom-endpoint/v1
```
### Anthropic
```bash
ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514
```
Optional custom endpoint:
```bash
ANTHROPIC_BASE_URL=https://your-custom-endpoint
```
### DeepSeek
```bash
DEEPSEEK_API_KEY=your_api_key
AI_MODEL=deepseek-chat
```
Optional custom endpoint:
```bash
DEEPSEEK_BASE_URL=https://your-custom-endpoint
```
### SiliconFlow (OpenAI-compatible)
```bash
SILICONFLOW_API_KEY=your_api_key
AI_MODEL=deepseek-ai/DeepSeek-V3 # example; use any SiliconFlow model id
```
Optional custom endpoint (defaults to the recommended domain):
```bash
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # or https://api.siliconflow.cn/v1
```
### SGLang
```bash
SGLANG_API_KEY=your_api_key
AI_MODEL=your_model_id
```
Optional custom endpoint:
```bash
SGLANG_BASE_URL=https://your-custom-endpoint/v1
```
### Azure OpenAI
```bash
AZURE_API_KEY=your_api_key
AZURE_RESOURCE_NAME=your-resource-name # Required: your Azure resource name
AI_MODEL=your-deployment-name
```
Or use a custom endpoint instead of resource name:
```bash
AZURE_API_KEY=your_api_key
AZURE_BASE_URL=https://your-resource.openai.azure.com # Alternative to AZURE_RESOURCE_NAME
AI_MODEL=your-deployment-name
```
Optional reasoning configuration:
```bash
AZURE_REASONING_EFFORT=low # Optional: low, medium, high
AZURE_REASONING_SUMMARY=detailed # Optional: none, brief, detailed
```
### AWS Bedrock
```bash
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
```
Note: On AWS (Lambda, EC2 with IAM role), credentials are automatically obtained from the IAM role.
### OpenRouter
```bash
OPENROUTER_API_KEY=your_api_key
AI_MODEL=anthropic/claude-sonnet-4
```
Optional custom endpoint:
```bash
OPENROUTER_BASE_URL=https://your-custom-endpoint
```
### Ollama (Local)
```bash
AI_PROVIDER=ollama
AI_MODEL=llama3.2
```
Optional custom URL:
```bash
OLLAMA_BASE_URL=http://localhost:11434
```
### Vercel AI Gateway
Vercel AI Gateway provides unified access to multiple AI providers through a single API key. This simplifies authentication and allows you to switch between providers without managing multiple API keys.
**Basic Usage (Vercel-hosted Gateway):**
```bash
AI_GATEWAY_API_KEY=your_gateway_api_key
AI_MODEL=openai/gpt-4o
```
**Custom Gateway URL (for local development or self-hosted Gateway):**
```bash
AI_GATEWAY_API_KEY=your_custom_api_key
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
AI_MODEL=openai/gpt-4o
```
Model format uses `provider/model` syntax:
- `openai/gpt-4o` - OpenAI GPT-4o
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
**Configuration notes:**
- If `AI_GATEWAY_BASE_URL` is not set, the default Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`) is used
- Custom base URL is useful for:
- Local development with a custom Gateway instance
- Self-hosted AI Gateway deployments
- Enterprise proxy configurations
- When using a custom base URL, you must also provide `AI_GATEWAY_API_KEY`
Get your API key from the [Vercel AI Gateway dashboard](https://vercel.com/ai-gateway).
## Auto-Detection
If you only configure **one** provider's API key, the system will automatically detect and use that provider. No need to set `AI_PROVIDER`.
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
```bash
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
```
## Model Capability Requirements
This task requires exceptionally strong model capabilities, as it involves generating long-form text with strict formatting constraints (draw.io XML).
**Recommended models**:
- Claude Sonnet 4.5 / Opus 4.5
**Note on Ollama**: While Ollama is supported as a provider, it's generally not practical for this use case unless you're running high-capability models like DeepSeek R1 or Qwen3-235B locally.
## Temperature Setting
You can optionally configure the temperature via environment variable:
```bash
TEMPERATURE=0 # More deterministic output (recommended for diagrams)
```
**Important**: Leave `TEMPERATURE` unset for models that don't support temperature settings, such as:
- GPT-5.1 and other reasoning models
- Some specialized models
When unset, the model uses its default behavior.
## Recommendations
- **Best experience**: Use models with vision support (GPT-4o, Claude, Gemini) for image-to-diagram features
- **Budget-friendly**: DeepSeek offers competitive pricing
- **Privacy**: Use Ollama for fully local, offline operation (requires powerful hardware)
- **Flexibility**: OpenRouter provides access to many models through a single API

View File

@@ -0,0 +1,267 @@
# Deploy on Cloudflare Workers
This project can be deployed as a **Cloudflare Worker** using the **OpenNext adapter**, giving you:
- Global edge deployment
- Very low latency
- Free `workers.dev` hosting
- Full Next.js ISR support via R2 (optional)
> **Important Windows Note:** OpenNext and Wrangler are **not fully reliable on native Windows**. Recommended options:
>
> - Use **GitHub Codespaces** (works perfectly)
> - OR use **WSL (Linux)**
>
> Pure Windows builds may fail due to WASM file path issues.
---
## Prerequisites
1. A **Cloudflare account** (free tier works for basic deployment)
2. **Node.js 18+**
3. **Wrangler CLI** installed (dev dependency is fine):
```bash
npm install -D wrangler
```
4. Cloudflare login:
```bash
npx wrangler login
```
> **Note:** A payment method is only required if you want to enable R2 for ISR caching. Basic Workers deployment is free.
---
## Step 1 — Install dependencies
```bash
npm install
```
---
## Step 2 — Configure environment variables
Cloudflare uses a different file for local testing.
### 1) Create `.dev.vars` (for Cloudflare local + deploy)
```bash
cp env.example .dev.vars
```
Fill in your API keys and configuration.
### 2) Make sure `.env.local` also exists (for regular Next.js dev)
```bash
cp env.example .env.local
```
Fill in the same values there.
---
## Step 3 — Choose your deployment type
### Option A: Deploy WITHOUT R2 (Simple, Free)
If you don't need ISR caching, you can deploy without R2:
**1. Use simple `open-next.config.ts`:**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
export default defineCloudflareConfig({})
```
**2. Use simple `wrangler.jsonc` (without r2_buckets):**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
Skip to **Step 4**.
---
### Option B: Deploy WITH R2 (Full ISR Support)
R2 enables **Incremental Static Regeneration (ISR)** caching. Requires a payment method on your Cloudflare account.
**1. Create an R2 bucket** in the Cloudflare Dashboard:
- Go to **Storage & Databases → R2**
- Click **Create bucket**
- Name it: `next-inc-cache`
**2. Configure `open-next.config.ts`:**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
})
```
**3. Configure `wrangler.jsonc` (with R2):**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "next-inc-cache"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
> **Important:** The `bucket_name` must exactly match the name you created in the Cloudflare dashboard.
---
## Step 4 — Register a workers.dev subdomain (first-time only)
Before your first deployment, you need a workers.dev subdomain.
**Option 1: Via Cloudflare Dashboard (Recommended)**
Visit: https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
**Option 2: During deploy**
When you run `npm run deploy`, Wrangler may prompt:
```
Would you like to register a workers.dev subdomain? (Y/n)
```
Type `Y` and choose a subdomain name.
> **Note:** In CI/CD or non-interactive environments, the prompt won't appear. Register via the dashboard first.
---
## Step 5 — Deploy to Cloudflare
```bash
npm run deploy
```
What the script does:
- Builds the Next.js app
- Converts it to a Cloudflare Worker via OpenNext
- Uploads static assets
- Publishes the Worker
Your app will be available at:
```
https://<worker-name>.<your-subdomain>.workers.dev
```
---
## Common issues & fixes
### `You need to register a workers.dev subdomain`
**Cause:** No workers.dev subdomain registered for your account.
**Fix:** Go to https://dash.cloudflare.com → Workers & Pages → Set up a subdomain.
---
### `Please enable R2 through the Cloudflare Dashboard`
**Cause:** R2 is configured in wrangler.jsonc but not enabled on your account.
**Fix:** Either enable R2 (requires payment method) or use Option A (deploy without R2).
---
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
**Cause:** `r2_buckets` is missing from `wrangler.jsonc`.
**Fix:** Add the `r2_buckets` section or switch to Option A (without R2).
---
### `Can't set compatibility date in the future`
**Cause:** `compatibility_date` in wrangler config is set to a future date.
**Fix:** Change `compatibility_date` to today or an earlier date.
---
### Windows error: `resvg.wasm?module` (ENOENT)
**Cause:** Windows filenames cannot include `?`, but a wasm asset uses `?module` in its filename.
**Fix:** Build/deploy on Linux (WSL, Codespaces, or CI).
---
## Optional: Preview locally
Preview the Worker locally before deploying:
```bash
npm run preview
```
---
## Summary
| Feature | Without R2 | With R2 |
|---------|------------|---------|
| Cost | Free | Requires payment method |
| ISR Caching | No | Yes |
| Static Pages | Yes | Yes |
| API Routes | Yes | Yes |
| Setup Complexity | Simple | Moderate |
Choose **without R2** for testing or simple apps. Choose **with R2** for production apps that need ISR caching.

29
docs/en/docker.md Normal file
View File

@@ -0,0 +1,29 @@
# Run with Docker
If you just want to run it locally, the best way is to use Docker.
First, install Docker if you haven't already: [Get Docker](https://docs.docker.com/get-docker/)
Then run:
```bash
docker run -d -p 3000:3000 \
-e AI_PROVIDER=openai \
-e AI_MODEL=gpt-4o \
-e OPENAI_API_KEY=your_api_key \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
Or use an env file:
```bash
cp env.example .env
# Edit .env with your configuration
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
Open [http://localhost:3000](http://localhost:3000) in your browser.
Replace the environment variables with your preferred AI provider configuration. See [AI Providers](./ai-providers.md) for available options.
> **Offline Deployment:** If `embed.diagrams.net` is blocked, see [Offline Deployment](./offline-deployment.md) for configuration options.

View File

@@ -0,0 +1,39 @@
# Offline Deployment
Deploy Next AI Draw.io offline by self-hosting draw.io to replace `embed.diagrams.net`.
**Note:** `NEXT_PUBLIC_DRAWIO_BASE_URL` is a **build-time** variable. Changing it requires rebuilding the Docker image.
## Docker Compose Setup
1. Clone the repository and define API keys in `.env`.
2. Create `docker-compose.yml`:
```yaml
services:
drawio:
image: jgraph/drawio:latest
ports: ["8080:8080"]
next-ai-draw-io:
build:
context: .
args:
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
ports: ["3000:3000"]
env_file: .env
depends_on: [drawio]
```
3. Run `docker compose up -d` and open `http://localhost:3000`.
## Configuration & Critical Warning
**The `NEXT_PUBLIC_DRAWIO_BASE_URL` must be accessible from the user's browser.**
| Scenario | URL Value |
|----------|-----------|
| Localhost | `http://localhost:8080` |
| Remote/Server | `http://YOUR_SERVER_IP:8080` |
**Do NOT use** internal Docker aliases like `http://drawio:8080`; the browser cannot resolve them.

245
docs/ja/README_JA.md Normal file
View File

@@ -0,0 +1,245 @@
# Next AI Draw.io
<div align="center">
**AI搭載のダイアグラム作成ツール - チャット、描画、可視化**
[English](../../README.md) | [中文](../cn/README_CN.md) | 日本語
[![TrendShift](https://trendshift.io/api/badge/repositories/15449)](https://next-ai-drawio.jiang.jp/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Next.js](https://img.shields.io/badge/Next.js-16.x-black)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19.x-61dafb)](https://react.dev/)
[![Sponsor](https://img.shields.io/badge/Sponsor-❤-ea4aaa)](https://github.com/sponsors/DayuanJiang)
[![Live Demo](../../public/live-demo-button.svg)](https://next-ai-drawio.jiang.jp/)
</div>
AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。
> 注:<img src="https://raw.githubusercontent.com/DayuanJiang/next-ai-draw-io/main/public/doubao-color.png" alt="" height="20" /> [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project) のご支援により、デモサイトに強力な K2-thinking モデルを導入しました!
https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
## 目次
- [Next AI Draw.io](#next-ai-drawio)
- [目次](#目次)
- [](#例)
- [機能](#機能)
- [MCPサーバープレビュー](#mcpサーバープレビュー)
- [Claude Code CLI](#claude-code-cli)
- [はじめに](#はじめに)
- [オンラインで試す](#オンラインで試す)
- [デスクトップアプリケーション](#デスクトップアプリケーション)
- [Dockerで実行](#dockerで実行)
- [インストール](#インストール)
- [デプロイ](#デプロイ)
- [EdgeOne Pagesへのデプロイ](#edgeone-pagesへのデプロイ)
- [Vercelへのデプロイ推奨](#vercelへのデプロイ推奨)
- [Cloudflare Workersへのデプロイ](#cloudflare-workersへのデプロイ)
- [マルチプロバイダーサポート](#マルチプロバイダーサポート)
- [仕組み](#仕組み)
- [サポート&お問い合わせ](#サポートお問い合わせ)
- [スター履歴](#スター履歴)
## 例
以下はいくつかのプロンプト例と生成されたダイアグラムです:
<div align="center">
<table width="100%">
<tr>
<td colspan="2" valign="top" align="center">
<strong>アニメーションTransformerコネクタ</strong><br />
<p><strong>プロンプト:</strong> **アニメーションコネクタ**付きのTransformerアーキテクチャ図を作成してください。</p>
<img src="../../public/animated_connectors.svg" alt="アニメーションコネクタ付きTransformerアーキテクチャ" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>GCPアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **GCPアイコン**を使用してGCPアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/gcp_demo.svg" alt="GCPアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<strong>AWSアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **AWSアイコン**を使用してAWSアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/aws_demo.svg" alt="AWSアーキテクチャ図" width="480" />
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Azureアーキテクチャ図</strong><br />
<p><strong>プロンプト:</strong> **Azureアイコン**を使用してAzureアーキテクチャ図を生成してください。この図では、ユーザーがインスタンス上でホストされているフロントエンドに接続します。</p>
<img src="../../public/azure_demo.svg" alt="Azureアーキテクチャ図" width="480" />
</td>
<td width="50%" valign="top">
<strong>猫のスケッチ</strong><br />
<p><strong>プロンプト:</strong> かわいい猫を描いてください。</p>
<img src="../../public/cat_demo.svg" alt="猫の絵" width="240" />
</td>
</tr>
</table>
</div>
## 機能
- **LLM搭載のダイアグラム作成**大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作
- **画像ベースのダイアグラム複製**既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化
- **PDFとテキストファイルのアップロード**PDFドキュメントやテキストファイルをアップロードして、既存のドキュメントからコンテンツを抽出し、ダイアグラムを生成
- **AI推論プロセス表示**サポートされているモデルOpenAI o1/o3、Gemini、ClaudeなどのAIの思考プロセスを表示
- **ダイアグラム履歴**すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能
- **インタラクティブなチャットインターフェース**AIとリアルタイムでコミュニケーションしてダイアグラムを改善
- **クラウドアーキテクチャダイアグラムサポート**クラウドアーキテクチャダイアグラムの生成を専門的にサポートAWS、GCP、Azure
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
## MCPサーバープレビュー
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
MCPModel Context Protocolを介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。
```json
{
"mcpServers": {
"drawio": {
"command": "npx",
"args": ["@next-ai-drawio/mcp-server@latest"]
}
}
}
```
### Claude Code CLI
```bash
claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
```
Claudeにダイアグラムの作成を依頼
> 「ログイン、MFA、セッション管理を含むユーザー認証のフローチャートを作成してください」
ダイアグラムがリアルタイムでブラウザに表示されます!
詳細は[MCPサーバーREADME](../../packages/mcp-server/README.md)をご覧くださいVS Code、Cursorなどのクライアント設定も含む
## はじめに
### オンラインで試す
インストール不要!デモサイトで直接お試しください:
[![Live Demo](../../public/live-demo-button.svg)](https://next-ai-drawio.jiang.jp/)
> **自分のAPIキーを使用**自分のAPIキーを使用することで、デモサイトの利用制限を回避できます。チャットパネルの設定アイコンをクリックして、プロバイダーとAPIキーを設定してください。キーはブラウザのローカルに保存され、サーバーには保存されません。
### デスクトップアプリケーション
[Releases ページ](https://github.com/DayuanJiang/next-ai-draw-io/releases)からお使いのプラットフォーム用のネイティブデスクトップアプリをダウンロードしてください:
対応プラットフォームWindows、macOS、Linux。
### Dockerで実行
[Docker ガイドを参照](./docker.md)
### インストール
1. リポジトリをクローン:
```bash
git clone https://github.com/DayuanJiang/next-ai-draw-io
cd next-ai-draw-io
npm install
cp env.example .env.local
```
詳細な設定手順については[プロバイダー設定ガイド](./ai-providers.md)を参照してください。
2. 開発サーバーを起動:
```bash
npm run dev
```
3. ブラウザで[http://localhost:6002](http://localhost:6002)を開いてアプリケーションを確認。
## デプロイ
### EdgeOne Pagesへのデプロイ
[Tencent EdgeOne Pages](https://pages.edgeone.ai/)を使用してワンクリックでデプロイできます。
このボタンでデプロイ:
[![Deploy to EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
詳細は[Tencent EdgeOne Pagesドキュメント](https://pages.edgeone.ai/document/deployment-overview)をご覧ください。
また、Tencent EdgeOne Pagesでデプロイすると、[DeepSeekモデルの毎日の無料クォータ](https://pages.edgeone.ai/document/edge-ai)が付与されます。
### Vercelへのデプロイ推奨
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDayuanJiang%2Fnext-ai-draw-io)
Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成者による[Vercelプラットフォーム](https://vercel.com/new)を使用することです。ローカルの`.env.local`ファイルと同様に、Vercelダッシュボードで**環境変数を設定**してください。
詳細は[Next.jsデプロイメントドキュメント](https://nextjs.org/docs/app/building-your-application/deploying)をご覧ください。
### Cloudflare Workersへのデプロイ
[Cloudflare デプロイガイドを参照](./cloudflare-deploy.md)
## マルチプロバイダーサポート
- [ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)
- AWS Bedrockデフォルト
- OpenAI
- Anthropic
- Google AI
- Azure OpenAI
- Ollama
- OpenRouter
- DeepSeek
- SiliconFlow
- SGLang
- Vercel AI Gateway
AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタムエンドポイントをサポートしています。
📖 **[詳細なプロバイダー設定ガイド](./ai-providers.md)** - 各プロバイダーの設定手順をご覧ください。
**モデル要件**このタスクは厳密なフォーマット制約draw.io XMLを持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro、DeepSeek V3.2/R1を推奨します。
注:`claude`シリーズはAWS、Azure、GCPなどのクラウドアーキテクチャロゴ付きのdraw.ioダイアグラムで学習されているため、クラウドアーキテクチャダイアグラムを作成したい場合は最適な選択です。
## 仕組み
本アプリケーションは以下の技術を使用しています:
- **Next.js**:フロントエンドフレームワークとルーティング
- **Vercel AI SDK**`ai` + `@ai-sdk/*`ストリーミングAIレスポンスとマルチプロバイダーサポート
- **react-drawio**:ダイアグラムの表現と操作
ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。
## サポート&お問い合わせ
**デモサイトのAPIトークン使用を支援してくださった[ByteDance Doubao](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に特別な感謝を申し上げます!** ARKプラットフォームに登録すると、50万トークンが無料でもらえます
このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために[スポンサー](https://github.com/sponsors/DayuanJiang)をご検討ください!
サポートやお問い合わせについては、GitHubリポジトリでissueを開くか、メンテナーにご連絡ください
- メールme[at]jiang.jp
## スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=DayuanJiang/next-ai-draw-io&type=date&legend=top-left)](https://www.star-history.com/#DayuanJiang/next-ai-draw-io&type=date&legend=top-left)
---

236
docs/ja/ai-providers.md Normal file
View File

@@ -0,0 +1,236 @@
# AIプロバイダーの設定
このガイドでは、next-ai-draw-io でさまざまな AI モデルプロバイダーを設定する方法について説明します。
## クイックスタート
1. `.env.example``.env.local` にコピーします
2. 選択したプロバイダーの API キーを設定します
3. `AI_MODEL` を希望のモデルに設定します
4. `npm run dev` を実行します
## 対応プロバイダー
### Doubao (ByteDance Volcengine)
> **無料トークン**: [Volcengine ARK プラットフォーム](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new&utm_campaign=doubao&utm_content=aidrawio&utm_medium=github&utm_source=coopensrc&utm_term=project)に登録すると、すべてのモデルで使える50万トークンが無料で入手できます
```bash
DOUBAO_API_KEY=your_api_key
AI_MODEL=doubao-seed-1-8-251215 # または他の Doubao モデル
```
### Google Gemini
```bash
GOOGLE_GENERATIVE_AI_API_KEY=your_api_key
AI_MODEL=gemini-2.0-flash
```
任意のカスタムエンドポイント:
```bash
GOOGLE_BASE_URL=https://your-custom-endpoint
```
### OpenAI
```bash
OPENAI_API_KEY=your_api_key
AI_MODEL=gpt-4o
```
任意のカスタムエンドポイントOpenAI 互換サービス用):
```bash
OPENAI_BASE_URL=https://your-custom-endpoint/v1
```
### Anthropic
```bash
ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514
```
任意のカスタムエンドポイント:
```bash
ANTHROPIC_BASE_URL=https://your-custom-endpoint
```
### DeepSeek
```bash
DEEPSEEK_API_KEY=your_api_key
AI_MODEL=deepseek-chat
```
任意のカスタムエンドポイント:
```bash
DEEPSEEK_BASE_URL=https://your-custom-endpoint
```
### SiliconFlow (OpenAI 互換)
```bash
SILICONFLOW_API_KEY=your_api_key
AI_MODEL=deepseek-ai/DeepSeek-V3 # 例; 任意の SiliconFlow モデル ID を使用
```
任意のカスタムエンドポイント(デフォルトは推奨ドメイン):
```bash
SILICONFLOW_BASE_URL=https://api.siliconflow.com/v1 # または https://api.siliconflow.cn/v1
```
### SGLang
```bash
SGLANG_API_KEY=your_api_key
AI_MODEL=your_model_id
```
任意のカスタムエンドポイント:
```bash
SGLANG_BASE_URL=https://your-custom-endpoint/v1
```
### Azure OpenAI
```bash
AZURE_API_KEY=your_api_key
AZURE_RESOURCE_NAME=your-resource-name # 必須: Azure リソース名
AI_MODEL=your-deployment-name
```
またはリソース名の代わりにカスタムエンドポイントを使用:
```bash
AZURE_API_KEY=your_api_key
AZURE_BASE_URL=https://your-resource.openai.azure.com # AZURE_RESOURCE_NAME の代替
AI_MODEL=your-deployment-name
```
任意の推論設定:
```bash
AZURE_REASONING_EFFORT=low # 任意: low, medium, high
AZURE_REASONING_SUMMARY=detailed # 任意: none, brief, detailed
```
### AWS Bedrock
```bash
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AI_MODEL=anthropic.claude-sonnet-4-5-20250514-v1:0
```
注: AWS 上IAM ロールを持つ Lambda や EC2では、認証情報は IAM ロールから自動的に取得されます。
### OpenRouter
```bash
OPENROUTER_API_KEY=your_api_key
AI_MODEL=anthropic/claude-sonnet-4
```
任意のカスタムエンドポイント:
```bash
OPENROUTER_BASE_URL=https://your-custom-endpoint
```
### Ollama (ローカル)
```bash
AI_PROVIDER=ollama
AI_MODEL=llama3.2
```
任意のカスタム URL:
```bash
OLLAMA_BASE_URL=http://localhost:11434
```
### Vercel AI Gateway
Vercel AI Gateway は、単一の API キーで複数の AI プロバイダーへの統合アクセスを提供します。これにより認証が簡素化され、複数の API キーを管理することなくプロバイダーを切り替えることができます。
**基本的な使用法 (Vercel ホストの Gateway):**
```bash
AI_GATEWAY_API_KEY=your_gateway_api_key
AI_MODEL=openai/gpt-4o
```
**カスタム Gateway URL (ローカル開発またはセルフホスト Gateway 用):**
```bash
AI_GATEWAY_API_KEY=your_custom_api_key
AI_GATEWAY_BASE_URL=https://your-custom-gateway.com/v1/ai
AI_MODEL=openai/gpt-4o
```
モデル形式は `provider/model` 構文を使用します:
- `openai/gpt-4o` - OpenAI GPT-4o
- `anthropic/claude-sonnet-4-5` - Anthropic Claude Sonnet 4.5
- `google/gemini-2.0-flash` - Google Gemini 2.0 Flash
**設定に関する注意点:**
- `AI_GATEWAY_BASE_URL` が設定されていない場合、デフォルトの Vercel Gateway URL (`https://ai-gateway.vercel.sh/v1/ai`) が使用されます
- カスタムベース URL は以下の場合に便利です:
- カスタム Gateway インスタンスを使用したローカル開発
- セルフホスト AI Gateway デプロイメント
- エンタープライズプロキシ設定
- カスタムベース URL を使用する場合、`AI_GATEWAY_API_KEY` も指定する必要があります
[Vercel AI Gateway ダッシュボード](https://vercel.com/ai-gateway)から API キーを取得してください。
## 自動検出
**1つ**のプロバイダーの API キーのみを設定した場合、システムはそのプロバイダーを自動的に検出して使用します。`AI_PROVIDER` を設定する必要はありません。
**複数**の API キーを設定する場合は、`AI_PROVIDER` を明示的に設定する必要があります:
```bash
AI_PROVIDER=google # または: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
```
## モデル性能要件
このタスクは、厳密なフォーマット制約draw.io XMLを伴う長文テキストの生成を含むため、非常に強力なモデル性能が必要です。
**推奨モデル**:
- Claude Sonnet 4.5 / Opus 4.5
**Ollama に関する注意**: Ollama はプロバイダーとしてサポートされていますが、DeepSeek R1 や Qwen3-235B のような高性能モデルをローカルで実行していない限り、このユースケースでは一般的に実用的ではありません。
## Temperature温度設定
環境変数で Temperature を任意に設定できます:
```bash
TEMPERATURE=0 # より決定論的な出力(ダイアグラムに推奨)
```
**重要**: 以下の Temperature 設定をサポートしていないモデルでは、`TEMPERATURE` を未設定のままにしてください:
- GPT-5.1 およびその他の推論モデル
- 一部の特殊なモデル
未設定の場合、モデルはデフォルトの挙動を使用します。
## 推奨事項
- **最高の体験**: 画像からダイアグラムを生成する機能には、ビジョン画像認識をサポートするモデルGPT-4o, Claude, Geminiを使用してください
- **低コスト**: DeepSeek は競争力のある価格を提供しています
- **プライバシー**: 完全にローカルなオフライン操作には Ollama を使用してください(強力なハードウェアが必要です)
- **柔軟性**: OpenRouter は単一の API で多数のモデルへのアクセスを提供します

View File

@@ -0,0 +1,267 @@
# Cloudflare Workers へのデプロイ
このプロジェクトは **OpenNext アダプター** を使用して **Cloudflare Worker** としてデプロイすることができ、以下のメリットがあります:
- グローバルエッジへのデプロイ
- 超低レイテンシー
- 無料の `workers.dev` ホスティング
- R2 を介した完全な Next.js ISR サポート(オプション)
> **Windows ユーザー向けの重要な注意:** OpenNext と Wrangler は、**ネイティブ Windows 環境では完全には信頼できません**。以下の方法を推奨します:
>
> - **GitHub Codespaces** を使用する(完全に動作します)
> - または **WSL (Linux)** を使用する
>
> 純粋な Windows 環境でのビルドは、WASM ファイルパスの問題により失敗する可能性があります。
---
## 前提条件
1. **Cloudflare アカウント**(基本的なデプロイには無料プランで十分です)
2. **Node.js 18以上**
3. **Wrangler CLI** のインストール(開発依存関係で問題ありません):
```bash
npm install -D wrangler
```
4. Cloudflare へのログイン:
```bash
npx wrangler login
```
> **注意:** 支払い方法の登録が必要なのは、ISR キャッシュのために R2 を有効にする場合のみです。基本的な Workers へのデプロイは無料です。
---
## ステップ 1 — 依存関係のインストール
```bash
npm install
```
---
## ステップ 2 — 環境変数の設定
Cloudflare はローカルテスト用に別のファイルを使用します。
### 1) `.dev.vars` の作成Cloudflare ローカルおよびデプロイ用)
```bash
cp env.example .dev.vars
```
API キーと設定を入力してください。
### 2) `.env.local` も存在することを確認(通常の Next.js 開発用)
```bash
cp env.example .env.local
```
同じ値を入力してください。
---
## ステップ 3 — デプロイタイプの選択
### オプション A: R2 なしでのデプロイ(シンプル、無料)
ISR キャッシュが不要な場合は、R2 なしでデプロイできます:
**1. シンプルな `open-next.config.ts` を使用:**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
export default defineCloudflareConfig({})
```
**2. シンプルな `wrangler.jsonc` を使用r2_buckets なし):**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
**ステップ 4** へ進んでください。
---
### オプション B: R2 ありでのデプロイ(完全な ISR サポート)
R2 を使用すると **Incremental Static Regeneration (ISR)** キャッシュが有効になります。これには Cloudflare アカウントに支払い方法の登録が必要です。
**1. R2 バケットの作成**Cloudflare ダッシュボードにて):
- **Storage & Databases → R2** へ移動
- **Create bucket** をクリック
- 名前を入力: `next-inc-cache`
**2. `open-next.config.ts` の設定:**
```ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
})
```
**3. `wrangler.jsonc` の設定R2 あり):**
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "next-ai-draw-io-worker",
"compatibility_date": "2025-12-08",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "next-inc-cache"
}
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "next-ai-draw-io-worker"
}
]
}
```
> **重要:** `bucket_name` は Cloudflare ダッシュボードで作成した名前と完全に一致させる必要があります。
---
## ステップ 4 — workers.dev サブドメインの登録(初回のみ)
初回デプロイの前に、workers.dev サブドメインが必要です。
**オプション 1: Cloudflare ダッシュボード経由(推奨)**
アクセス先: https://dash.cloudflare.com → Workers & Pages → Overview → Set up a subdomain
**オプション 2: デプロイ時**
`npm run deploy` を実行した際、Wrangler が以下のように尋ねてくる場合があります:
```
Would you like to register a workers.dev subdomain? (Y/n)
```
`Y` を入力し、サブドメイン名を選択してください。
> **注意:** CI/CD や非対話型環境では、このプロンプトは表示されません。事前にダッシュボードで登録してください。
---
## ステップ 5 — Cloudflare へのデプロイ
```bash
npm run deploy
```
スクリプトの処理内容:
- Next.js アプリのビルド
- OpenNext を介した Cloudflare Worker への変換
- 静的アセットのアップロード
- Worker の公開
アプリは以下の URL で利用可能になります:
```
https://<worker-name>.<your-subdomain>.workers.dev
```
---
## よくある問題と解決策
### `You need to register a workers.dev subdomain`
**原因:** アカウントに workers.dev サブドメインが登録されていません。
**解決策:** https://dash.cloudflare.com → Workers & Pages → Set up a subdomain から登録してください。
---
### `Please enable R2 through the Cloudflare Dashboard`
**原因:** `wrangler.jsonc` で R2 が設定されていますが、アカウントで R2 が有効になっていません。
**解決策:** R2 を有効にする(支払い方法が必要)か、オプション AR2 なしでデプロイ)を使用してください。
---
### `No R2 binding "NEXT_INC_CACHE_R2_BUCKET" found`
**原因:** `wrangler.jsonc``r2_buckets` がありません。
**解決策:** `r2_buckets` セクションを追加するか、オプション AR2 なし)に切り替えてください。
---
### `Can't set compatibility date in the future`
**原因:** wrangler 設定の `compatibility_date` が未来の日付に設定されています。
**解決策:** `compatibility_date` を今日またはそれ以前の日付に変更してください。
---
### Windows エラー: `resvg.wasm?module` (ENOENT)
**原因:** Windows のファイル名には `?` を含めることができませんが、wasm アセットのファイル名に `?module` が使用されているためです。
**解決策:** Linux 環境WSL、Codespaces、または CIでビルド/デプロイしてください。
---
## オプション: ローカルでのプレビュー
デプロイ前に Worker をローカルでプレビューできます:
```bash
npm run preview
```
---
## まとめ
| 機能 | R2 なし | R2 あり |
|---------|------------|---------|
| コスト | 無料 | 支払い方法が必要 |
| ISR キャッシュ | なし | あり |
| 静的ページ | あり | あり |
| API ルート | あり | あり |
| 設定の複雑さ | シンプル | 普通 |
テストやシンプルなアプリには **R2 なし** を選んでください。ISR キャッシュが必要な本番アプリには **R2 あり** を選んでください。

29
docs/ja/docker.md Normal file
View File

@@ -0,0 +1,29 @@
# Dockerで実行する
ローカルで実行したいだけであれば、Dockerを使用するのが最も良い方法です。
まず、Dockerがまだインストールされていない場合はインストールしてください: [Dockerを入手](https://docs.docker.com/get-docker/)
次に、以下を実行します。
```bash
docker run -d -p 3000:3000 \
-e AI_PROVIDER=openai \
-e AI_MODEL=gpt-4o \
-e OPENAI_API_KEY=your_api_key \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
または、envファイルを使用します。
```bash
cp env.example .env
# .envを構成に合わせて編集します
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
ブラウザで[http://localhost:3000](http://localhost:3000)を開きます。
環境変数は、お好みのAIプロバイダー設定に置き換えてください。利用可能なオプションについては、[AIプロバイダー](./ai-providers.md)を参照してください。
> **オフラインデプロイ:** `embed.diagrams.net`がブロックされている場合は、構成オプションについて[オフラインデプロイ](./offline-deployment.md)を参照してください。

View File

@@ -0,0 +1,38 @@
# オフラインデプロイ
`embed.diagrams.net` の代わりに draw.io をセルフホストすることで、Next AI Draw.io をオフライン環境にデプロイできます。
**注:** `NEXT_PUBLIC_DRAWIO_BASE_URL` は**ビルド時**の変数です。これを変更する場合は、Docker イメージの再ビルドが必要です。
## Docker Compose のセットアップ
1. リポジトリをクローンし、`.env` ファイルに API キーを定義します。
2. `docker-compose.yml` を作成します。
```yaml
services:
drawio:
image: jgraph/drawio:latest
ports: ["8080:8080"]
next-ai-draw-io:
build:
context: .
args:
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
ports: ["3000:3000"]
env_file: .env
depends_on: [drawio]
```
3. `docker compose up -d` を実行し、`http://localhost:3000` にアクセスします。
## 設定と重要な警告
**`NEXT_PUBLIC_DRAWIO_BASE_URL` は、ユーザーのブラウザからアクセスできる必要があります。**
| シナリオ | URL の値 |
|----------|-----------|
| ローカルホスト | `http://localhost:8080` |
| リモート/サーバー | `http://YOUR_SERVER_IP:8080` |
**`http://drawio:8080` のような Docker 内部のエイリアスは絶対に使用しないでください。** ブラウザはこれらを名前解決できません。

View File

@@ -0,0 +1,78 @@
# Draw.io Shape Libraries
Reference: `style="shape=mxgraph.<library>.<shape_name>"`
## Cloud Providers
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| aws4 | 1031 | `mxgraph.aws4` | Amazon Web Services (2025) - EC2, S3, Lambda, RDS, etc. | [aws4.md](./aws4.md) |
| azure2 | 608 | `img/lib/azure2/` | Microsoft Azure (2024) - VMs, Storage, AI, Networking, etc. | [azure2.md](./azure2.md) |
| gcp2 | 297 | `mxgraph.gcp2` | Google Cloud Platform - Compute Engine, BigQuery, GKE, etc. | [gcp2.md](./gcp2.md) |
| alibaba_cloud | 273 | `mxgraph.alibaba_cloud` | Alibaba Cloud - ECS, OSS, RDS, SLB, VPC, etc. | [alibaba_cloud.md](./alibaba_cloud.md) |
| openstack | 18 | `mxgraph.openstack` | OpenStack cloud platform icons | [openstack.md](./openstack.md) |
| digitalocean | 74 | `mxgraph.digitalocean` | DigitalOcean - Droplets, Spaces, Kubernetes, etc. | [digitalocean.md](./digitalocean.md) |
| salesforce | 96 | `mxgraph.salesforce` | Salesforce platform icons | [salesforce.md](./salesforce.md) |
## Networking & Infrastructure
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| cisco19 | 232 | `mxgraph.cisco19` | Cisco network equipment - routers, switches, firewalls | [cisco19.md](./cisco19.md) |
| network | 58 | `mxgraph.networks` | General network diagram symbols | [network.md](./network.md) |
| arista | 45 | `mxgraph.arista` | Arista network switches and equipment | [arista.md](./arista.md) |
| kubernetes | 40 | `mxgraph.kubernetes` | Kubernetes - pods, services, deployments, nodes | [kubernetes.md](./kubernetes.md) |
| vvd | 93 | `mxgraph.vvd` | VMware Validated Design icons | [vvd.md](./vvd.md) |
| rack | 11 | `mxgraph.rack` | Server rack and data center equipment | [rack.md](./rack.md) |
## Business Process
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| bpmn | 39 | `mxgraph.bpmn` | Business Process Model and Notation - events, gateways, tasks | [bpmn.md](./bpmn.md) |
| eip | 36 | `mxgraph.eip` | Enterprise Integration Patterns - messaging, routing | [eip.md](./eip.md) |
| lean_mapping | 13 | `mxgraph.lean_mapping` | Lean/Value Stream Mapping symbols | [lean_mapping.md](./lean_mapping.md) |
## General Diagrams
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| flowchart | 34 | `mxgraph.flowchart` | Standard flowchart symbols - process, decision, data | [flowchart.md](./flowchart.md) |
| basic | 30 | `mxgraph.basic` | Basic shapes - stars, banners, callouts, hearts | [basic.md](./basic.md) |
| arrows2 | 34 | `mxgraph.arrows2` | Arrow shapes and connectors | [arrows2.md](./arrows2.md) |
| infographic | 29 | `mxgraph.infographic` | Infographic elements - charts, icons, badges | [infographic.md](./infographic.md) |
| sitemap | 50 | `mxgraph.sitemap` | Website sitemap icons - pages, forms, navigation | [sitemap.md](./sitemap.md) |
## UI/Mockups
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| android | 17 | `mxgraph.android` | Android UI mockup components | [android.md](./android.md) |
## Enterprise Software
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| citrix | 97 | `mxgraph.citrix` | Citrix virtualization - XenApp, XenDesktop, NetScaler | [citrix.md](./citrix.md) |
| sap | 98 | `mxgraph.sap` | SAP enterprise software icons | [sap.md](./sap.md) |
| mscae | 73 | `mxgraph.mscae` | Microsoft Cloud and Enterprise symbols | [mscae.md](./mscae.md) |
| atlassian | 26 | `mxgraph.atlassian` | Atlassian - Jira, Confluence issue types | [atlassian.md](./atlassian.md) |
## Engineering
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| fluidpower | 246 | `mxgraph.fluid_power` | Hydraulic/pneumatic engineering symbols | [fluidpower.md](./fluidpower.md) |
| electrical | 50 | `mxgraph.electrical` | Electrical circuit symbols - resistors, capacitors | [electrical.md](./electrical.md) |
| pid | 18 | `mxgraph.pid2` | Piping and Instrumentation Diagram symbols | [pid.md](./pid.md) |
| cabinets | 53 | `mxgraph.cabinets` | Electrical cabinet components - breakers, terminals | [cabinets.md](./cabinets.md) |
| floorplan | 44 | `mxgraph.floorplan` | Floor plan furniture and fixtures | [floorplan.md](./floorplan.md) |
## Icons & Graphics
| Library | Shapes | Prefix | Description | File |
|---------|--------|--------|-------------|------|
| webicons | 176 | `mxgraph.webicons` | Web/social media logos - GitHub, Twitter, AWS, etc. | [webicons.md](./webicons.md) |
| un-ocha-icons | 242 | `mxgraph.un-ocha-icons` | UN OCHA humanitarian icons | [un-ocha-icons.md](./un-ocha-icons.md) |
**Total: 33 libraries, 4,281 shapes**

View File

@@ -0,0 +1,328 @@
# alibaba_cloud
**Type:** mxgraph shapes
**Prefix:** `mxgraph.alibaba_cloud`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.alibaba_cloud.{shape};fillColor=#FF6A00;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (311)
- `abap_business_application_platform`
- `acms_application_configuration_manangement`
- `acr_cloud_container_registry`
- `actiontrail`
- `adam_advanced_database_and_application_migration`
- `adb_analyticdb_for_mysql`
- `address_purification`
- `afs_fraud_service`
- `agw_aligateway`
- `ahas_application_high_availability_service`
- `airec_artificial_intelligence_recommendation`
- `alb_application_load_balancer_01`
- `alb_application_load_balancer_02`
- `alibaba_cloud_logo`
- `alibaba_cloud_logo_chinese`
- `alibaba_cloud_logo_english`
- `alimail`
- `alimt_machine_translation`
- `aliyun_linux`
- `amqp_advanced_message_queuing_protocol`
- `amscloudapp`
- `analyticdb_for_postgresql`
- `antibot`
- `apigateway`
- `apsara_file_storage_for_hdfs`
- `apsaravideo_vod`
- `arms_application_real-time_monitoring_service`
- `ask_ack_container_service_for_kubernetes`
- `asm_service_mesh`
- `assettech`
- `avds_vulnerability_db_scanning`
- `baas_blockchain_as_a_service`
- `bandwidth_bag`
- `bastionhost`
- `batchcompute`
- `bccluster`
- `beebot`
- `beian`
- `bizdevops`
- `bizworks`
- `bpstudio`
- `cas_ssl_central_authentication_service`
- `cassandra_wide-column_database_01`
- `cassandra_wide-column_database_02`
- `ccc_cloud_call_center`
- `ccn_cloud_connect_network`
- `ccs_customer_service_01`
- `ccs_customer_service_02`
- `cddc_cloud_database_dedicated_cluster`
- `cdn_content_distribution_network`
- `cdp_cloudera_cdp`
- `cdt_cloud_datatransfer`
- `cen_cloud_enterprise_network`
- `cfw_cloud_firewall`
- `cityvisual`
- `clb_classic_load_balancer_01`
- `clb_classic_load_balancer_02`
- `clickhouse`
- `cloud_auth`
- `cloud_config`
- `cloud_display`
- `cloud_governance_center`
- `cloud_security_center`
- `cloud_shield`
- `cloudap`
- `cloudbox`
- `clouddesktop`
- `clouddev`
- `cloudphoto`
- `cloudproc`
- `cloudshell`
- `cmn_cloud_managed_network`
- `cmp_cloud_mobile_push`
- `cms_cloud_monitor_service`
- `codepipeline`
- `codestore`
- `companyreg`
- `computenest`
- `content_security`
- `coo`
- `cpns_cell_phone_number_service`
- `csas_cloud_security_access_service`
- `cvc_cloud_video_conferencing`
- `cwh_cloud_web_hosting`
- `das_database_autonomy_service`
- `databot`
- `datahub`
- `dataphin`
- `dataquotient`
- `datav`
- `dataworks_dataide`
- `dbaudit`
- `dbes_database_expert_service`
- `dbfs_database_file_system`
- `dbs_database_backup`
- `dcdn_dynamic_route_for_cdn`
- `ddh_dedicated_host`
- `ddos-bgp`
- `ddos-dip`
- `ddos-pro`
- `ddos_protection`
- `devops`
- `dg_database_gateway`
- `directmail`
- `disk_block_storage`
- `dlf_data_lake_formation`
- `dms_data_management_service`
- `dns_domain_name_system`
- `dns_privatezone_01`
- `dns_privatezone_02`
- `domain`
- `domain_and_website`
- `drds_distribute_relational_database_service`
- `dsi_data_security_insurance`
- `dts_data_transmission_service`
- `e-mapreduce`
- `eais_elastic_accelerated_computing_instances`
- `eci_elastic_container_instance`
- `ecs_elastic_compute_service`
- `edas_enterprise_distributed_application_service`
- `ehpc_elastic_high_performance_computing`
- `eip_elastic_ip_address`
- `elastic_web_hosting`
- `elasticsearch`
- `emas_enterprise_mobile_application_studio`
- `energyexpert`
- `ens_edge_node_service`
- `enterprise_website`
- `eprofile`
- `esign`
- `ess_elastic_scaling_service`
- `eventbridge`
- `express_connect`
- `face_recognition`
- `fc_function_compute`
- `flow_service`
- `flowbag`
- `fnf_serverless_function_flow`
- `fpga_field_programmable_gate_array`
- `fraud_detection`
- `ga_global_accelerator`
- `gameshield`
- `gdb_graph_database`
- `graphanalytics`
- `graphcompute`
- `gtm_global_traffic_manager`
- `gts_global_transaction_service`
- `gws_graphic_workstation`
- `havip_high-availability_virtual_ip_address`
- `hbase`
- `hbr_hybrid_backup_recovery`
- `hcs-hgw_hybrid_cloud_storage_array`
- `hcs-mgw_hybrid_cloud_storage_datatransport`
- `hcs-sgw_hybrid_cloud_storage_gateway`
- `hdr_hybrid_disaster_recovery`
- `hologres`
- `holowatcher`
- `hsm_hardware_security_module`
- `httpdns`
- `idrsservice`
- `image_recognition`
- `imagesearch`
- `imarketing`
- `imm_intelligent_media_management`
- `imp_intelligent_media_production`
- `imp_low_code_video_factory`
- `indvi_industrial_visual_intelligence`
- `intelligent_advisor`
- `iot_internet_of_things_platform`
- `iot_wireless_connection_service`
- `iotid_identity`
- `iov_iot_vehicle_cloud`
- `ipv6_gateway`
- `isoc_iot_security_operations_center`
- `isu_intelligent_semantic_understanding`
- `ivision`
- `ivpd_intelligent_visual_production`
- `kafka`
- `linkedmall`
- `linkwan`
- `live`
- `livinglink`
- `log_streaming`
- `logic_composer`
- `machine_learning`
- `man_mobile_analytics`
- `mariadb`
- `mas_mobile_acceleration_service`
- `maxcompute`
- `memcache`
- `miniappdev`
- `mns_message_service`
- `mobile_hotfix`
- `mobsec`
- `mongodb`
- `mps-ai`
- `mps-censor`
- `mps-cover`
- `mps-dna`
- `mps-multimod`
- `mps-produce`
- `mps_apsaravideo_media_processing`
- `mq_message_queue`
- `mqc_mobile_quality_center`
- `mse_microservices_engine`
- `multi-cloud_finops`
- `multi-mode_database_lindorm`
- `multimediaai`
- `mxgraph.alibaba_cloud`
- `mysql`
- `nas_network_attached_storage`
- `nat_gateway`
- `network_acl_access_control_list`
- `nlb_network_load_balancer_01`
- `nlb_network_load_balancer_02`
- `nlp-address`
- `nlp-automl`
- `nlp-ie_text_information_extraction`
- `nlp-ke_keyword_extraction`
- `nlp-ner_named_entity_recognition`
- `nlp-pos_part-of-speech_tagging`
- `nlp-ra_reflexive_anaphora`
- `nlp-sa_sentiment_analysis`
- `nlp-tc_text_categorization`
- `nlp-ws_word_segmentation`
- `nlp_natural_language_processing`
- `nls`
- `nls-asrbag`
- `nls-asrcustommodel`
- `nls-filebag`
- `nls-service`
- `nls-shortasrbag`
- `nls-ttsbag`
- `nodejs_performance_platform`
- `oceanbase`
- `ocr_optical_character_recognition`
- `onsmqtt_micro_message_queuing_telemetry_transport`
- `oos_operation_orchestration_service`
- `openanalytics`
- `openapi_explorer`
- `opensearch`
- `oss_object_storage_service`
- `ots_tablestore`
- `outboundbot`
- `pcdn_p2p_cdn`
- `petadata_hybriddb_for_mysql`
- `physical_connection`
- `pnvs_phone_number_verification_service`
- `polardb`
- `porana_portrait_analysis`
- `postgresql`
- `ppas_pay-as-you-go_database`
- `privatelink`
- `prometheus`
- `prophet`
- `pts_performance_test_service`
- `quickbi`
- `ram_resource_access_management`
- `re_recommendation_engine`
- `realtime_compute`
- `redis_kvstore`
- `region`
- `retailir`
- `ros_resource_orchestration_service`
- `route_table`
- `router`
- `rsimganalys`
- `rtc_real-time_communication`
- `sae_serverless_app_engine`
- `sag_smart_access_gateway_01`
- `sag_smart_access_gateway_02`
- `sas_situational_awareness`
- `sca_smart_conversation_analysis_01`
- `sca_smart_conversation_analysis_02`
- `scc_super_computing_cluster`
- `scdn_secure_cdn`
- `scu_storage_capacity_unit`
- `sddp_sensitive_data_protection`
- `shared_bandwidth`
- `shared_flow_bag`
- `shc_shield_hybrid_cloud`
- `slb_server_load_balancer_01`
- `slb_server_load_balancer_02`
- `slb_server_load_balancer_03`
- `sls_simple_log_service`
- `smc_server_migration_center`
- `sms_short_message_service`
- `sos`
- `spark_data_insights`
- `sppc`
- `sqlserver`
- `swas_simple_application_server`
- `tr_transit_router`
- `trademark_service`
- `uis_ultimate_internet_service`
- `user`
- `user_feedback_01`
- `user_feedback_02`
- `vbr_virtual_border_router`
- `vcs_visual_computing_service`
- `vms_voice_messaging_service`
- `voicebot_intelligent_voice_navigation`
- `vpc_virtual_private_cloud`
- `vpn_gateway`
- `vs_video_surveillance`
- `vswitch`
- `waf_web_application_firewall`
- `webplus_web_app_service`
- `xdragon_bare_metal_server`
- `xtrace`
- `yida`

View File

@@ -0,0 +1,62 @@
# android
**Type:** mxgraph shapes
**Prefix:** `mxgraph.android`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.android.phone2;strokeColor=#c0c0c0;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="200" height="390" as="geometry" />
</mxCell>
```
## Shapes (47)
- `action_bar`
- `action_bar_landscape`
- `anchor`
- `checkbox`
- `contact_badge_focused`
- `contextual_action_bar`
- `contextual_action_bar_landscape`
- `contextual_split_action_bar`
- `contextual_split_action_bar_landscape`
- `contextual_split_action_bar_landscape_white`
- `indeterminateSpinner`
- `indeterminate_progress_bar`
- `keyboard`
- `navigation_bar_1`
- `navigation_bar_1_landscape`
- `navigation_bar_1_vertical`
- `navigation_bar_2`
- `navigation_bar_3`
- `navigation_bar_3_landscape`
- `navigation_bar_4`
- `navigation_bar_5`
- `navigation_bar_5_vertical`
- `navigation_bar_6`
- `phone2`
- `progressBar`
- `progressScrubberDisabled`
- `progressScrubberFocused`
- `progressScrubberPressed`
- `quick_contact`
- `quickscroll2`
- `quickscroll3`
- `rect`
- `rrect`
- `scrollbars2`
- `spinner2`
- `split_action_bar`
- `split_action_bar_landscape`
- `statusBar`
- `switch_off`
- `switch_on`
- `tab2`
- `textSelHandles`
- `text_insertion_point`
- `textfield`
- `time_picker`
- `time_picker_dark`
- `transparent`

View File

@@ -0,0 +1,33 @@
# arrows2
**Type:** mxgraph shapes
**Prefix:** `mxgraph.arrows2`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.arrows2.arrow;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="100" height="60" as="geometry" />
</mxCell>
```
## Shapes (18)
- `arrow`
- `bendArrow`
- `bendDoubleArrow`
- `calloutArrow`
- `calloutDouble90Arrow`
- `calloutDoubleArrow`
- `calloutQuadArrow`
- `jumpInArrow`
- `quadArrow`
- `sharpArrow`
- `sharpArrow2`
- `stripedArrow`
- `stylisedArrow`
- `tailedArrow`
- `tailedNotchedArrow`
- `triadArrow`
- `twoWayArrow`
- `uTurnArrow`

View File

@@ -0,0 +1,32 @@
# atlassian
**Type:** SVG images
**Path:** `img/lib/atlassian/`
## Usage
```xml
<mxCell value="label" style="image;aspect=fixed;image=img/lib/atlassian/Jira_Logo.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (17)
- `Atlassian_Logo`
- `Bamboo_Logo`
- `Bitbucket_Logo`
- `Clover_Logo`
- `Confluence_Logo`
- `Crowd_Logo`
- `Crucible_Logo`
- `Fisheye_Logo`
- `Hipchat_Logo`
- `Jira_Core_Logo`
- `Jira_Logo`
- `Jira_Service_Desk_Logo`
- `Jira_Software_Logo`
- `Sourcetree_Logo`
- `Statuspage_Logo`
- `Stride_Logo`
- `Trello_Logo`

1049
docs/shape-libraries/aws4.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,431 @@
# azure2
**Type:** SVG images
**Path:** `img/lib/azure2/`
## Usage
```xml
<mxCell value="label" style="image;aspect=fixed;image=img/lib/azure2/compute/Virtual_Machine.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (648)
Shapes are organized by category: `azure2/{category}/{shape}.svg`
### ai_machine_learning (30)
- `AI_Studio`
- `Anomaly_Detector`
- `Azure_Applied_AI`
- `Azure_Experimentation_Studio`
- `Azure_Object_Understanding`
- `Azure_OpenAI`
- `Batch_AI`
- `Bonsai`
- `Bot_Services`
- `Cognitive_Services`
- `Cognitive_Services_Decisions`
- `Computer_Vision`
- `Content_Moderators`
- `Content_Safety`
- `Custom_Vision`
- `Face_APIs`
- `Form_Recognizers`
- `Genomics`
- `Immersive_Readers`
- `Language_Services`
- `Language_Understanding`
- `Machine_Learning`
- `Machine_Learning_Studio_Classic_Web_Services`
- `Machine_Learning_Studio_Web_Service_Plans`
- `Machine_Learning_Studio_Workspaces`
- `Personalizers`
- `QnA_Makers`
- `Serverless_Search`
- `Speech_Services`
- `Translator_Text`
### analytics (14)
- `Analysis_Services`
- `Azure_Databricks`
- `Azure_Synapse_Analytics`
- `Azure_Workbooks`
- `Data_Lake_Analytics`
- `Data_Lake_Store_Gen1`
- `Endpoint_Analytics`
- `Event_Hub_Clusters`
- `Event_Hubs`
- `HD_Insight_Clusters`
- `Log_Analytics_Workspaces`
- `Power_BI_Embedded`
- `Power_Platform`
- `Stream_Analytics_Jobs`
### app_services (9)
- `API_Management_Services`
- `App_Service_Certificates`
- `App_Service_Domains`
- `App_Service_Environments`
- `App_Service_Plans`
- `App_Services`
- `CDN_Profiles`
- `Notification_Hubs`
- `Search_Services`
### compute (38)
- `App_Services`
- `Application_Group`
- `Automanaged_VM`
- `Availability_Sets`
- `Azure_Compute_Galleries`
- `Azure_Spring_Cloud`
- `Batch_Accounts`
- `Cloud_Services_Classic`
- `Container_Instances`
- `Container_Services_Deprecated`
- `Disk_Encryption_Sets`
- `Disks`
- `Disks_Classic`
- `Disks_Snapshots`
- `Function_Apps`
- `Host_Groups`
- `Host_Pools`
- `Hosts`
- `Image_Definitions`
- `Image_Templates`
- `Image_Versions`
- `Images`
- `Kubernetes_Services`
- `Maintenance_Configuration`
- `Managed_Service_Fabric`
- `Mesh_Applications`
- `Metrics_Advisor`
- `OS_Images_Classic`
- `Restore_Points`
- `Restore_Points_Collections`
- `Service_Fabric_Clusters`
- `Shared_Image_Galleries`
- `VM_Images_Classic`
- `VM_Scale_Sets`
- `Virtual_Machine`
- `Virtual_Machines_Classic`
- `Workspaces`
- `Workspaces2`
### containers (7)
- `App_Services`
- `Azure_Red_Hat_OpenShift`
- `Batch_Accounts`
- `Container_Instances`
- `Container_Registries`
- `Kubernetes_Services`
- `Service_Fabric_Clusters`
### databases (27)
- `Azure_Cosmos_DB`
- `Azure_Data_Explorer_Clusters`
- `Azure_Database_MariaDB_Server`
- `Azure_Database_Migration_Services`
- `Azure_Database_MySQL_Server`
- `Azure_Database_PostgreSQL_Server`
- `Azure_Database_PostgreSQL_Server_Group`
- `Azure_Purview_Accounts`
- `Azure_SQL`
- `Azure_SQL_Edge`
- `Azure_SQL_Server_Stretch_Databases`
- `Azure_SQL_VM`
- `Azure_Synapse_Analytics`
- `Cache_Redis`
- `Data_Factory`
- `Elastic_Job_Agents`
- `Instance_Pools`
- `Managed_Database`
- `Oracle_Database`
- `SQL_Data_Warehouses`
- `SQL_Database`
- `SQL_Elastic_Pools`
- `SQL_Managed_Instance`
- `SQL_Server`
- `SQL_Server_Registries`
- `SSIS_Lift_And_Shift_IR`
- `Virtual_Clusters`
### identity (35)
- `AAD_Licenses`
- `Active_Directory_Connect_Health`
- `Active_Directory_Connect_Health2`
- `Administrative_Units`
- `App_Registrations`
- `Azure_AD_B2C`
- `Azure_AD_B2C2`
- `Azure_AD_Domain_Services`
- `Azure_AD_Identity_Protection`
- `Azure_AD_Privilege_Identity_Management`
- `Azure_Active_Directory`
- `Azure_Information_Protection`
- `Custom_Azure_AD_Roles`
- `Enterprise_Applications`
- `Entra_Connect`
- `Entra_Domain_Services`
- `Entra_Global_Secure_Access`
- `Entra_ID_Protection`
- `Entra_Internet_Access`
- `Entra_Managed_Identities`
- `Entra_Private_Access`
- `Entra_Privileged_Identity_Management`
- `Entra_Verified_ID`
- `External_Identities`
- `Groups`
- `Identity_Governance`
- `Managed_Identities`
- `Multi_Factor_Authentication`
- `PIM`
- `Security`
- `Tenant_Properties`
- `User_Settings`
- `Users`
- `Verifiable_Credentials`
- `Verification_As_A_Service`
### networking (51)
- `ATM_Multistack`
- `Application_Gateway_Containers`
- `Application_Gateways`
- `Azure_Communications_Gateway`
- `Azure_Firewall_Manager`
- `Azure_Firewall_Policy`
- `Bastions`
- `CDN_Profiles`
- `Connections`
- `DDoS_Protection_Plans`
- `DNS_Multistack`
- `DNS_Private_Resolver`
- `DNS_Security_Policy`
- `DNS_Zones`
- `ExpressRoute_Circuits`
- `Firewalls`
- `Front_Doors`
- `IP_Address_manager`
- `IP_Groups`
- `Load_Balancer_Hub`
- `Load_Balancers`
- `Local_Network_Gateways`
- `NAT`
- `Network_Interfaces`
- `Network_Security_Groups`
- `Network_Watcher`
- `On_Premises_Data_Gateways`
- `Private_Endpoint`
- `Private_Link`
- `Private_Link_Hub`
- `Private_Link_Service`
- `Proximity_Placement_Groups`
- `Public_IP_Addresses`
- `Public_IP_Addresses_Classic`
- `Public_IP_Prefixes`
- `Reserved_IP_Addresses_Classic`
- `Resource_Management_Private_Link`
- `Route_Filters`
- `Route_Tables`
- `Service_Endpoint_Policies`
- `Spot_VM`
- `Spot_VMSS`
- `Subnet`
- `Traffic_Manager_Profiles`
- `Virtual_Network_Gateways`
- `Virtual_Networks`
- `Virtual_Networks_Classic`
- `Virtual_Router`
- `Virtual_WAN_Hub`
- `Virtual_WANs`
- `Web_Application_Firewall_Policies_WAF`
### security (14)
- `Application_Security_Groups`
- `Azure_AD_Risky_Signins`
- `Azure_AD_Risky_Users`
- `Azure_Defender`
- `Azure_Sentinel`
- `Conditional_Access`
- `Detonation`
- `ExtendedSecurityUpdates`
- `Identity_Secure_Score`
- `Key_Vaults`
- `Keys`
- `MS_Defender_EASM`
- `Multifactor_Authentication`
- `Security_Center`
### storage (17)
- `Azure_Fileshare`
- `Azure_HCP_Cache`
- `Azure_NetApp_Files`
- `Azure_Stack_Edge`
- `Data_Box`
- `Data_Box_Edge`
- `Data_Lake_Storage_Gen1`
- `Data_Share_Invitations`
- `Data_Shares`
- `Import_Export_Jobs`
- `Recovery_Services_Vaults`
- `StorSimple_Data_Managers`
- `StorSimple_Device_Managers`
- `Storage_Accounts`
- `Storage_Accounts_Classic`
- `Storage_Explorer`
- `Storage_Sync_Services`
### general (98)
- `All_Resources`
- `Backlog`
- `Biz_Talk`
- `Blob_Block`
- `Blob_Page`
- `Branch`
- `Browser`
- `Bug`
- `Builds`
- `Cache`
- `Code`
- `Commit`
- `Controls`
- `Controls_Horizontal`
- `Cost_Alerts`
- `Cost_Analysis`
- `Cost_Budgets`
- `Cost_Management`
- `Cost_Management_and_Billing`
- `Counter`
- `Cubes`
- `Dashboard`
- `Dashboard2`
- `Dev_Console`
- `Download`
- `Error`
- `Extensions`
- `FTP`
- `File`
- `Files`
- `Folder_Blank`
- `Folder_Website`
- `Free_Services`
- `Gear`
- `Globe`
- `Globe_Error`
- `Globe_Success`
- `Globe_Warning`
- `Guide`
- `Heart`
- `Help_and_Support`
- `Image`
- `Information`
- `Input_Output`
- `Journey_Hub`
- `Launch_Portal`
- `Learn`
- `Load_Test`
- `Location`
- `Log_Streaming`
- `Management_Groups`
- `Management_Portal`
- `Marketplace`
- `Media`
- `Media_File`
- `Mobile`
- `Mobile_Engagement`
- `Module`
- `Power`
- `Power_Up`
- `Powershell`
- `Preview`
- `Preview_Features`
- `Process_Explorer`
- `Production_Ready_Database`
- `Quickstart_Center`
- `Recent`
- `Reservations`
- `Resource_Explorer`
- `Resource_Group_List`
- `Resource_Groups`
- `Resource_Linked`
- `SSD`
- `Scale`
- `Scheduler`
- `Search`
- `Search_Grid`
- `Server_Farm`
- `Service_Bus`
- `Service_Health`
- `Storage_Azure_Files`
- `Storage_Container`
- `Storage_Queue`
- `Subscriptions`
- `TFS_VC_Repository`
- `Table`
- `Tag`
- `Tags`
- `Templates`
- `Toolbox`
- `Troubleshoot`
- `Versions`
- `Web_Slots`
- `Web_Test`
- `Website_Power`
- `Website_Staging`
- `Workbooks`
- `Workflow`
### other (149)
(See draw.io for complete list of 149 shapes in the "other" category)
Selected shapes:
- `Azure_Backup_Center`
- `Azure_Chaos_Studio`
- `Azure_Cloud_Shell`
- `Azure_Communication_Services`
- `Azure_Deployment_Environments`
- `Azure_Load_Testing`
- `Azure_Monitor_Dashboard`
- `Azure_Network_Manager`
- `Azure_Orbital`
- `Azure_Sphere`
- `Azure_Storage_Mover`
- `Grafana`
- `Kubernetes_Fleet_Manager`
- `SSH_Keys`
### Additional Categories
- **azure_ecosystem** (3): Applens, Azure_Hybrid_Center, Collaborative_Service
- **azure_stack** (8): Azure_Stack, Capacity, Infrastructure_Backup, Multi_Tenancy, Offers, Plans, Updates, User_Subscriptions
- **azure_vmware_solution** (1): AVS
- **blockchain** (6): ABS_Member, Azure_Blockchain_Service, Azure_Token_Service, Blockchain_Applications, Consortium, Outbound_Connection
- **cxp** (2): Elixir, Elixir_Purple
- **devops** (10): API_Connections, Application_Insights, Azure_DevOps, Change_Analysis, CloudTest, Code_Optimization, DevOps_Starter, DevTest_Labs, Lab_Accounts, Lab_Services
- **hybrid_multicloud** (5): Azure_Operator_5G_Core, Azure_Operator_Insights, Azure_Operator_Nexus, Azure_Operator_Service_Manager, Azure_Programmable_Connectivity
- **integration** (21): API_Management_Services, App_Configuration, Azure_API_for_FHIR, Azure_Data_Catalog, Event_Grid_Domains, Event_Grid_Subscriptions, Event_Grid_Topics, Integration_Accounts, Integration_Environments, Integration_Service_Environments, Logic_Apps, Logic_Apps_Custom_Connector, Partner_Namespace, Partner_Registration, Partner_Topic, Relays, SQL_Data_Warehouses, SendGrid_Accounts, Service_Bus, Software_as_a_Service, System_Topic
- **internet_of_things** (3): Digital_Twins, Logic_Apps, Time_Series_Insights_Access_Policies
- **intune** (17): Azure_AD_Roles_and_Administrators, Client_Apps, Device_Compliance, Device_Configuration, Device_Enrollment, Device_Security_Apple, Device_Security_Google, Device_Security_Windows, Devices, Exchange_Access, Intune, Intune_For_Education, Mindaro, Security_Baselines, Software_Updates, Tenant_Status, eBooks
- **iot** (19): Azure_IoT_Operations, Azure_Maps_Accounts, Azure_Stack_HCI_Sizer, Device_Provisioning_Services, Digital_Twins, Event_Hubs, Function_Apps, Industrial_IoT, IoT_Central_Applications, IoT_Edge, IoT_Hub, Logic_Apps, Notification_Hubs, Stack_HCI_Premium, Stream_Analytics_Jobs, Time_Series_Data_Sets, Time_Series_Insights_Environments, Time_Series_Insights_Event_Sources, Windows10_Core_Services
- **management_governance** (32): Activity_Log, Advisor, Alerts, Application_Insights, Arc_Machines, Automation_Accounts, Azure_Arc, Azure_Lighthouse, Blueprints, Compliance, Cost_Management_and_Billing, Customer_Lockbox_for_MS_Azure, Diagnostics_Settings, Education, Log_Analytics_Workspaces, MachinesAzureArc, Managed_Applications_Center, Managed_Desktop, Metrics, Monitor, My_Customers, Operation_Log_Classic, Policy, Recovery_Services_Vaults, Resource_Graph_Explorer, Resources_Provider, Scheduler_Job_Collections, Service_Catalog_MAD, Service_Providers, Solutions, Universal_Print, User_Privacy
- **menu** (1): Keys
- **migrate** (5): Azure_Migrate, Cost_Management_and_Billing, Data_Box, Data_Box_Edge, Recovery_Services_Vaults
- **mixed_reality** (2): Remote_Rendering, Spatial_Anchor_Accounts
- **monitor** (1): SAP_Azure_Monitor
- **power_platform** (9): AIBuilder, CopilotStudio, Dataverse, PowerApps, PowerAutomate, PowerBI, PowerFx, PowerPages, PowerPlatform
- **preview** (9): Azure_Cloud_Shell, Azure_Sphere, Azure_Workbooks, IoT_Edge, Private_Link_Hub, RTOS, Static_Apps, Time_Series_Data_Sets, Web_Environment
- **web** (5): API_Center, App_Space, Azure_Media_Service, Notification_Hub_Namespaces, SignalR

View File

@@ -0,0 +1,48 @@
# basic
**Type:** mxgraph shapes
**Prefix:** `mxgraph.basic`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.basic.{shape};fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (31)
- `4_point_star`
- `6_point_star`
- `8_point_star`
- `banner`
- `cloud_callout`
- `cloud_rect`
- `cone`
- `cross`
- `document`
- `flash`
- `half_circle`
- `heart`
- `loud_callout`
- `moon`
- `mxgraph.basic`
- `no_symbol`
- `octagon`
- `orthogonal_triangle`
- `oval_callout`
- `parallelepiped`
- `pentagon`
- `pointed_oval`
- `rectangular_callout`
- `rounded_rectangular_callout`
- `smiley`
- `star`
- `sun`
- `tick`
- `trapezoid`
- `wave`
- `x`

View File

@@ -0,0 +1,60 @@
# bpmn
**Type:** mxgraph shapes
**Prefix:** `mxgraph.bpmn`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.bpmn.shape;symbol=message;outline=throwing;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Parameters
- `outline` - Event type: `start`, `end`, `catching`, `throwing`, `none`
- `symbol` - Icon inside: `message`, `timer`, `error`, `cancel`, `compensation`, `link`, `terminate`, `general`, `multiple`, `rule`
## Shapes (40)
- `ad_hoc`
- `business_rule_task`
- `cancel_end`
- `cancel_intermediate`
- `compensation`
- `compensation_end`
- `compensation_intermediate`
- `error_end`
- `error_intermediate`
- `gateway`
- `gateway_and`
- `gateway_complex`
- `gateway_or`
- `gateway_xor_(data)`
- `gateway_xor_(event)`
- `general_end`
- `general_intermediate`
- `general_start`
- `link_end`
- `link_intermediate`
- `link_start`
- `loop`
- `loop_marker`
- `manual_task`
- `message_end`
- `message_intermediate`
- `message_start`
- `multiple_end`
- `multiple_instances`
- `multiple_intermediate`
- `multiple_start`
- `mxgraph.bpmn`
- `rule_intermediate`
- `rule_start`
- `script_task`
- `service_task`
- `terminate`
- `timer_intermediate`
- `timer_start`
- `user_task`

View File

@@ -0,0 +1,71 @@
# cabinets
**Type:** mxgraph shapes
**Prefix:** `mxgraph.cabinets`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.cabinets.{shape};" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (54)
- `auxiliary_contact_contactor_1_32a`
- `auxiliary_contact_contactor_32_125a`
- `cb_1p`
- `cb_1p_x10`
- `cb_2p`
- `cb_2p_x10`
- `cb_3p`
- `cb_3p_x5`
- `cb_4p`
- `cb_4p_x5`
- `cb_auxiliary_contact`
- `contactor_125_400a`
- `contactor_1_32a`
- `contactor_32_125a`
- `din_rail`
- `distribution_block_4p_125a_11_connections`
- `distribution_block_4p_125a_11_connections_2`
- `mccb_25_63a_3p`
- `mccb_25_63a_4p`
- `mccb_63_250a_3p`
- `mccb_63_250a_4p`
- `motor_cb_125_400a`
- `motor_cb_1_32a`
- `motor_cb_32_125a`
- `motor_protection_cb`
- `motor_starter_125_400a`
- `motor_starter_1_32a`
- `motor_starter_32_125a`
- `motorized_switch_3p`
- `motorized_switch_4p`
- `mxgraph.cabinets`
- `overcurrent_relay_125_400a`
- `overcurrent_relay_1_32a`
- `overcurrent_relay_32_125a`
- `plugin_relay_1`
- `plugin_relay_2`
- `residual_current_device_2p`
- `residual_current_device_4p`
- `surge_protection_1p`
- `surge_protection_2p`
- `surge_protection_3p`
- `surge_protection_4p`
- `terminal_40mm2`
- `terminal_40mm2_x10`
- `terminal_4_6mm2`
- `terminal_4_6mm2_x10`
- `terminal_4mm2`
- `terminal_4mm2_x10`
- `terminal_50mm2`
- `terminal_50mm2_x10`
- `terminal_6_25mm2`
- `terminal_6_25mm2_x10`
- `terminal_75mm2`
- `terminal_75mm2_x10`

View File

@@ -0,0 +1,250 @@
# cisco19
**Type:** mxgraph shapes
**Prefix:** `mxgraph.cisco19`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.cisco19.rect;prIcon={shape};fillColor=#00bceb;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (233)
- `3g_4g_indicator`
- `6500_vss`
- `6500_vss2`
- `access_control_and_trustsec`
- `aci`
- `aci2`
- `acibg`
- `acs`
- `ad_decoder`
- `ad_encoder`
- `analysis_correlation`
- `anomaly_detection`
- `anti_malware`
- `anti_malware2`
- `appnav`
- `asa_5500`
- `asr_1000`
- `asr_9000`
- `avc_application_visibility_control`
- `avc_application_visibility_control2`
- `bg1`
- `bg10`
- `bg2`
- `bg3`
- `bg4`
- `bg5`
- `bg6`
- `bg7`
- `bg8`
- `bg9`
- `blade_server`
- `branch`
- `branch2`
- `camera`
- `camera2`
- `cell_phone`
- `cell_phone2`
- `cisco_15800`
- `cisco_dna`
- `cisco_dna_center`
- `cisco_meetingplace_express`
- `cisco_security_manager`
- `cisco_unified_contact_center_enterprise_and_hosted`
- `cisco_unified_presence_service`
- `clock`
- `cloud`
- `cloud2`
- `cognitive`
- `collab1`
- `collab2`
- `collab3`
- `collab4`
- `communications_manager`
- `contact_center_express`
- `content_recording_streaming_server`
- `content_router`
- `csr_1000v`
- `da_decoder`
- `da_encoder`
- `data_center`
- `data_center2`
- `database_relational`
- `dns_server`
- `dns_server2`
- `dual_mode_access_point`
- `email_security`
- `fabric_interconnect`
- `fibre_channel_director_mds_9000`
- `fibre_channel_fabric_switch`
- `firewall`
- `flow_analytics`
- `flow_analytics2`
- `flow_collector`
- `h323`
- `handheld`
- `handheld2`
- `hdtv`
- `hdtv2`
- `home_office`
- `home_office2`
- `host_based_security`
- `hypervisor`
- `immersive_telepresence_endpoint`
- `ip_ip_gateway`
- `ip_phone`
- `ip_phone2`
- `ip_telephone_router`
- `ips_ids`
- `ironport`
- `ise`
- `joystick_keyboard`
- `joystick_keyboard2`
- `key`
- `key2`
- `l2_modular`
- `l2_modular2`
- `l2_switch`
- `l2_switch_with_dual_supervisor`
- `l3_modular`
- `l3_modular2`
- `l3_modular3`
- `l3_switch`
- `l3_switch_with_dual_supervisor`
- `laptop`
- `laptop2`
- `laptop_video_client`
- `laptop_video_client2`
- `layer3_nexus_5k_switch`
- `ldap`
- `ldap2`
- `load_balancer`
- `lock`
- `lock2`
- `media_server`
- `meeting_scheduling_and_management_server`
- `mesh_access_point`
- `monitor`
- `monitoring`
- `multipoint_meeting_server`
- `mxgraph.cisco19`
- `nac_appliance`
- `nam_virtual_service_blade`
- `net_mgmt_appliance`
- `netflow_router`
- `netflow_router2`
- `netflow_router3`
- `next_generation_intrusion_prevention_system`
- `nexus_1010`
- `nexus_1k`
- `nexus_1kv_vsm`
- `nexus_2000_10ge`
- `nexus_2k`
- `nexus_3k`
- `nexus_4k`
- `nexus_5k`
- `nexus_5k_with_integrated_vsm`
- `nexus_7k`
- `nexus_9300`
- `nexus_9500`
- `operations_manager`
- `phone_polycom`
- `phone_polycom2`
- `policy_configuration`
- `pos`
- `pos2`
- `posture_assessment`
- `primary_codec`
- `printer`
- `printer2`
- `router`
- `router_with_firewall`
- `router_with_firewall2`
- `router_with_voice`
- `rps`
- `secondary_codec`
- `secure_catalyst_switch_color`
- `secure_catalyst_switch_color2`
- `secure_catalyst_switch_color3`
- `secure_catalyst_switch_subdued`
- `secure_catalyst_switch_subdued2`
- `secure_endpoint_pc`
- `secure_endpoint_pc2`
- `secure_endpoints`
- `secure_endpoints2`
- `secure_router`
- `secure_server`
- `secure_server2`
- `secure_switch`
- `security_management`
- `server`
- `server2`
- `service_ready_engine`
- `set_top`
- `set_top2`
- `shield`
- `ssl_terminator`
- `stealthwatch_management_console_smc`
- `stealthwatch_management_console_smc2`
- `storage`
- `surveillance_camera`
- `surveillance_camera2`
- `tablet`
- `tablet2`
- `telepresence_endpoint`
- `telepresence_endpoint_twin_data_display`
- `telepresence_exchange`
- `threat_intelligence`
- `transcoder`
- `ucs_5108_blade_chassis`
- `ucs_c_series_server`
- `ucs_express`
- `unity`
- `upc_unified_personal_communicator`
- `upc_unified_personal_communicator2`
- `ups`
- `user`
- `user2`
- `vbond`
- `video_analytics`
- `video_call_server`
- `video_gateway`
- `virtual_desktop_service`
- `virtual_matrix_switch`
- `virtual_private_network`
- `virtual_private_network2`
- `virtual_private_network_connector`
- `vmanage`
- `vpn_concentrator`
- `vsmart`
- `vts`
- `vts2`
- `web_application_firewall`
- `web_reputation_filtering`
- `web_reputation_filtering_2`
- `web_security`
- `web_security_services`
- `web_security_services2`
- `webex`
- `wifi_indicator`
- `wireless_access_point`
- `wireless_access_point2`
- `wireless_bridge`
- `wireless_bridge2`
- `wireless_connector`
- `wireless_intrusion_prevention`
- `wireless_lan_controller`
- `wireless_location_appliance`
- `wireless_router`
- `workgroup_switch`
- `workstation`
- `workstation2`
- `x509_certificate`
- `x509_certificate2`

View File

@@ -0,0 +1,115 @@
# citrix
**Type:** mxgraph shapes
**Prefix:** `mxgraph.citrix`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.citrix.{shape};fillColor=#00A4E4;strokeColor=none;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="60" as="geometry" />
</mxCell>
```
## Shapes (98)
- `1u_2u_server`
- `access_card`
- `branch_repeater`
- `browser`
- `cache_server`
- `calendar`
- `cell_phone`
- `chassis`
- `citrix_hdx`
- `citrix_logo`
- `cloud`
- `command_center`
- `database`
- `database_server`
- `datacenter`
- `desktop`
- `desktop_web`
- `dhcp_server`
- `directory_server`
- `dns_server`
- `document`
- `edgesight_server`
- `file_server`
- `firewall`
- `ftp_server`
- `geolocation_database`
- `globe`
- `goto_meeting`
- `government`
- `home_office`
- `hq_enterprise`
- `inspection`
- `ip_phone`
- `kiosk`
- `laptop_1`
- `laptop_2`
- `license_server`
- `merchandising_server`
- `middleware`
- `mxgraph.citrix`
- `netscaler_gateway`
- `netscaler_mpx`
- `netscaler_sdx`
- `netscaler_vpx`
- `pbx_server`
- `pda`
- `podio`
- `printer`
- `process`
- `provisioning_server`
- `proxy_server`
- `radius_server`
- `remote_office`
- `reporting`
- `role_appcontroller`
- `role_applications`
- `role_cloudbridge`
- `role_desktops`
- `role_load_testing_controller`
- `role_load_testing_launcher`
- `role_receiver`
- `role_repeater`
- `role_secure_access`
- `role_security`
- `role_services`
- `role_storefront`
- `role_storefront_services`
- `role_synchronizer`
- `role_xenmobile`
- `role_xenmobile_device_manager`
- `router`
- `security`
- `sharefile`
- `site`
- `smtp_server`
- `storefront_services`
- `switch`
- `tablet_1`
- `tablet_2`
- `thin_client`
- `tower_server`
- `user_control`
- `users`
- `web_server`
- `web_service`
- `worxenroll`
- `worxhome`
- `worxmail`
- `worxweb`
- `xenapp_server`
- `xenapp_services`
- `xenapp_web`
- `xencenter`
- `xenclient`
- `xenclient_synchronizer`
- `xendesktop_server`
- `xenmobile`
- `xenserver`

View File

@@ -0,0 +1,50 @@
# electrical
**Type:** mxgraph shapes
**Prefix:** `mxgraph.electrical`
## Usage
```xml
<mxCell value="label" style="shape=mxgraph.electrical.resistors.resistor_1;" vertex="1" parent="1">
<mxGeometry x="0" y="0" width="60" height="20" as="geometry" />
</mxCell>
```
Shapes are organized by category: `mxgraph.electrical.{category}.{shape}`
## Categories
### resistors
- `resistor_1`
- `resistor_2`
### capacitors
- `capacitor_1`
- `capacitor_3`
### inductors
- `inductor_3`
- `transformer_1`
### diodes
- `diode`
- `zener_diode_1`
### transistors
- `npn_transistor_1`
- `pnp_transistor_1`
### mosfets1
- `n-channel_mosfet_1`
- `p-channel_mosfet_1`
### logic_gates
- `logic_gate`
- `dual_inline_ic`
### electro-mechanical
- `singleSwitch`
- `pushbutton`
(See draw.io Electrical shape library for complete list)

Some files were not shown because too many files have changed in this diff Show More