From 60105ddb51c5e02824cb15be39d5184ed552438d Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:35:55 +0900 Subject: [PATCH] refactor: remove dead code and over-engineering found by two audits (#986) ## Problem Two audits of the repository (a workflow of finder, dedup and per-file verifier agents, run on 2026-10-11 against 1f4141e and again against 07f3e3c after a first round of refactoring) found dead code and features that were more machinery than the app needs: an Electron configuration-preset system with its own settings window, three copies of the about page, a hand-written `.env` parser, an "extended" system prompt nobody selected, two data migrations for formats from 2025-12 and 2026-01, a server-side example cache, type-to-confirm provider deletion, 81 unused dictionary keys, wrapper components around single call sites, and a long tail of unused exports, props, options and tests. ## Change Every item below was either approved one by one or covered by "fix all of the others". - **Electron:** the configuration-preset system is gone (settings window, preload, IPC, menu entry, dev watcher); `config-manager.ts` keeps only `userLocale`; `.env` files are parsed with `dotenv`. Entitlements file and a duplicate CI workflow removed; `test.yml` gets a type check and concurrency. - **Server:** no extended prompt, no example cache, no `ENABLE_HISTORY_XML_REPLACE`, no message metadata or `hasConfig`; the connection test is one shared helper; EdgeOne loses its mock branch and model aliases; `DEBUG_LLM_PAYLOAD` logs message structure instead of payloads; Langfuse scoring helper; `apiKeyEnv` arrays kept and documented in `env.example` and the provider docs. - **Pages:** one about page per language from a single component (old `/cn` and `/ja` URLs redirect with 308); layout uses `hasLocale`; the GitHub icon is a local component instead of `react-icons`; Cloudflare deploys without an R2 cache by default. - **Chat and canvas:** the legacy `diagramHistory` conversion, the localStorage chat migration and the templates storage are gone; `chat-engine` reads the diagram through the context's refs; the diagram context has one tagged export path (`requestTaggedExport` / `fetchDiagramXml`); the screenshot check uses fetch + JSON and `generateText` with `Output.object`; `load-diagram` accepts `` and strips the XML prolog. - **Settings UI:** `model-selector` inlines its Dialog, Command and Tooltip; `ai-elements/model-selector`, `button-with-tooltip`, `error-toast` removed; no type-to-confirm, no footer, no `bedrockApiKey`; `use-model-config` loses the old-format migration; one reasoning component; the shimmer is CSS (no `motion`, no `@radix-ui/react-use-controllable-state`); 81 dictionary keys removed in all four languages. - **MCP server:** `registerTool` / `ok` / `fail` replace per-tool try/catch; `syncFromBrowser`, `commitDocument`, `describePages`; the hand-written XML syntax check, unused result fields, `pickPageSelector`, `expandStyles` and the `host` attribute handling are gone; tests get a shared `setup.ts` and drive the server through the SDK client. - **Prompt:** the web app's style rules come from the MCP drawing guide (`styleRules`), byte-identical to before. - **Tests:** shared e2e helpers (`openApp`, `sse`, `toolCallEvents`, `chunkedReplies`); placeholder specs dropped; `settings-review-fixes` split by topic; unit tests use `vi.stubEnv`. - **Dependencies removed:** `@xmldom/xmldom`, `remark-gfm`, `@anthropic-ai/tokenizer`, `@testing-library/user-event`, `concurrently`, `wait-on`, `eslint`, `eslint-config-next`, `@vitest/coverage-v8`, `shx`, `motion`, `@radix-ui/react-use-controllable-state`, `react-icons`. Source lines: 74,983 before, 67,418 after (excluding lockfiles). The last commit merges #984 (the canvas shell). Where the shell, the classic page or their tests depend on a wire format or signature this branch had changed, main's version wins (export fields of the poll response, `addHistory` signature); where main only extended code this branch restructured, main's change is carried into the new structure (`start_session`, `get_selection`, `prepareNewDiagram`, the diagram context's `requestExport`). ## Testing - `tsc --noEmit` for the web app, `packages/mcp-server`, its shell and `electron`: clean - `biome ci`: 0 errors (37 pre-existing warnings) - Root `vitest run`: 719 passed; `packages/mcp-server` `vitest run`: 372 passed - `next build`: clean - Playwright, whole web suite on the dev server (4 workers): 280 tests, 279 passed; `language.spec.ts` "a stale offer to switch without saving does nothing once a message is being sent" failed under parallel load and passed alone, as it does on main --- .eslintrc.json | 3 - .github/workflows/ci.yml | 42 - .github/workflows/docker-build.yml | 3 - .github/workflows/test.yml | 14 +- app/[lang]/about/cn/page.tsx | 336 - app/[lang]/about/ja/page.tsx | 350 -- app/[lang]/about/page.tsx | 685 +- app/[lang]/admin/admin-shared.ts | 23 +- app/[lang]/admin/setting-field.tsx | 27 - app/[lang]/layout.tsx | 29 +- app/api/admin/settings/route.ts | 4 - app/api/admin/test-model/route.ts | 51 +- app/api/chat/route.ts | 158 +- app/api/chat/xml_guide.md | 321 - app/api/config/route.ts | 3 - app/api/log-feedback/route.ts | 84 +- app/api/log-save/route.ts | 40 +- app/api/server-models/route.ts | 5 +- app/api/validate-diagram/route.ts | 48 +- app/api/validate-model/route.ts | 246 +- app/globals.css | 31 +- components/ai-elements/model-selector.tsx | 238 - components/ai-elements/reasoning.tsx | 221 +- components/ai-elements/shimmer.tsx | 27 +- components/button-with-tooltip.tsx | 36 - components/canvas/canvas-stage.tsx | 3 +- components/canvas/drawio-frame.tsx | 32 +- components/chat/TemplateCreateDialog.tsx | 206 - ...eEditDialog.tsx => TemplateFormDialog.tsx} | 117 +- components/chat/TemplatePanel.tsx | 108 +- components/chat/ValidationCard.tsx | 4 - components/chat/chat-engine.tsx | 128 +- components/chat/composer.tsx | 27 +- components/chat/lobby-hero.tsx | 31 +- components/chat/message-list.tsx | 18 +- components/chat/use-diagram-streaming.ts | 43 +- components/dev-xml-simulator.tsx | 37 +- components/error-toast.tsx | 44 - components/icons/github.tsx | 13 + components/model-config-dialog.tsx | 43 +- components/model-selector.tsx | 702 ++- components/provider-credentials-fields.tsx | 37 +- components/quota-limit-toast.tsx | 18 +- components/settings-dialog.tsx | 6 +- components/ui/alert-dialog.tsx | 11 - components/ui/command.tsx | 51 - components/ui/dialog.tsx | 11 +- components/ui/popover.tsx | 8 +- components/ui/scroll-area.tsx | 2 +- components/ui/select.tsx | 37 - components/workspace/icon-button.tsx | 16 +- contexts/diagram-context.tsx | 263 +- docs/cn/ai-providers.md | 2 +- docs/cn/cloudflare-deploy.md | 46 +- docs/en/ai-providers.md | 2 +- docs/en/cloudflare-deploy.md | 46 +- docs/ja/ai-providers.md | 2 +- docs/ja/cloudflare-deploy.md | 46 +- edge-functions/api/edgeai/chat/completions.ts | 70 +- electron/electron.d.ts | 72 +- electron/main/app-menu.ts | 163 +- electron/main/config-manager.ts | 585 +- electron/main/env-loader.ts | 61 +- electron/main/index.ts | 8 - electron/main/ipc-handlers.ts | 182 +- electron/main/menu-i18n.ts | 140 +- electron/main/next-server.ts | 26 +- electron/main/port-manager.ts | 37 +- electron/main/settings-window.ts | 78 - electron/main/window-manager.ts | 6 +- electron/preload/index.ts | 14 +- electron/preload/settings.ts | 35 - electron/settings/index.html | 117 - electron/settings/settings.css | 344 -- electron/settings/settings.js | 313 - env.example | 23 +- hooks/use-diagram-tool-handlers.ts | 91 +- hooks/use-dictionary.ts | 2 - hooks/use-model-config.ts | 97 +- hooks/use-session-manager.ts | 22 +- hooks/use-validate-diagram.ts | 154 +- lib/admin/providers.ts | 12 +- lib/admin/settings-registry.ts | 10 +- lib/admin/settings.ts | 2 +- lib/ai-providers.ts | 186 +- lib/cached-responses.ts | 5 +- lib/chat-helpers.ts | 29 - lib/diagram-diff.ts | 25 +- lib/diagram-pages.ts | 52 +- lib/diagram-validator.ts | 4 +- lib/drawio/editor-bridge.ts | 13 - lib/dynamo-quota-manager.ts | 106 +- lib/i18n/dictionaries/en.json | 91 +- lib/i18n/dictionaries/ja.json | 91 +- lib/i18n/dictionaries/zh-Hant.json | 91 +- lib/i18n/dictionaries/zh.json | 91 +- lib/i18n/utils.ts | 2 - lib/langfuse.ts | 51 +- lib/llm-errors.ts | 4 +- lib/model-catalog.ts | 2 +- lib/output-token-limit.ts | 11 +- lib/provider-models.ts | 2 +- lib/server-model-config.ts | 3 +- lib/session-storage.ts | 116 +- lib/system-prompts.ts | 175 +- lib/template-storage.ts | 61 +- lib/test-model-connection.ts | 239 + lib/types/model-config.ts | 9 - lib/use-file-processor.tsx | 9 +- lib/use-quota-manager.tsx | 41 +- lib/utils.ts | 262 +- lib/validation-schema.ts | 1 - next.config.ts | 25 +- open-next.config.ts | 6 +- package-lock.json | 5498 ++--------------- package.json | 19 +- packages/mcp-server/src/compact-cells.ts | 4 +- packages/mcp-server/src/diagram-operations.ts | 133 +- packages/mcp-server/src/dom.ts | 2 +- packages/mcp-server/src/drawing-guide.ts | 16 +- packages/mcp-server/src/drawio-themes.ts | 4 +- packages/mcp-server/src/http-server.ts | 17 +- packages/mcp-server/src/index.ts | 2342 +++---- packages/mcp-server/src/load-diagram.ts | 16 +- packages/mcp-server/src/new-diagram.ts | 40 +- packages/mcp-server/src/pages.ts | 5 +- packages/mcp-server/src/preview/preview.css | 1 - packages/mcp-server/src/shape-library.ts | 2 +- packages/mcp-server/src/style-classes.ts | 6 - packages/mcp-server/src/xml-examples.ts | 18 + packages/mcp-server/src/xml-validation.ts | 145 +- .../tests/custom-instructions.test.ts | 16 +- .../tests/diagram-operations.test.ts | 8 +- .../mcp-server/tests/edit-diagram.test.ts | 8 +- packages/mcp-server/tests/edit-gate.test.ts | 8 +- packages/mcp-server/tests/history.test.ts | 8 +- packages/mcp-server/tests/http-server.test.ts | 7 +- .../mcp-server/tests/load-diagram.test.ts | 42 +- packages/mcp-server/tests/multi-page.test.ts | 10 +- packages/mcp-server/tests/new-diagram.test.ts | 8 +- packages/mcp-server/tests/persistence.test.ts | 21 +- .../mcp-server/tests/server-wiring.test.ts | 286 +- packages/mcp-server/tests/setup.ts | 5 + packages/mcp-server/tests/wrap-cells.test.ts | 24 +- .../mcp-server/tests/xml-reference.test.ts | 8 +- .../mcp-server/tests/xml-validation.test.ts | 10 +- packages/mcp-server/tsconfig.json | 2 - packages/mcp-server/vitest.config.ts | 3 + resources/entitlements.mac.plist | 16 - scripts/afterPack.cjs | 43 +- scripts/copy-dereferenced.cjs | 43 + scripts/electron-dev.mjs | 164 +- scripts/prepare-electron-build.mjs | 44 +- stores/canvas-store.ts | 7 - stores/versions-store.ts | 20 +- tests/e2e/chat-errors.spec.ts | 5 +- tests/e2e/chat.spec.ts | 112 +- tests/e2e/copy-paste.spec.ts | 137 - tests/e2e/diagram-content.spec.ts | 323 +- tests/e2e/diagram-generation.spec.ts | 16 +- tests/e2e/error-handling.spec.ts | 28 +- tests/e2e/file-upload.spec.ts | 87 +- tests/e2e/fixtures/diagrams.ts | 5 - tests/e2e/history-restore.spec.ts | 199 +- tests/e2e/history.spec.ts | 18 - tests/e2e/iframe-interaction.spec.ts | 88 +- tests/e2e/keyboard.spec.ts | 13 +- tests/e2e/language.spec.ts | 22 +- tests/e2e/lib/fixtures.ts | 193 +- tests/e2e/lib/helpers.ts | 88 +- tests/e2e/model-config.spec.ts | 19 - tests/e2e/model-test.spec.ts | 20 +- tests/e2e/multi-turn.spec.ts | 16 +- tests/e2e/provider-models.spec.ts | 94 +- tests/e2e/save.spec.ts | 16 - tests/e2e/settings-bedrock.spec.ts | 341 + tests/e2e/settings-providers.spec.ts | 516 +- tests/e2e/settings-review-fixes.spec.ts | 830 --- tests/e2e/settings-tabs-sync.spec.ts | 292 + tests/e2e/settings.spec.ts | 5 +- tests/e2e/smoke.spec.ts | 9 +- tests/e2e/theme.spec.ts | 88 - tests/e2e/upload.spec.ts | 20 - tests/e2e/workspace.spec.ts | 510 +- tests/unit/admin-providers-route.test.ts | 14 +- tests/unit/admin-providers.test.ts | 76 +- tests/unit/admin-settings.test.ts | 16 +- tests/unit/admin-test-model.test.ts | 54 +- tests/unit/ai-providers-cache.test.ts | 11 +- tests/unit/ai-providers-credentials.test.ts | 113 +- tests/unit/ai-providers-google.test.ts | 57 +- tests/unit/ai-providers.test.ts | 156 +- tests/unit/api-access-code.test.ts | 8 +- tests/unit/app-menu.test.ts | 104 - tests/unit/bedrock-api-key.test.ts | 1 - tests/unit/chat-helpers.test.ts | 86 - tests/unit/chat-route-edgeone.test.ts | 59 +- tests/unit/chat-route-errors.test.ts | 61 +- tests/unit/chat-route-quota.test.ts | 54 +- tests/unit/client-ip.test.ts | 15 +- tests/unit/compact-cells.test.ts | 5 +- tests/unit/config-manager.test.ts | 109 - tests/unit/cross-site-requests.test.ts | 8 +- tests/unit/diagram-context-exports.test.tsx | 7 +- tests/unit/diagram-context.test.tsx | 40 +- tests/unit/diagram-pages.test.ts | 19 +- tests/unit/edgeone-function.test.ts | 27 +- tests/unit/env-loader.test.ts | 111 - tests/unit/log-save-route.test.ts | 6 +- tests/unit/mcp-core.test.ts | 177 +- tests/unit/mcp-preview-recovery.test.ts | 2 +- tests/unit/output-token-limit.test.ts | 115 +- tests/unit/provider-models.test.ts | 51 +- tests/unit/redesign-helpers.test.ts | 25 +- tests/unit/server-model-config.test.ts | 59 +- tests/unit/ssrf-protection.test.ts | 7 +- tests/unit/style-classes.test.ts | 7 +- tests/unit/template-storage.test.ts | 5 - tests/unit/use-diagram-streaming.test.tsx | 29 + tests/unit/use-file-processor.test.tsx | 8 +- tests/unit/use-model-config.test.tsx | 42 - tests/unit/use-session-manager.test.tsx | 1 - tests/unit/utils.test.ts | 34 - tests/unit/validate-diagram-route.test.ts | 52 +- tests/unit/validate-model-route.test.ts | 184 +- vitest.config.mts | 8 +- wrangler.jsonc | 6 - 227 files changed, 5973 insertions(+), 18897 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 .github/workflows/ci.yml delete mode 100644 app/[lang]/about/cn/page.tsx delete mode 100644 app/[lang]/about/ja/page.tsx delete mode 100644 app/api/chat/xml_guide.md delete mode 100644 components/ai-elements/model-selector.tsx delete mode 100644 components/button-with-tooltip.tsx delete mode 100644 components/chat/TemplateCreateDialog.tsx rename components/chat/{TemplateEditDialog.tsx => TemplateFormDialog.tsx} (68%) delete mode 100644 components/error-toast.tsx create mode 100644 components/icons/github.tsx delete mode 100644 electron/main/settings-window.ts delete mode 100644 electron/preload/settings.ts delete mode 100644 electron/settings/index.html delete mode 100644 electron/settings/settings.css delete mode 100644 electron/settings/settings.js create mode 100644 lib/test-model-connection.ts create mode 100644 packages/mcp-server/tests/setup.ts delete mode 100644 resources/entitlements.mac.plist create mode 100644 scripts/copy-dereferenced.cjs delete mode 100644 tests/e2e/copy-paste.spec.ts delete mode 100644 tests/e2e/history.spec.ts delete mode 100644 tests/e2e/model-config.spec.ts delete mode 100644 tests/e2e/save.spec.ts create mode 100644 tests/e2e/settings-bedrock.spec.ts delete mode 100644 tests/e2e/settings-review-fixes.spec.ts create mode 100644 tests/e2e/settings-tabs-sync.spec.ts delete mode 100644 tests/e2e/theme.spec.ts delete mode 100644 tests/e2e/upload.spec.ts delete mode 100644 tests/unit/app-menu.test.ts delete mode 100644 tests/unit/config-manager.test.ts delete mode 100644 tests/unit/env-loader.test.ts diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 98ef693e..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals", "next/typescript"] -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0566d3f4..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,42 +0,0 @@ -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 - diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 66ac4e7d..46f9ea6b 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -4,14 +4,11 @@ on: push: branches: - main - - master - - dev tags: - 'v*' pull_request: branches: - main - - master env: REGISTRY: ghcr.io diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0645bff7..c90a969e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,13 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: lint-and-unit: name: Lint & Unit Tests @@ -16,12 +23,15 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" cache: "npm" - name: Install dependencies run: npm ci + - name: Type check + run: npx tsc --noEmit + - name: Run lint run: npm run check @@ -141,7 +151,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" cache: "npm" - name: Install dependencies diff --git a/app/[lang]/about/cn/page.tsx b/app/[lang]/about/cn/page.tsx deleted file mode 100644 index 2f1ce910..00000000 --- a/app/[lang]/about/cn/page.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import type { Metadata } from "next" -import Link from "next/link" -import { FaGithub } from "react-icons/fa" -import Image from "@/components/image-with-basepath" - -export const metadata: Metadata = { - title: "关于 - Next AI Draw.io", - description: - "AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。", - keywords: ["AI图表", "draw.io", "AWS架构", "GCP图表", "Azure图表", "LLM"], -} - -export default function AboutCN() { - return ( -
- {/* Navigation */} -
-
-
- - Next AI Draw.io - - -
-
-
- - {/* Main Content */} -
-
- {/* Title */} -
-

- Next AI Draw.io -

-

- AI驱动的图表创建工具 - 对话、绘制、可视化 -

-
- -
-
-
- {/* Bring Your Own Key */} -
-

- 使用自己的 API Key -

-

- 您也可以使用自己的 API - Key,支持多种服务商。点击聊天面板中的设置图标即可配置。 -

-

- 您的 Key - 仅保存在浏览器本地,不会被存储在服务器上。 -

-
-
-
- -

- 一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。 -

- - {/* Features */} -

- 功能特性 -

-
    -
  • - LLM驱动的图表创建 - :利用大语言模型通过自然语言命令直接创建和操作draw.io图表 -
  • -
  • - 基于图像的图表复制 - :上传现有图表或图像,让AI自动复制和增强 -
  • -
  • - 版本与撤销 - :AI - 的每次修改都会成为聊天里的一个版本,可以对比、恢复或撤销;在画布上按 - Ctrl+Z 也能撤回 -
  • -
  • - 交互式聊天界面 - :与AI实时对话来完善您的图表 -
  • -
  • - AWS架构图支持 - :专门支持生成AWS架构图 -
  • -
  • - 动画连接器 - :在图表元素之间创建动态动画连接器,实现更好的可视化效果 -
  • -
- - {/* Examples */} -

- 示例 -

-

- 以下是一些示例提示词及其生成的图表: -

- -
- {/* ResNet50 Architecture */} -
-

- ResNet50模型架构动画 -

-

- Prompt: Give me an{" "} - animated architecture diagram - of the ResNet50 model. -

-
- ResNet50模型架构图 -
-
- - {/* Diagram Grid */} -
-
-

- RAG技术图 -

-

- Prompt: Generate a RAG - architecture diagram for{" "} - chat application. Use - connected diagram for data ingestion -

-
- RAG架构图 -
-
-
-

- React和AWS认证流程 -

-

- Prompt: Generate - authentication process using React with{" "} - AWS. Use Serverless - architecture. -

-
- 认证架构图 -
-
-
-

- 敏捷Scrum流程 -

-

- Prompt: Generate agile - scrum workflow diagram for software - development team. -

-
- 敏捷Scrum流程图 -
-
-
-

- 开放式创新 -

-

- Prompt: Create - visualization of Henry Chesbrough's - Open Innovation model. -

-
- 开放式创新图 -
-
-
-
- - {/* How It Works */} -

- 工作原理 -

-

本应用使用以下技术:

-
    -
  • - Next.js:用于前端框架和路由 -
  • -
  • - Vercel AI SDK(ai +{" "} - @ai-sdk/* - ):用于流式AI响应和多提供商支持 -
  • -
  • - draw.io - :内置在应用里的编辑器,应用可以直接控制它 -
  • -
-

- 图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。 -

- - {/* Multi-Provider Support */} -

- 多提供商支持 -

-
    -
  • 字节跳动豆包
  • -
  • AWS Bedrock(默认)
  • -
  • - OpenAI / OpenAI兼容API(通过{" "} - OPENAI_BASE_URL) -
  • -
  • Anthropic
  • -
  • Google AI
  • -
  • Google Vertex AI
  • -
  • Azure OpenAI
  • -
  • Ollama
  • -
  • OpenRouter
  • -
  • DeepSeek
  • -
  • SiliconFlow
  • -
  • ModelScope
  • -
-

- 注意:claude-sonnet-4-5{" "} - 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。 -

- - {/* Support */} -

- 支持与联系 -

-

- 如果您觉得这个项目有用,请考虑{" "} - - 赞助 - {" "} - 来帮助托管在线演示站点! -

-

- 如需支持或咨询,请在{" "} - - GitHub仓库 - {" "} - 上提交issue或联系:me[at]jiang.jp -

- - {/* CTA */} -
- - 打开编辑器 - -
-
-
- - {/* Footer */} -
-
-

- Next AI Draw.io - 开源AI驱动的图表生成器 -

-
-
-
- ) -} diff --git a/app/[lang]/about/ja/page.tsx b/app/[lang]/about/ja/page.tsx deleted file mode 100644 index bbe311fa..00000000 --- a/app/[lang]/about/ja/page.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import type { Metadata } from "next" -import Link from "next/link" -import { FaGithub } from "react-icons/fa" -import Image from "@/components/image-with-basepath" - -export const metadata: Metadata = { - title: "概要 - Next AI Draw.io", - description: - "AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。", - keywords: [ - "AIダイアグラム", - "draw.io", - "AWSアーキテクチャ", - "GCPダイアグラム", - "Azureダイアグラム", - "LLM", - ], -} - -export default function AboutJA() { - return ( -
- {/* Navigation */} -
-
-
- - Next AI Draw.io - - -
-
-
- - {/* Main Content */} -
-
- {/* Title */} -
-

- Next AI Draw.io -

-

- AI搭載のダイアグラム作成ツール - - チャット、描画、可視化 -

-
- -
-
-
- {/* Bring Your Own Key */} -
-

- 自分のAPIキーを使用 -

-

- お好みのプロバイダーで自分のAPIキーを使用することもできます。チャットパネルの設定アイコンをクリックして設定してください。 -

-

- キーはブラウザのローカルに保存され、サーバーには保存されません。 -

-
-
-
- -

- AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。 -

- - {/* Features */} -

- 機能 -

-
    -
  • - LLM搭載のダイアグラム作成 - :大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作 -
  • -
  • - 画像ベースのダイアグラム複製 - :既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化 -
  • -
  • - バージョンと取り消し - :AI - の変更はそれぞれチャット内のバージョンになり、比較・復元・取り消しができます。キャンバスで - Ctrl+Z を押しても元に戻せます -
  • -
  • - - インタラクティブなチャットインターフェース - - :AIとリアルタイムでコミュニケーションしてダイアグラムを改善 -
  • -
  • - - AWSアーキテクチャダイアグラムサポート - - :AWSアーキテクチャダイアグラムの生成を専門的にサポート -
  • -
  • - アニメーションコネクタ - :より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成 -
  • -
- - {/* Examples */} -

- 例 -

-

- 以下はいくつかのプロンプト例と生成されたダイアグラムです: -

- -
- {/* ResNet50 Architecture */} -
-

- ResNet50モデルアーキテクチャアニメーション -

-

- Prompt: Give me an{" "} - animated architecture diagram - of the ResNet50 model. -

-
- ResNet50モデルアーキテクチャ図 -
-
- - {/* Diagram Grid */} -
-
-

- RAG技術ダイアグラム -

-

- Prompt: Generate a RAG - architecture diagram for{" "} - chat application. Use - connected diagram for data ingestion -

-
- RAGアーキテクチャ図 -
-
-
-

- ReactとAWSによる認証 -

-

- Prompt: Generate - authentication process using React with{" "} - AWS. Use Serverless - architecture. -

-
- 認証アーキテクチャ図 -
-
-
-

- アジャイルスクラムプロセス -

-

- Prompt: Generate agile - scrum workflow diagram for software - development team. -

-
- アジャイルスクラム図 -
-
-
-

- オープンイノベーション -

-

- Prompt: Create - visualization of Henry Chesbrough's - Open Innovation model. -

-
- オープンイノベーション図 -
-
-
-
- - {/* How It Works */} -

- 仕組み -

-

- 本アプリケーションは以下の技術を使用しています: -

-
    -
  • - Next.js - :フロントエンドフレームワークとルーティング -
  • -
  • - Vercel AI SDK(ai +{" "} - @ai-sdk/* - ):ストリーミングAIレスポンスとマルチプロバイダーサポート -
  • -
  • - draw.io - :アプリに同梱されたエディター。アプリから直接操作できます -
  • -
-

- ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。 -

- - {/* Multi-Provider Support */} -

- マルチプロバイダーサポート -

-
    -
  • ByteDance Doubao
  • -
  • AWS Bedrock(デフォルト)
  • -
  • - OpenAI / OpenAI互換API(OPENAI_BASE_URL - 経由) -
  • -
  • Anthropic
  • -
  • Google AI
  • -
  • Google Vertex AI
  • -
  • Azure OpenAI
  • -
  • Ollama
  • -
  • OpenRouter
  • -
  • DeepSeek
  • -
  • SiliconFlow
  • -
  • ModelScope
  • -
-

- 注:claude-sonnet-4-5 - はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。 -

- - {/* Support */} -

- サポート&お問い合わせ -

-

- このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために{" "} - - スポンサー - {" "} - をご検討ください! -

-

- サポートやお問い合わせについては、{" "} - - GitHubリポジトリ - {" "} - でissueを開くか、ご連絡ください:me[at]jiang.jp -

- - {/* CTA */} -
- - エディタを開く - -
-
-
- - {/* Footer */} -
-
-

- Next AI Draw.io - - オープンソースAI搭載ダイアグラムジェネレーター -

-
-
-
- ) -} diff --git a/app/[lang]/about/page.tsx b/app/[lang]/about/page.tsx index 2183e402..6e9b08d4 100644 --- a/app/[lang]/about/page.tsx +++ b/app/[lang]/about/page.tsx @@ -1,23 +1,420 @@ import type { Metadata } from "next" import Link from "next/link" -import { FaGithub } from "react-icons/fa" +import type { ReactNode } from "react" +import { GitHubIcon } from "@/components/icons/github" import Image from "@/components/image-with-basepath" +import type { Locale } from "@/lib/i18n/config" +import { hasLocale } from "@/lib/i18n/dictionaries" -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", - ], +type ExampleId = "resnet50" | "rag" | "auth" | "scrum" | "inno" + +type AboutTexts = { + title: string + description: string + keywords: string[] + nav: { editor: string; about: string; github: string } + tagline: string + byok: { title: string; body: string; note: string } + intro: string + // Punctuation differs between Latin and CJK text + colon: string + parenOpen: string + parenClose: string + featuresHeading: string + features: { title: string; text: string }[] + examplesHeading: string + examplesIntro: string + examples: Record + howHeading: string + howIntro: string + tech: { nextjs: string; aiSdk: string; drawio: string } + howOutro: string + providersHeading: string + doubao: string + bedrock: string + // Text around the OPENAI_BASE_URL code span + openai: { before: string; after: string } + // Text around the claude-sonnet-4-5 code span + modelNote: { before: string; after: string } + supportHeading: string + // Text around the sponsor link + sponsor: { before: string; link: string; after: string } + // Text around the GitHub repository link + contact: { before: string; link: string; after: string } + openEditor: string + footer: string } -export default function About() { +const texts: Record<"en" | "zh" | "ja", AboutTexts> = { + en: { + 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", + ], + nav: { editor: "Editor", about: "About", github: "View on GitHub" }, + tagline: "AI-Powered Diagram Creation Tool - Chat, Draw, Visualize", + byok: { + title: "Bring Your Own API Key", + body: "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.", + note: "Your key is stored locally in your browser and is never stored on the server.", + }, + intro: "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.", + colon: ": ", + parenOpen: " (", + parenClose: ")", + featuresHeading: "Features", + features: [ + { + title: "LLM-Powered Diagram Creation", + text: "Leverage Large Language Models to create and manipulate draw.io diagrams directly through natural language commands", + }, + { + title: "Image-Based Diagram Replication", + text: "Upload existing diagrams or images and have the AI replicate and enhance them automatically", + }, + { + title: "Versions and Undo", + text: "Every AI change becomes a version in the chat that you can compare, restore or undo; Ctrl+Z on the canvas takes it back too", + }, + { + title: "Interactive Chat Interface", + text: "Communicate with AI to refine your diagrams in real-time", + }, + { + title: "AWS Architecture Diagram Support", + text: "Specialized support for generating AWS architecture diagrams", + }, + { + title: "Animated Connectors", + text: "Create dynamic and animated connectors between diagram elements for better visualization", + }, + ], + examplesHeading: "Examples", + examplesIntro: + "Here are some example prompts and their generated diagrams:", + examples: { + resnet50: { + title: "Animated ResNet50 Model Architecture", + alt: "Architecture diagram for ResNet50 model", + }, + rag: { + title: "RAG Technique Diagram", + alt: "RAG Architecture Diagram", + }, + auth: { + title: "Authentication using React and AWS", + alt: "Authentication Architecture Diagram", + }, + scrum: { title: "Agile Scrum Process", alt: "Agile Scrum Diagram" }, + inno: { title: "Open Innovation", alt: "Open Innovation Diagram" }, + }, + howHeading: "How It Works", + howIntro: "The application uses the following technologies:", + tech: { + nextjs: "For the frontend framework and routing", + aiSdk: "For streaming AI responses and multi-provider support", + drawio: "The editor, bundled with the app so it can be driven directly", + }, + howOutro: + "Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly.", + providersHeading: "Multi-Provider Support", + doubao: "ByteDance Doubao", + bedrock: "AWS Bedrock (default)", + openai: { before: "OpenAI / OpenAI-compatible APIs (via ", after: ")" }, + modelNote: { + before: "Note that ", + after: " has trained on draw.io diagrams with AWS logos, so if you want to create AWS architecture diagrams, this is the best choice.", + }, + supportHeading: "Support & Contact", + sponsor: { + before: "If you find this project useful, please consider", + link: "sponsoring", + after: "to help host the live demo site!", + }, + contact: { + before: "For support or inquiries, please open an issue on the", + link: "GitHub repository", + after: "or contact: me[at]jiang.jp", + }, + openEditor: "Open Editor", + footer: "Next AI Draw.io - Open Source AI-Powered Diagram Generator", + }, + zh: { + title: "关于 - Next AI Draw.io", + description: + "AI驱动的图表创建工具 - 对话、绘制、可视化。使用自然语言创建AWS、GCP和Azure架构图。", + keywords: [ + "AI图表", + "draw.io", + "AWS架构", + "GCP图表", + "Azure图表", + "LLM", + ], + nav: { editor: "编辑器", about: "关于", github: "在GitHub上查看" }, + tagline: "AI驱动的图表创建工具 - 对话、绘制、可视化", + byok: { + title: "使用自己的 API Key", + body: "您也可以使用自己的 API Key,支持多种服务商。点击聊天面板中的设置图标即可配置。", + note: "您的 Key 仅保存在浏览器本地,不会被存储在服务器上。", + }, + intro: "一个集成了AI功能的Next.js网页应用,与draw.io图表无缝结合。通过自然语言命令和AI辅助可视化来创建、修改和增强图表。", + colon: ":", + parenOpen: "(", + parenClose: ")", + featuresHeading: "功能特性", + features: [ + { + title: "LLM驱动的图表创建", + text: "利用大语言模型通过自然语言命令直接创建和操作draw.io图表", + }, + { + title: "基于图像的图表复制", + text: "上传现有图表或图像,让AI自动复制和增强", + }, + { + title: "版本与撤销", + text: "AI 的每次修改都会成为聊天里的一个版本,可以对比、恢复或撤销;在画布上按 Ctrl+Z 也能撤回", + }, + { + title: "交互式聊天界面", + text: "与AI实时对话来完善您的图表", + }, + { title: "AWS架构图支持", text: "专门支持生成AWS架构图" }, + { + title: "动画连接器", + text: "在图表元素之间创建动态动画连接器,实现更好的可视化效果", + }, + ], + examplesHeading: "示例", + examplesIntro: "以下是一些示例提示词及其生成的图表:", + examples: { + resnet50: { + title: "ResNet50模型架构动画", + alt: "ResNet50模型架构图", + }, + rag: { title: "RAG技术图", alt: "RAG架构图" }, + auth: { title: "React和AWS认证流程", alt: "认证架构图" }, + scrum: { title: "敏捷Scrum流程", alt: "敏捷Scrum流程图" }, + inno: { title: "开放式创新", alt: "开放式创新图" }, + }, + howHeading: "工作原理", + howIntro: "本应用使用以下技术:", + tech: { + nextjs: "用于前端框架和路由", + aiSdk: "用于流式AI响应和多提供商支持", + drawio: "内置在应用里的编辑器,应用可以直接控制它", + }, + howOutro: + "图表以XML格式表示,可在draw.io中渲染。AI处理您的命令并相应地生成或修改此XML。", + providersHeading: "多提供商支持", + doubao: "字节跳动豆包", + bedrock: "AWS Bedrock(默认)", + openai: { before: "OpenAI / OpenAI兼容API(通过 ", after: ")" }, + modelNote: { + before: "注意:", + after: " 已在带有AWS标志的draw.io图表上进行训练,因此如果您想创建AWS架构图,这是最佳选择。", + }, + supportHeading: "支持与联系", + sponsor: { + before: "如果您觉得这个项目有用,请考虑", + link: "赞助", + after: "来帮助托管在线演示站点!", + }, + contact: { + before: "如需支持或咨询,请在", + link: "GitHub仓库", + after: "上提交issue或联系:me[at]jiang.jp", + }, + openEditor: "打开编辑器", + footer: "Next AI Draw.io - 开源AI驱动的图表生成器", + }, + ja: { + title: "概要 - Next AI Draw.io", + description: + "AI搭載のダイアグラム作成ツール - チャット、描画、可視化。自然言語でAWS、GCP、Azureアーキテクチャ図を作成。", + keywords: [ + "AIダイアグラム", + "draw.io", + "AWSアーキテクチャ", + "GCPダイアグラム", + "Azureダイアグラム", + "LLM", + ], + nav: { editor: "エディタ", about: "概要", github: "GitHubで見る" }, + tagline: "AI搭載のダイアグラム作成ツール - チャット、描画、可視化", + byok: { + title: "自分のAPIキーを使用", + body: "お好みのプロバイダーで自分のAPIキーを使用することもできます。チャットパネルの設定アイコンをクリックして設定してください。", + note: "キーはブラウザのローカルに保存され、サーバーには保存されません。", + }, + intro: "AI機能とdraw.ioダイアグラムを統合したNext.jsウェブアプリケーションです。自然言語コマンドとAI支援の可視化により、ダイアグラムを作成、修正、強化できます。", + colon: ":", + parenOpen: "(", + parenClose: ")", + featuresHeading: "機能", + features: [ + { + title: "LLM搭載のダイアグラム作成", + text: "大規模言語モデルを活用して、自然言語コマンドで直接draw.ioダイアグラムを作成・操作", + }, + { + title: "画像ベースのダイアグラム複製", + text: "既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化", + }, + { + title: "バージョンと取り消し", + text: "AI の変更はそれぞれチャット内のバージョンになり、比較・復元・取り消しができます。キャンバスで Ctrl+Z を押しても元に戻せます", + }, + { + title: "インタラクティブなチャットインターフェース", + text: "AIとリアルタイムでコミュニケーションしてダイアグラムを改善", + }, + { + title: "AWSアーキテクチャダイアグラムサポート", + text: "AWSアーキテクチャダイアグラムの生成を専門的にサポート", + }, + { + title: "アニメーションコネクタ", + text: "より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成", + }, + ], + examplesHeading: "例", + examplesIntro: + "以下はいくつかのプロンプト例と生成されたダイアグラムです:", + examples: { + resnet50: { + title: "ResNet50モデルアーキテクチャアニメーション", + alt: "ResNet50モデルアーキテクチャ図", + }, + rag: { title: "RAG技術ダイアグラム", alt: "RAGアーキテクチャ図" }, + auth: { + title: "ReactとAWSによる認証", + alt: "認証アーキテクチャ図", + }, + scrum: { + title: "アジャイルスクラムプロセス", + alt: "アジャイルスクラム図", + }, + inno: { + title: "オープンイノベーション", + alt: "オープンイノベーション図", + }, + }, + howHeading: "仕組み", + howIntro: "本アプリケーションは以下の技術を使用しています:", + tech: { + nextjs: "フロントエンドフレームワークとルーティング", + aiSdk: "ストリーミングAIレスポンスとマルチプロバイダーサポート", + drawio: "アプリに同梱されたエディター。アプリから直接操作できます", + }, + howOutro: + "ダイアグラムはdraw.ioでレンダリングできるXMLとして表現されます。AIがコマンドを処理し、それに応じてこのXMLを生成または変更します。", + providersHeading: "マルチプロバイダーサポート", + doubao: "ByteDance Doubao", + bedrock: "AWS Bedrock(デフォルト)", + openai: { before: "OpenAI / OpenAI互換API(", after: "経由)" }, + modelNote: { + before: "注:", + after: "はAWSロゴ付きのdraw.ioダイアグラムで学習されているため、AWSアーキテクチャダイアグラムを作成したい場合は最適な選択です。", + }, + supportHeading: "サポート&お問い合わせ", + sponsor: { + before: "このプロジェクトが役に立ったら、ライブデモサイトのホスティングを支援するために", + link: "スポンサー", + after: "をご検討ください!", + }, + contact: { + before: "サポートやお問い合わせについては、", + link: "GitHubリポジトリ", + after: "でissueを開くか、ご連絡ください:me[at]jiang.jp", + }, + openEditor: "エディタを開く", + footer: "Next AI Draw.io - オープンソースAI搭載ダイアグラムジェネレーター", + }, +} + +// The example prompts stay in English in every language +const examples: { id: ExampleId; src: string; prompt: ReactNode }[] = [ + { + id: "resnet50", + src: "/resnet50.svg", + prompt: ( + <> + Give me an animated architecture diagram of the + ResNet50 model. + + ), + }, + { + id: "rag", + src: "/rag_prod.svg", + prompt: ( + <> + Generate a RAG architecture diagram for{" "} + chat application. Use connected diagram for + data ingestion + + ), + }, + { + id: "auth", + src: "/auth.svg", + prompt: ( + <> + Generate authentication process using React with{" "} + AWS. Use Serverless architecture. + + ), + }, + { + id: "scrum", + src: "/agile_scrum.svg", + prompt: ( + <> + Generate agile scrum workflow diagram for software development + team. + + ), + }, + { + id: "inno", + src: "/inno.svg", + prompt: ( + <> + Create visualization of Henry Chesbrough's Open Innovation + model. + + ), + }, +] + +// Traditional Chinese has no About text of its own and shows the Simplified one +function textsFor(rawLang: string): AboutTexts { + const lang: Locale = hasLocale(rawLang) ? rawLang : "en" + return texts[lang === "zh-Hant" ? "zh" : lang] +} + +type Props = { params: Promise<{ lang: string }> } + +export async function generateMetadata({ params }: Props): Promise { + const { lang } = await params + const t = textsFor(lang) + return { title: t.title, description: t.description, keywords: t.keywords } +} + +export default async function About({ params }: Props) { + const { lang } = await params + const t = textsFor(lang) + const [hero, ...grid] = examples + return (
{/* Navigation */} @@ -25,32 +422,32 @@ export default function About() {
Next AI Draw.io
@@ -66,8 +463,7 @@ export default function About() { Next AI Draw.io

- AI-Powered Diagram Creation Tool - Chat, Draw, - Visualize + {t.tagline}

@@ -77,92 +473,53 @@ export default function About() { {/* Bring Your Own Key */}

- Bring Your Own API Key + {t.byok.title}

- 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. + {t.byok.body}

- Your key is stored locally in your browser - and is never stored on the server. + {t.byok.note}

-

- 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. -

+

{t.intro}

{/* Features */}

- Features + {t.featuresHeading}

    -
  • - 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 -
  • -
  • - Versions and Undo: Every AI change - becomes a version in the chat that you can compare, - restore or undo; Ctrl+Z on the canvas takes it back - too -
  • -
  • - 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 -
  • + {t.features.map((feature) => ( +
  • + {feature.title} + {t.colon} + {feature.text} +
  • + ))}
{/* Examples */}

- Examples + {t.examplesHeading}

-

- Here are some example prompts and their generated - diagrams: -

+

{t.examplesIntro}

- {/* ResNet50 Architecture */} + {/* First example on its own */}

- Animated ResNet50 Model Architecture + {t.examples[hero.id].title}

- Prompt: Give me an{" "} - animated architecture diagram - of the ResNet50 model. + Prompt: {hero.prompt}

Architecture diagram for ResNet50 model -
-

- RAG Technique Diagram -

-

- Prompt: Generate a RAG - architecture diagram for{" "} - chat application. Use - connected diagram for data ingestion -

-
- RAG Architecture Diagram + {grid.map((example) => ( +
+

+ {t.examples[example.id].title} +

+

+ Prompt:{" "} + {example.prompt} +

+
+ {t.examples[example.id].alt} +
-
-
-

- Authentication using React and AWS -

-

- Prompt: Generate - authentication process using React with{" "} - AWS. Use Serverless - architecture. -

-
- Authentication Architecture Diagram -
-
-
-

- Agile Scrum Process -

-

- Prompt: Generate agile - scrum workflow diagram for software - development team. -

-
- Agile Scrum Diagram -
-
-
-

- Open Innovation -

-

- Prompt: Create - visualization of Henry Chesbrough's - Open Innovation model. -

-
- Open Innovation Diagram -
-
+ ))}
{/* How It Works */}

- How It Works + {t.howHeading}

-

- The application uses the following technologies: -

+

{t.howIntro}

  • - Next.js: For the frontend framework - and routing + Next.js + {t.colon} + {t.tech.nextjs}
  • - Vercel AI SDK (ai +{" "} - @ai-sdk/*): For streaming AI responses - and multi-provider support + Vercel AI SDK + {t.parenOpen} + ai + @ai-sdk/* + {t.parenClose} + {t.colon} + {t.tech.aiSdk}
  • - draw.io: The editor, bundled with - the app so it can be driven directly + draw.io + {t.colon} + {t.tech.drawio}
-

- Diagrams are represented as XML that can be rendered in - draw.io. The AI processes your commands and generates or - modifies this XML accordingly. -

+

{t.howOutro}

{/* Multi-Provider Support */}

- Multi-Provider Support + {t.providersHeading}

    -
  • ByteDance Doubao
  • -
  • AWS Bedrock (default)
  • +
  • {t.doubao}
  • +
  • {t.bedrock}
  • - OpenAI / OpenAI-compatible APIs (via{" "} - OPENAI_BASE_URL) + {t.openai.before} + OPENAI_BASE_URL + {t.openai.after}
  • Anthropic
  • Google AI
  • @@ -303,48 +602,47 @@ export default function About() {
  • ModelScope

- 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. + {t.modelNote.before} + claude-sonnet-4-5 + {t.modelNote.after}

{/* Support */}

- Support & Contact + {t.supportHeading}

- If you find this project useful, please consider{" "} + {t.sponsor.before}{" "} - sponsoring + {t.sponsor.link} {" "} - to help host the live demo site! + {t.sponsor.after}

- For support or inquiries, please open an issue on the{" "} + {t.contact.before}{" "} - GitHub repository + {t.contact.link} {" "} - or contact: me[at]jiang.jp + {t.contact.after}

{/* CTA */}
- Open Editor + {t.openEditor}
@@ -354,8 +652,7 @@ export default function About() {

- Next AI Draw.io - Open Source AI-Powered Diagram - Generator + {t.footer}

diff --git a/app/[lang]/admin/admin-shared.ts b/app/[lang]/admin/admin-shared.ts index fc93310e..fc8b5284 100644 --- a/app/[lang]/admin/admin-shared.ts +++ b/app/[lang]/admin/admin-shared.ts @@ -1,3 +1,5 @@ +import type { MaskedSecret as SecretValue } from "@/lib/admin/auth" +import type { AdminProviderInput as AdminProvider } from "@/lib/admin/providers" import { getApiEndpoint } from "@/lib/base-path" import type { ProviderName } from "@/lib/types/model-config" @@ -5,7 +7,10 @@ export const SESSION_PASSWORD_KEY = "next-ai-draw-io-admin-password" // ── Shared types ───────────────────────────────────────────────────── -export type SecretValue = { isSet: true; hint: string } +// The same shapes the admin API validates and returns. Secret fields of a +// provider hold either a masked marker (unchanged) or a plaintext string +// (new value). +export type { AdminProvider, SecretValue } export function isSecretValue(v: unknown): v is SecretValue { return typeof v === "object" && v !== null && "isSet" in v @@ -24,22 +29,6 @@ export function savedTextOf(state: SettingState | undefined): string { return state && !isSecretValue(state.value) ? (state.value ?? "") : "" } -// Admin provider in client state. Secret fields hold either a masked -// marker (unchanged) or a plaintext string (new value). -export interface AdminProvider { - id: string - provider: ProviderName - name?: string - apiKey?: string | SecretValue - baseUrl?: string - awsAccessKeyId?: string | SecretValue - awsSecretAccessKey?: string | SecretValue - awsRegion?: string - vertexApiKey?: string | SecretValue - models: string[] - isDefault?: boolean -} - // Provider defined in AI_MODELS_CONFIG / ai-models.json — shown read-only export interface EnvProvider { name: string diff --git a/app/[lang]/admin/setting-field.tsx b/app/[lang]/admin/setting-field.tsx index 2bd02325..d11be1ce 100644 --- a/app/[lang]/admin/setting-field.tsx +++ b/app/[lang]/admin/setting-field.tsx @@ -3,13 +3,6 @@ import { useState } from "react" import { Button } from "@/components/ui/button" 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 type { SettingDef } from "@/lib/admin/settings-registry" @@ -206,26 +199,6 @@ export function SettingField({ ) break } - case "enum": - control = ( - - ) - break case "secret": control = (
diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index 2e09e6b9..ede4a684 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -41,6 +41,13 @@ export async function generateStaticParams() { return i18n.locales.map((locale) => ({ lang: locale })) } +const OG_LOCALE: Record = { + en: "en_US", + zh: "zh_CN", + ja: "ja_JP", + "zh-Hant": "zh_HK", +} + // Generate metadata per locale export async function generateMetadata({ params, @@ -48,22 +55,19 @@ export async function generateMetadata({ params: Promise<{ lang: string }> }): Promise { const { lang: rawLang } = await params - const lang = ( - rawLang in { en: 1, zh: 1, ja: 1, "zh-Hant": 1 } ? rawLang : "en" - ) as Locale + const lang: Locale = hasLocale(rawLang) ? rawLang : i18n.defaultLocale - // Default to English metadata const titles: Record = { en: "Next AI Draw.io - AI-Powered Diagram Generator", - zh: "Next AI Draw.io - AI powered diagram generator", - ja: "Next AI Draw.io - AI-powered diagram generator", + zh: "Next AI Draw.io - AI 驱动的图表生成器", + ja: "Next AI Draw.io - AI 搭載ダイアグラム生成ツール", "zh-Hant": "Next AI Draw.io - AI 驅動的圖表產生器", } const descriptions: Record = { en: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.", - zh: "Use AI to create AWS architecture diagrams, flowcharts, and technical diagrams. Free online tool integrated with draw.io and AI assistance for professional diagram creation.", - ja: "Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Create professional diagrams with a free online tool that integrates draw.io with an AI assistant.", + zh: "使用 AI 创建 AWS 架构图、流程图和技术图表。免费在线工具,整合 draw.io 与 AI 辅助,轻松创建专业图表。", + ja: "AI で AWS アーキテクチャ図、フローチャート、技術図を作成。draw.io と AI アシスタントを統合した無料オンラインツールで、プロ品質の図を簡単に作成できます。", "zh-Hant": "使用 AI 建立 AWS 架構圖、流程圖和技術圖表。免費線上工具整合 draw.io 與 AI 輔助,輕鬆建立專業圖表。", } @@ -92,14 +96,7 @@ export async function generateMetadata({ type: "website", url: "https://next-ai-drawio.jiang.jp", siteName: "Next AI Draw.io", - locale: - lang === "zh" - ? "zh_CN" - : lang === "zh-Hant" - ? "zh_HK" - : lang === "ja" - ? "ja_JP" - : "en_US", + locale: OG_LOCALE[lang], images: [ { url: "/architecture.png", diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts index d4d09755..183c0777 100644 --- a/app/api/admin/settings/route.ts +++ b/app/api/admin/settings/route.ts @@ -53,10 +53,6 @@ function validateValue(def: SettingDef, value: string): string | null { return value === "true" || value === "false" ? null : 'Must be "true" or "false"' - case "enum": - return def.options?.includes(value) - ? null - : `Must be one of: ${def.options?.join(", ")}` default: return null } diff --git a/app/api/admin/test-model/route.ts b/app/api/admin/test-model/route.ts index 51999ca8..4cb886b1 100644 --- a/app/api/admin/test-model/route.ts +++ b/app/api/admin/test-model/route.ts @@ -1,4 +1,3 @@ -import { POST as validateModel } from "@/app/api/validate-model/route" import { checkAdminAuth } from "@/lib/admin/auth" import { AdminProviderSchema, @@ -6,6 +5,7 @@ import { mergeSecrets, } from "@/lib/admin/providers" import { globalBaseUrl } from "@/lib/ai-providers" +import { testModelConnection } from "@/lib/test-model-connection" export const runtime = "nodejs" export const dynamic = "force-dynamic" @@ -48,37 +48,24 @@ export async function POST(req: Request) { sameEndpoint && stored ? [stored] : [], ) + // Without a URL of its own, chat sends the entry's key to the server's + //

_BASE_URL: test that endpoint, not another one. It is the server's + // own, which chat uses without the checks for a URL a user typed. + // The admin's own request supplies the EdgeOne endpoint, cookies and + // access code, as a chat request would. const serverUrl = globalBaseUrl(resolved.provider) - return validateModel( - new Request(new URL("/api/validate-model", req.url), { - method: "POST", - headers: { - "Content-Type": "application/json", - // Checked again there, in place of an access code - "x-admin-password": req.headers.get("x-admin-password") || "", - // The EdgeOne function checks the access code and Pages - // cookies, and its URL is built from the page's origin - "x-access-code": req.headers.get("x-access-code") || "", - cookie: req.headers.get("cookie") || "", - ...(req.headers.get("origin") && { - origin: req.headers.get("origin") as string, - }), - }, - body: JSON.stringify({ - provider: resolved.provider, - apiKey: resolved.apiKey, - // Without a URL of its own, chat sends the entry's key to - // the server's

_BASE_URL: test that endpoint, not - // another one. It is the server's own, which chat uses - // without the checks for a URL a user typed. - baseUrl: resolved.baseUrl || serverUrl, - ...(!resolved.baseUrl && serverUrl && { serverBaseUrl: true }), - modelId: body.modelId, - awsAccessKeyId: resolved.awsAccessKeyId, - awsSecretAccessKey: resolved.awsSecretAccessKey, - awsRegion: resolved.awsRegion, - vertexApiKey: resolved.vertexApiKey, - }), - }), + return testModelConnection( + req, + { + provider: resolved.provider, + apiKey: resolved.apiKey, + baseUrl: resolved.baseUrl || serverUrl, + modelId: body.modelId, + awsAccessKeyId: resolved.awsAccessKeyId, + awsSecretAccessKey: resolved.awsSecretAccessKey, + awsRegion: resolved.awsRegion, + vertexApiKey: resolved.vertexApiKey, + }, + !resolved.baseUrl && !!serverUrl, ) } diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index ebd56283..4ea92508 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,8 +1,6 @@ import { APICallError, convertToModelMessages, - createUIMessageStream, - createUIMessageStreamResponse, InvalidToolInputError, stepCountIs, streamText, @@ -21,12 +19,10 @@ import { usesServerCredentials, usesServerEndpoint, } from "@/lib/ai-providers" -import { findCachedResponse } from "@/lib/cached-responses" import { dropInvalidToolCalls, fixToolInputJson, formatSelectionContext, - replaceHistoricalToolInputs, validateFileParts, } from "@/lib/chat-helpers" import { withDeprecatedParamsFallback } from "@/lib/deprecated-params" @@ -56,7 +52,6 @@ import { getSystemPrompt } from "@/lib/system-prompts" import { normalizeBaseUrl } from "@/lib/types/model-config" import { getUserIdFromRequest } from "@/lib/user-id" import { foldCells } from "@/packages/mcp-server/src/compact-cells.ts" -import { hasCells } from "@/packages/mcp-server/src/pages.ts" import { getShapeLibrary, SHAPE_LIBRARY_LIST, @@ -70,36 +65,6 @@ import { // the tool call, so take whatever the host allows. Vercel's own default is 300s, // which is also where Node's response-body timeout on the upstream stream lands. -// Helper function to create cached stream response -function createCachedStreamResponse(xml: string): Response { - const toolCallId = `cached-${Date.now()}` - - const stream = createUIMessageStream({ - execute: async ({ writer }) => { - writer.write({ type: "start" }) - writer.write({ - type: "tool-input-start", - toolCallId, - toolName: "display_diagram", - }) - writer.write({ - type: "tool-input-delta", - toolCallId, - inputTextDelta: xml, - }) - writer.write({ - type: "tool-input-available", - toolCallId, - toolName: "display_diagram", - input: { xml }, - }) - writer.write({ type: "finish" }) - }, - }) - - return createUIMessageStreamResponse({ stream }) -} - // Responses streamed from the model, whose trace streamText's callbacks end const modelStreamResponses = new WeakSet() // A Request's signal follows the client's disconnect only while the Request @@ -111,6 +76,21 @@ const requestOfResponse = new WeakMap() // Inner handler function const DEBUG_LLM_PAYLOAD = process.env.DEBUG_LLM_PAYLOAD === "true" +// Message structure for DEBUG_LLM_PAYLOAD logs: roles, part types, tool names +// and input keys, without the diagram XML or image data +const describeMessages = (msgs: any[]) => + msgs.map((m) => ({ + role: m.role, + parts: (m.parts ?? m.content ?? []).map( + (p: any) => + p.type + + (p.toolName ? `:${p.toolName}` : "") + + (p.input && typeof p.input === "object" + ? `(${Object.keys(p.input).join(",")})` + : ""), + ), + })) + async function handleChatRequest(req: Request): Promise { const crossSite = rejectCrossSite(req) if (crossSite) return crossSite @@ -156,23 +136,6 @@ async function handleChatRequest(req: Request): Promise { } // === FILE VALIDATION END === - // === CACHE CHECK START === - const isFirstMessage = messages.length === 1 - const isEmptyDiagram = !xml || !hasCells(xml) - - if (isFirstMessage && isEmptyDiagram) { - const lastMessage = messages[0] - const textPart = lastMessage.parts?.find((p: any) => p.type === "text") - const filePart = lastMessage.parts?.find((p: any) => p.type === "file") - - const cached = findCachedResponse(textPart?.text || "", !!filePart) - - if (cached) { - return createCachedStreamResponse(cached.xml) - } - } - // === CACHE CHECK END === - // Read client AI provider overrides from headers const provider = req.headers.get("x-ai-provider") let baseUrl = req.headers.get("x-ai-base-url") @@ -348,7 +311,7 @@ async function handleChatRequest(req: Request): Promise { `[Prompt Caching] ${shouldCache ? "ENABLED" : "DISABLED"} for model: ${modelId}`, ) - // Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5) + // System prompt, with the model name filled in and the style section chosen const systemMessage = getSystemPrompt(modelId, minimalStyle) const finalSystemMessage = customSystemMessage ? `${systemMessage}\n\n## Custom Instructions\n${customSystemMessage}` @@ -380,47 +343,15 @@ ${userInputText} // DEBUG_LLM_PAYLOAD=true logs the incoming message structure if (DEBUG_LLM_PAYLOAD) { - console.log("[route.ts] Incoming messages count:", messages.length) - messages.forEach((msg: any, idx: number) => { - console.log( - `[route.ts] Message ${idx} role:`, - msg.role, - "parts count:", - msg.parts?.length, - ) - if (msg.parts) { - msg.parts.forEach((part: any, partIdx: number) => { - if ( - part.type === "tool-invocation" || - part.type === "tool-result" - ) { - console.log(`[route.ts] Part ${partIdx}:`, { - type: part.type, - toolName: part.toolName, - hasInput: !!part.input, - inputType: typeof part.input, - inputKeys: - part.input && typeof part.input === "object" - ? Object.keys(part.input) - : null, - }) - } - }) - } - }) + console.log( + "[route.ts] incoming", + JSON.stringify(describeMessages(messages)), + ) } - // Replace historical tool call XML with placeholders to reduce tokens - // Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML - const enableHistoryReplace = - process.env.ENABLE_HISTORY_XML_REPLACE === "true" - const placeholderMessages = enableHistoryReplace - ? replaceHistoricalToolInputs(modelMessages) - : modelMessages - // Filter out messages with empty content arrays (Bedrock API rejects these) // This is a safety measure - ideally convertToModelMessages should handle all cases - let enhancedMessages = placeholderMessages.filter( + let enhancedMessages = modelMessages.filter( (msg: any) => msg.content && Array.isArray(msg.content) && msg.content.length > 0, ) @@ -432,36 +363,10 @@ ${userInputText} // DEBUG_LLM_PAYLOAD=true logs what is sent to the model if (DEBUG_LLM_PAYLOAD) { - console.log("[route.ts] Model messages count:", enhancedMessages.length) - enhancedMessages.forEach((msg: any, idx: number) => { - console.log( - `[route.ts] ModelMsg ${idx} role:`, - msg.role, - "content count:", - msg.content?.length, - ) - if (msg.content) { - msg.content.forEach((part: any, partIdx: number) => { - if ( - part.type === "tool-call" || - part.type === "tool-result" - ) { - console.log(`[route.ts] Content ${partIdx}:`, { - type: part.type, - toolName: part.toolName, - hasInput: !!part.input, - inputType: typeof part.input, - inputValue: - part.input === undefined - ? "undefined" - : part.input === null - ? "null" - : "object", - }) - } - }) - } - }) + console.log( + "[route.ts] to model", + JSON.stringify(describeMessages(enhancedMessages)), + ) } // Update the last message with user input only (XML moved to separate cached system message) @@ -790,17 +695,6 @@ Call this tool to get shape names and usage syntax for a specific library.`, // Keyless endpoints keep theirs: the desktop app's Ollama is the // user's own, and EdgeOne's text is our function's explanation. onError: (error) => streamErrorText(error, onServerCredentials), - messageMetadata: ({ part }) => { - if (part.type === "finish") { - const usage = (part as any).totalUsage - // AI SDK 6 provides totalTokens directly - return { - totalTokens: usage?.totalTokens ?? 0, - finishReason: (part as any).finishReason, - } - } - return undefined - }, }) modelStreamResponses.add(response) requestOfResponse.set(response, req) @@ -840,7 +734,7 @@ async function safeHandler(req: Request): Promise { } catch (error) { response = handleError(error) } - // Early returns, cache hits and errors never reach streamText's callbacks, + // Early returns and errors never reach streamText's callbacks, // so their Langfuse trace has to be ended here if (!modelStreamResponses.has(response)) endTrace() return response diff --git a/app/api/chat/xml_guide.md b/app/api/chat/xml_guide.md deleted file mode 100644 index e76edf66..00000000 --- a/app/api/chat/xml_guide.md +++ /dev/null @@ -1,321 +0,0 @@ -# Draw.io XML Schema Guide - -This guide explains the structure of draw.io (diagrams.net) XML files to help you understand and create diagrams programmatically. - -## Basic Structure - -A draw.io XML file has the following hierarchy: - -```xml - - - - - - - - - -``` - -## Root Element: `` - -The root element of a draw.io file. - -**Attributes:** - -- `host`: The application that created the file (e.g., "app.diagrams.net") -- `modified`: Last modification timestamp -- `agent`: Browser / user agent information -- `version`: Version of the application -- `type`: File type (usually "device" or "google") - -**Example:** - -```xml - -``` - -## Diagram Element: `` - -Each page in your draw.io document is represented by a `` element. - -**Attributes:** - -- `id`: Unique identifier for the diagram -- `name`: The name of the diagram / page - -**Example:** - -```xml - -``` - -## Graph Model: `` - -Contains the actual diagram data. - -**Attributes:** - -- `dx`: Grid size in x-direction (usually 1) -- `dy`: Grid size in y-direction (usually 1) -- `grid`: Whether grid is enabled (0 or 1) -- `gridSize`: Grid cell size (usually 10) -- `guides`: Whether guides are enabled (0 or 1) -- `tooltips`: Whether tooltips are enabled (0 or 1) -- `connect`: Whether connections are enabled (0 or 1) -- `arrows`: Whether arrows are enabled (0 or 1) -- `fold`: Whether folding is enabled (0 or 1) -- `page`: Whether page view is enabled (0 or 1) -- `pageScale`: Scale of the page (usually 1) -- `pageWidth`: Width of the page (e.g., 850) -- `pageHeight`: Height of the page (e.g., 1100) -- `math`: Whether math typesetting is enabled (0 or 1) -- `shadow`: Whether shadows are enabled (0 or 1) - -**Example:** - -```xml - -``` - -## Root Cell Container: `` - -Contains all the cells in the diagram. **Note:** When generating diagrams, you only need to provide the mxCell elements - the root container and root cells (id="0", id="1") are added automatically. - -**Internal structure (auto-generated):** - -```xml - - - - - -``` - -## Cell Elements: `` - -The basic building block of diagrams. Cells represent shapes, connectors, text, etc. - -**Attributes for all cells:** - -- `id`: Unique identifier for the cell -- `parent`: ID of the parent cell (typically "1" for most cells) -- `value`: Text content of the cell -- `style`: Styling information (see Style section below) - -**Attributes for shapes (vertices):** - -- `vertex`: Set to "1" for shapes - - `connectable`: Whether the shape can be connected (0 or 1) - -**Attributes for connectors (edges):** - -- `edge`: Set to "1" for connectors - - `source`: ID of the source cell - - `target`: ID of the target cell - -**Example (Rectangle shape):** - -```xml - - - -``` - -**Example (Connector):** - -```xml - - - - - - -``` - -## Geometry: `` - -Defines the position and dimensions of cells. - -**Attributes for shapes:** - -- `x`: The x-coordinate of the **top-left** point of the shape. -- `y`: The y-coordinate of the **top-left** point of the shape. -- `width`: The width of the shape. -- `height`: The height of the shape. -- `as`: Specifies the role of this geometry within its parent cell. Typically set to `"geometry"` for the main shape definition. - -**Attributes for connectors:** - -- `relative`: Set to "1" for relative geometry -- `as`: Set to "geometry" - -**Example for shapes:** - -```xml - -``` - -**Example for connectors:** - -```xml - - - - -``` - -## Cell Style Reference - -Styles are specified as semicolon-separated `key=value` pairs in the `style` attribute of `` elements. - -### Shape-specific Styles - -- Rectangle: `shape=rectangle` -- Ellipse: `shape=ellipse` -- Triangle: `shape=triangle` -- Rhombus: `shape=rhombus` -- Hexagon: `shape=hexagon` -- Cloud: `shape=cloud` -- Actor: `shape=actor` -- Cylinder: `shape=cylinder` -- Document: `shape=document` -- Note: `shape=note` -- Card: `shape=card` -- Parallelogram: `shape=parallelogram` - -### Connector Styles - -- `endArrow=classic`: Arrow type at the end (classic, open, oval, diamond, block) -- `startArrow=none`: Arrow type at the start (none, classic, open, oval, diamond) -- `curved=1`: Curved connector (0 or 1) -- `edgeStyle=orthogonalEdgeStyle`: Connector routing style -- `elbow=vertical`: Elbow direction (vertical, horizontal) -- `jumpStyle=arc`: Jump style for line crossing (arc, gap) -- `jumpSize=10`: Size of the jump - -## Special Cells - -Draw.io files contain two special cells that are always present: - -1. **Root Cell** (id = "0"): The parent of all cells -2. **Default Parent Cell** (id = "1", parent = "0"): The default layer and parent for most cells - -## Tips for Creating Draw.io XML - -1. **Generate ONLY mxCell elements** - wrapper tags and root cells (id="0", id="1") are added automatically -2. Start IDs from "2" (id="0" and id="1" are reserved for root cells) -3. Assign unique and sequential IDs to all cells -4. Define parent relationships correctly (use parent="1" for top-level shapes) -5. Use `mxGeometry` elements to position shapes -6. For connectors, specify `source` and `target` attributes -7. **CRITICAL: All mxCell elements must be siblings. NEVER nest mxCell inside another mxCell.** - -## Common Patterns - -### Grouping Elements - -To group elements, create a parent cell and set other cells' `parent` attribute to its ID: - -```xml - - - - - - - - - - - -``` - -### Swimlanes - -Swimlanes use the `swimlane` shape style. **IMPORTANT: All mxCell elements (swimlanes, steps, and edges) must be siblings under ``. Edges are NOT nested inside swimlanes or steps.** - -```xml - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Tables - -Tables use multiple cells with parent-child relationships: - -```xml - - - - - - -``` - -## Advanced Features - -### Custom Attributes - -Draw.io allows adding custom attributes to cells: - -```xml - - - - -``` - -These custom attributes can store additional metadata or be used by plugins and custom behaviors. - -### User-defined Styles - -You can define custom styles for cells by combining various style attributes: - -```xml - - - -``` - -### Layers - -You can create multiple layers in a diagram to organize complex diagrams: - -```xml - - - - - - - - - - -``` diff --git a/app/api/config/route.ts b/app/api/config/route.ts index f4b5bc4e..f4dac222 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -3,8 +3,5 @@ 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, }) } diff --git a/app/api/log-feedback/route.ts b/app/api/log-feedback/route.ts index cfa41ca3..e7cfa7dd 100644 --- a/app/api/log-feedback/route.ts +++ b/app/api/log-feedback/route.ts @@ -1,6 +1,5 @@ -import { randomUUID } from "crypto" import { z } from "zod" -import { getLangfuseClient } from "@/lib/langfuse" +import { getLangfuseClient, scoreLatestTrace } from "@/lib/langfuse" import { getUserIdFromRequest } from "@/lib/user-id" const feedbackSchema = z.object({ @@ -37,75 +36,24 @@ export async function POST(req: Request) { 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({ + // Score the session's latest chat trace; without one, a standalone + // feedback trace carries the score + await scoreLatestTrace(langfuse, { sessionId, - limit: 1, + name: "user-feedback", + value: feedback === "good" ? 1 : 0, + comment: `User gave ${feedback} feedback`, + fallbackTrace: { + name: "user-feedback", + userId, + input: { messageId, feedback }, + metadata: { + source: "feedback-button", + note: "standalone - no chat trace found", + }, + }, }) - 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) diff --git a/app/api/log-save/route.ts b/app/api/log-save/route.ts index eb30e0fe..cd97e2d2 100644 --- a/app/api/log-save/route.ts +++ b/app/api/log-save/route.ts @@ -1,6 +1,5 @@ -import { randomUUID } from "crypto" import { z } from "zod" -import { getLangfuseClient } from "@/lib/langfuse" +import { getLangfuseClient, scoreLatestTrace } from "@/lib/langfuse" const saveSchema = z.object({ filename: z.string().min(1).max(255), @@ -33,39 +32,16 @@ export async function POST(req: Request) { } 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({ + // Flag the session's latest chat trace as saved; a session without a + // trace (no chat yet) is skipped + const logged = await scoreLatestTrace(langfuse, { sessionId, - limit: 1, + name: "diagram-saved", + value: 1, + comment: `User saved diagram as ${filename}.${format}`, }) - 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 }) + return Response.json({ success: true, logged }) } catch (error) { console.error("Langfuse save error:", error) return Response.json( diff --git a/app/api/server-models/route.ts b/app/api/server-models/route.ts index 49ea12bb..4f152f8b 100644 --- a/app/api/server-models/route.ts +++ b/app/api/server-models/route.ts @@ -7,8 +7,5 @@ export const dynamic = "force-dynamic" export async function GET() { const models = await loadFlattenedServerModels() - return NextResponse.json({ - models, - hasConfig: models.length > 0, - }) + return NextResponse.json({ models }) } diff --git a/app/api/validate-diagram/route.ts b/app/api/validate-diagram/route.ts index 3fba6628..c91e6377 100644 --- a/app/api/validate-diagram/route.ts +++ b/app/api/validate-diagram/route.ts @@ -1,9 +1,9 @@ /** * API endpoint for VLM-based diagram validation. - * Accepts a PNG image and streams validation results using useObject-compatible format. + * Accepts a PNG image and answers with the validation result as JSON. */ -import { Output, streamText } from "ai" +import { generateText, Output } from "ai" import { checkAccessCode, rejectCrossSite } from "@/lib/access-code" import { getValidationModel } from "@/lib/ai-providers" import { @@ -35,13 +35,6 @@ const DEFAULT_VALID_RESULT: ValidationResult = { suggestions: [], } -/** A fixed result in the text format useObject reads */ -function createStreamingResponse(result: ValidationResult): Response { - return new Response(JSON.stringify(result), { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }) -} - export async function POST(req: Request): Promise { const crossSite = rejectCrossSite(req) if (crossSite) return crossSite @@ -53,7 +46,7 @@ export async function POST(req: Request): Promise { // Check if VLM validation is enabled (default: true) const enableValidation = process.env.ENABLE_VLM_VALIDATION !== "false" if (!enableValidation) { - return createStreamingResponse(DEFAULT_VALID_RESULT) + return Response.json(DEFAULT_VALID_RESULT) } const body: ValidateDiagramRequest = await req.json() @@ -123,7 +116,7 @@ export async function POST(req: Request): Promise { error, ) // Return valid if no vision model is configured - return createStreamingResponse(DEFAULT_VALID_RESULT) + return Response.json(DEFAULT_VALID_RESULT) } // Parse timeout with validation (minimum 1000ms, default 10000ms) @@ -133,8 +126,7 @@ export async function POST(req: Request): Promise { parseInt(process.env.VALIDATION_TIMEOUT || "10000", 10), ) || 10000 - // Stream the VLM response for useObject consumption - const result = streamText({ + const { output, totalUsage } = await generateText({ model, output: Output.object({ schema: ValidationResultSchema }), system: VALIDATION_SYSTEM_PROMPT, @@ -155,23 +147,19 @@ export async function POST(req: Request): Promise { ], maxOutputTokens: 1024, abortSignal: AbortSignal.timeout(timeout), - onFinish: ({ output, totalUsage }) => { - if (countsQuota && totalUsage) { - recordTokenUsage( - userId, - (totalUsage.inputTokens || 0) + - (totalUsage.outputTokens || 0), - ) - } - if (sessionId && output) { - console.log( - `[validate-diagram] Session ${sessionId}: valid=${output.valid}, issues=${output.issues?.length ?? 0}`, - ) - } - }, }) - - return result.toTextStreamResponse() + if (countsQuota) { + await recordTokenUsage( + userId, + (totalUsage.inputTokens || 0) + (totalUsage.outputTokens || 0), + ) + } + if (sessionId) { + console.log( + `[validate-diagram] Session ${sessionId}: valid=${output.valid}, issues=${output.issues?.length ?? 0}`, + ) + } + return Response.json(output) } catch (error) { // Log with session context if available const errorMessage = @@ -179,6 +167,6 @@ export async function POST(req: Request): Promise { console.error("[validate-diagram] Error:", errorMessage) // On error, return valid to not block the user - return createStreamingResponse(DEFAULT_VALID_RESULT) + return Response.json(DEFAULT_VALID_RESULT) } } diff --git a/app/api/validate-model/route.ts b/app/api/validate-model/route.ts index 7eac4c1a..3df9fb8c 100644 --- a/app/api/validate-model/route.ts +++ b/app/api/validate-model/route.ts @@ -1,249 +1,29 @@ -import { streamText, tool } from "ai" import { NextResponse } from "next/server" -import { z } from "zod" import { checkAccessCode, rejectCrossSite } from "@/lib/access-code" -import { checkAdminAuth } from "@/lib/admin/auth" import { - edgeOneEndpoint, - getAIModel, - globalBaseUrl, - usesServerCredentials, - usesServerEndpoint, -} from "@/lib/ai-providers" -import { - checkAndIncrementRequest, - isQuotaEnabled, -} from "@/lib/dynamo-quota-manager" -import { classifyLLMError } from "@/lib/llm-errors" -import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection" -import { normalizeBaseUrl, type ProviderName } from "@/lib/types/model-config" -import { getUserIdFromRequest } from "@/lib/user-id" + type TestModelRequest, + testModelConnection, +} from "@/lib/test-model-connection" export const runtime = "nodejs" -interface ValidateRequest { - provider: ProviderName - apiKey: string - baseUrl?: string - modelId: string - // AWS Bedrock specific - awsAccessKeyId?: string - awsSecretAccessKey?: string - awsRegion?: string - awsSessionToken?: string - // Vertex AI specific - vertexApiKey?: string // Express Mode API key - // Set by the admin panel's Test: baseUrl is the server's

_BASE_URL - serverBaseUrl?: boolean -} - -const TEST_TIMEOUT_MS = 15_000 - -// Drawing works through tool calls, so the test asks for one -const PING_TOOL = tool({ - description: "Report that the connection works.", - inputSchema: z.object({}), -}) - -const NO_TOOL_CALL_WARNING = - "Connected, but the model answered without calling a tool. It may not support tool calls, which drawing needs." - +// The Settings dialog's Test button: a model on the user's own credentials export async function POST(req: Request) { const crossSite = rejectCrossSite(req) if (crossSite) return crossSite - // Lets the server send requests to arbitrary URLs, so require the access - // code, or the admin password (the admin panel's Test button) + // Lets the server send requests to arbitrary URLs, so require the access code const accessError = checkAccessCode(req) - if (accessError && checkAdminAuth(req)) return accessError + if (accessError) return accessError + let body: TestModelRequest try { - const body: ValidateRequest = await req.json() - const { - provider, - apiKey, - modelId, - awsAccessKeyId, - awsSecretAccessKey, - awsRegion, - awsSessionToken, - // Note: Express Mode only needs vertexApiKey - vertexApiKey, - } = body - - if (!provider || !modelId) { - return NextResponse.json( - { valid: false, error: "Provider and model ID are required" }, - { status: 400 }, - ) - } - // EdgeOne is this site's own function, as in the chat; the admin - // panel's Test sends no URL, and a relative one cannot be fetched - const baseUrl = - provider === "edgeone" ? edgeOneEndpoint(req) : body.baseUrl - // The admin panel's Test of an entry without a URL sends the - // server's own

_BASE_URL, which chat uses as it is: not a URL a - // user chose, so no private-address or redirect rules - const serverUrl = - body.serverBaseUrl === true && - !!baseUrl && - baseUrl === globalBaseUrl(provider) && - !checkAdminAuth(req) - - // SECURITY: Block SSRF attacks via custom baseUrl - if ( - baseUrl && - !serverUrl && - !allowPrivateUrls() && - (await isPrivateUrl(baseUrl)) - ) { - return NextResponse.json( - { valid: false, error: "Invalid base URL" }, - { status: 400 }, - ) - } - - // Validate credentials based on provider - if (provider === "bedrock") { - if ( - !(apiKey || (awsAccessKeyId && awsSecretAccessKey)) || - !awsRegion - ) { - return NextResponse.json( - { - valid: false, - error: "AWS credentials (a Bedrock API key, or an Access Key ID and Secret Access Key, and a Region) are required", - }, - { status: 400 }, - ) - } - } else if (provider === "vertexai") { - if (!vertexApiKey) { - return NextResponse.json( - { - valid: false, - error: "Vertex AI API key is required for Express Mode", - }, - { status: 400 }, - ) - } - } else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) { - return NextResponse.json( - { valid: false, error: "API key is required" }, - { status: 400 }, - ) - } - // The Test button checks the user's own provider. On the server's - // keys (Ollama Cloud without a key or URL) anyone could run any model. - if ( - usesServerCredentials(provider, { - apiKey, - baseUrl, - awsAccessKeyId, - awsSecretAccessKey, - vertexApiKey, - }) - ) { - return NextResponse.json( - { valid: false, error: "API key is required" }, - { status: 400 }, - ) - } - - // On the deployment's own endpoints a Test runs a model as a chat - // does, so with the quota on it counts as a chat request (an - // admin's Test of the server's URL does not) - const userId = getUserIdFromRequest(req) - if ( - isQuotaEnabled() && - !serverUrl && - userId !== "anonymous" && - (await usesServerEndpoint( - provider, - normalizeBaseUrl(body.baseUrl ?? ""), - apiKey, - )) - ) { - const quotaCheck = await checkAndIncrementRequest(userId, { - requests: Number(process.env.DAILY_REQUEST_LIMIT) || 10, - tokens: Number(process.env.DAILY_TOKEN_LIMIT) || 200000, - tpm: Number(process.env.TPM_LIMIT) || 20000, - }) - if (!quotaCheck.allowed) { - return NextResponse.json( - { valid: false, error: quotaCheck.error }, - { status: 429 }, - ) - } - } - - // The same model the chat would use. A client base URL makes it - // refuse redirects to internal hosts. - const { model } = getAIModel({ - provider, - modelId, - apiKey, - baseUrl, - trustedBaseUrl: serverUrl, - awsAccessKeyId, - awsSecretAccessKey, - awsRegion, - // Temporary AWS credentials need it, as in the chat - awsSessionToken, - vertexApiKey, - // EdgeOne checks the Pages cookies and the access code - ...(provider === "edgeone" && { - headers: { - cookie: req.headers.get("cookie") || "", - "x-access-code": req.headers.get("x-access-code") || "", - }, - }), - }) - - // Streaming, like the chat (some models only stream). Stop at the - // first tool call; a reasoning model that runs out of tokens first - // proves the connection but not tool support. - const startTime = Date.now() - const result = streamText({ - model, - prompt: "Call the ping tool.", - tools: { ping: PING_TOOL }, - maxOutputTokens: 1024, - maxRetries: 0, - abortSignal: AbortSignal.timeout(TEST_TIMEOUT_MS), - }) - let calledTool = false - let finishReason: string | undefined - for await (const part of result.fullStream) { - if (part.type === "error") throw part.error - // The timeout ends the stream with an abort part, not an error - if (part.type === "abort") { - const timeout = new Error( - `The model did not answer within ${TEST_TIMEOUT_MS / 1000} s.`, - ) - timeout.name = "TimeoutError" - throw timeout - } - if (part.type === "tool-call") { - calledTool = true - break - } - if (part.type === "finish") finishReason = part.finishReason - } - const responseTime = Date.now() - startTime - - return NextResponse.json({ - valid: true, - responseTime, - ...(!calledTool && - finishReason !== "length" && { warning: NO_TOOL_CALL_WARNING }), - }) - } catch (error) { - console.error("[validate-model] Error:", error) - - const { code, message } = classifyLLMError(error) + body = await req.json() + } catch { return NextResponse.json( - { valid: false, code, error: message }, - { status: 200 }, // Return 200 so client can read error message + { valid: false, error: "Invalid JSON body" }, + { status: 400 }, ) } + // A URL in the request is one the user typed, never the server's own + return testModelConnection(req, body, false) } diff --git a/app/globals.css b/app/globals.css index c4df4ee3..6cf34e3f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -98,9 +98,7 @@ --shadow-pop: 0 1px 2px rgb(20 24 29 / 0.06), 0 18px 48px -12px rgb(20 24 29 / 0.22); - --duration-normal: 200ms; --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); } /* @@ -241,13 +239,6 @@ .shadow-pop { box-shadow: var(--shadow-pop); } - /* Older names kept for components that still use them */ - .shadow-soft { - box-shadow: var(--shadow-float); - } - .shadow-dialog { - box-shadow: var(--shadow-pop); - } } @keyframes fadeIn { @@ -284,17 +275,13 @@ } } -@keyframes check-pop { - 0% { - transform: scale(0.8); - opacity: 0; +/* A highlight sweeps across "Thinking..." text (Shimmer) */ +@keyframes shimmer { + from { + background-position: 100% center; } - 50% { - transform: scale(1.1); - } - 100% { - transform: scale(1); - opacity: 1; + to { + background-position: 0% center; } } @@ -307,15 +294,15 @@ .animate-panel-in { animation: panelIn 0.32s var(--ease-out) both; } -.animate-check-pop { - animation: check-pop 0.25s var(--ease-spring) forwards; +.animate-shimmer { + animation: shimmer var(--shimmer-duration, 2s) linear infinite; } @media (prefers-reduced-motion: reduce) { .animate-fade-in, .animate-message-in, .animate-panel-in, - .animate-check-pop { + .animate-shimmer { animation: none; } } diff --git a/components/ai-elements/model-selector.tsx b/components/ai-elements/model-selector.tsx deleted file mode 100644 index 7164f44b..00000000 --- a/components/ai-elements/model-selector.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { Cloud } from "lucide-react" -import type { ComponentProps, ElementRef, ReactNode } from "react" -import { useEffect, useRef, useState } 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 - -export const ModelSelector = (props: ModelSelectorProps) => ( -

-) - -export type ModelSelectorTriggerProps = ComponentProps - -export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => ( - -) - -export type ModelSelectorContentProps = ComponentProps & { - title?: ReactNode -} - -export const ModelSelectorContent = ({ - className, - children, - title = "Model Selector", - ...props -}: ModelSelectorContentProps) => ( - - {title} - - {children} - - -) - -export type ModelSelectorDialogProps = ComponentProps - -export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => ( - -) - -export type ModelSelectorInputProps = ComponentProps - -export const ModelSelectorInput = ({ - className, - ...props -}: ModelSelectorInputProps) => ( - -) - -export type ModelSelectorListProps = ComponentProps - -export const ModelSelectorList = ({ - className, - ...props -}: ModelSelectorListProps) => { - const listRef = useRef>(null) - const [showShadow, setShowShadow] = useState(false) - - useEffect(() => { - const listElement = listRef.current - if (!listElement) return - - const checkScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = listElement - // Show shadow if there is more content below - // Using a small threshold to handle fractional pixel rendering - setShowShadow( - scrollHeight > Math.ceil(scrollTop + clientHeight) + 1, - ) - } - - // Initial check - checkScroll() - - // Event listeners - listElement.addEventListener("scroll", checkScroll) - window.addEventListener("resize", checkScroll) - - // Observe content changes (e.g. async loading of items) - const observer = new MutationObserver(checkScroll) - observer.observe(listElement, { childList: true, subtree: true }) - - return () => { - listElement.removeEventListener("scroll", checkScroll) - window.removeEventListener("resize", checkScroll) - observer.disconnect() - } - }, []) - - return ( -
- - {/* Bottom shadow indicator for scrollable content */} -
-
- ) -} - -export type ModelSelectorEmptyProps = ComponentProps - -export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => ( - -) - -export type ModelSelectorGroupProps = ComponentProps - -export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => ( - -) - -export type ModelSelectorItemProps = ComponentProps - -export const ModelSelectorItem = (props: ModelSelectorItemProps) => ( - -) - -export type ModelSelectorShortcutProps = ComponentProps - -export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => ( - -) - -export type ModelSelectorSeparatorProps = ComponentProps< - typeof CommandSeparator -> - -export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => ( - -) - -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 - } - - return ( - // biome-ignore lint/performance/noImgElement: External URL from models.dev - {`${provider} - ) -} - -export type ModelSelectorLogoGroupProps = ComponentProps<"div"> - -export const ModelSelectorLogoGroup = ({ - className, - ...props -}: ModelSelectorLogoGroupProps) => ( -
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) => ( - -) - -export type ModelSelectorSectionHeaderProps = { - icon: ReactNode - label: string - className?: string -} - -export const ModelSelectorSectionHeader = ({ - icon, - label, - className, -}: ModelSelectorSectionHeaderProps) => ( -
- - {label} -
-) diff --git a/components/ai-elements/reasoning.tsx b/components/ai-elements/reasoning.tsx index 271db5cf..b3dbc985 100644 --- a/components/ai-elements/reasoning.tsx +++ b/components/ai-elements/reasoning.tsx @@ -1,186 +1,83 @@ "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 type { ReactNode } from "react" +import { 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(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 & { +export type ReasoningProps = { + className?: string isStreaming?: boolean - open?: boolean + /** Open while streaming, then folds once about a second after it ends */ defaultOpen?: boolean - onOpenChange?: (open: boolean) => void - duration?: number + getThinkingMessage: (isStreaming: boolean, duration?: number) => ReactNode + children: string } 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, - }) +export function Reasoning({ + className, + isStreaming = false, + defaultOpen = true, + getThinkingMessage, + children, +}: ReasoningProps) { + const [isOpen, setIsOpen] = useState(defaultOpen) + const [duration, setDuration] = useState() + const [hasAutoClosed, setHasAutoClosed] = useState(false) + const [startTime, setStartTime] = useState(null) - const [hasAutoClosed, setHasAutoClosed] = useState(false) - const [startTime, setStartTime] = useState(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) + // Track duration when streaming starts and ends + useEffect(() => { + if (isStreaming) { + if (startTime === null) { + setStartTime(Date.now()) } - }, [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) + } else if (startTime !== null) { + setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S)) + setStartTime(null) } + }, [isStreaming, startTime]) - return ( - - - {children} - - - ) - }, -) + // Auto-close once when streaming ends, after a short delay so the user + // sees the content + useEffect(() => { + if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) { + const timer = setTimeout(() => { + setIsOpen(false) + setHasAutoClosed(true) + }, AUTO_CLOSE_DELAY) -export type ReasoningTriggerProps = ComponentProps< - typeof CollapsibleTrigger -> & { - getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode -} + return () => clearTimeout(timer) + } + }, [isStreaming, isOpen, defaultOpen, hasAutoClosed]) -const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => { - if (isStreaming || duration === 0) { - return Thinking... - } - if (duration === undefined) { - return

Thought for a few seconds

- } - return

Thought for {duration} seconds

-} - -export const ReasoningTrigger = memo( - ({ - className, - children, - getThinkingMessage = defaultGetThinkingMessage, - ...props - }: ReasoningTriggerProps) => { - const { isStreaming, isOpen, duration } = useReasoning() - - return ( - - {children ?? ( - <> - - {getThinkingMessage(isStreaming, duration)} - - - )} - - ) - }, -) - -export type ReasoningContentProps = ComponentProps< - typeof CollapsibleContent -> & { - children: string -} - -export const ReasoningContent = memo( - ({ className, children, ...props }: ReasoningContentProps) => ( - -
{children}
-
- ), -) - -Reasoning.displayName = "Reasoning" -ReasoningTrigger.displayName = "ReasoningTrigger" -ReasoningContent.displayName = "ReasoningContent" + + + {getThinkingMessage(isStreaming, duration)} + + + +
{children}
+
+ + ) +} diff --git a/components/ai-elements/shimmer.tsx b/components/ai-elements/shimmer.tsx index 274bf85f..1ebed4f0 100644 --- a/components/ai-elements/shimmer.tsx +++ b/components/ai-elements/shimmer.tsx @@ -1,13 +1,6 @@ "use client" -import { motion } from "motion/react" -import { - type CSSProperties, - type ElementType, - type JSX, - memo, - useMemo, -} from "react" +import { type CSSProperties, type ElementType, memo, useMemo } from "react" import { cn } from "@/lib/utils" export type TextShimmerProps = { @@ -25,39 +18,29 @@ const ShimmerComponent = ({ duration = 2, spread = 2, }: TextShimmerProps) => { - const MotionComponent = motion.create( - Component as keyof JSX.IntrinsicElements, - ) - const dynamicSpread = useMemo( () => (children?.length ?? 0) * spread, [children, spread], ) return ( - {children} - + ) } diff --git a/components/button-with-tooltip.tsx b/components/button-with-tooltip.tsx deleted file mode 100644 index 089b0596..00000000 --- a/components/button-with-tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { VariantProps } from "class-variance-authority" -import type React from "react" -import { Button, type buttonVariants } from "@/components/ui/button" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" - -interface ButtonWithTooltipProps - extends React.ComponentProps<"button">, - VariantProps { - tooltipContent: string - children: React.ReactNode - asChild?: boolean -} - -export function ButtonWithTooltip({ - tooltipContent, - children, - ...buttonProps -}: ButtonWithTooltipProps) { - return ( - - - - - - - {tooltipContent} - - - - ) -} diff --git a/components/canvas/canvas-stage.tsx b/components/canvas/canvas-stage.tsx index 93c616e9..b6901738 100644 --- a/components/canvas/canvas-stage.tsx +++ b/components/canvas/canvas-stage.tsx @@ -155,8 +155,7 @@ export function CanvasStage({ key={src} ref={drawioRef} src={src} - configCss={DRAWIO_CSS} - editorCss={DRAWIO_CSS} + css={DRAWIO_CSS} dark={isDark} onLoad={onDrawioLoad} onAutoSave={handleDiagramAutoSave} diff --git a/components/canvas/drawio-frame.tsx b/components/canvas/drawio-frame.tsx index df47a3fc..177094d8 100644 --- a/components/canvas/drawio-frame.tsx +++ b/components/canvas/drawio-frame.tsx @@ -30,10 +30,11 @@ export interface DrawioFrameHandle { interface DrawioFrameProps { src: string - /** CSS sent with the configure message (works cross-origin too) */ - configCss: string - /** CSS injected once the editor is reachable directly (same origin) */ - editorCss: string + /** + * Theme CSS: sent with the configure message (works cross-origin too) + * and, same origin, injected as a