From c513d1a0c2b9e9ef508ac28f2fa261e7be36696a Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:20:15 +0900 Subject: [PATCH] feat(ui): canvas-first redesign with a self-hosted draw.io (#957) The canvas now fills the window and the chat floats on its right. draw.io is served from our own origin (public/drawio, downloaded at build time and checked against the release's SHA-256), so the app can drive the editor directly: - every AI change becomes a version card with a thumbnail; compare, restore or undo it, and Ctrl+Z on the canvas takes an AI change back in one step - shapes the AI just changed are highlighted on the canvas - selected shapes appear as a chip and are sent to the model - draw.io's own simple UI: its toolbar, menus (auto layout, Mermaid, CSV import) and page tabs; the shape library starts closed - new start screen, session rename and switcher, open .drawio files - settings split into Models & keys, Appearance, Advanced and About - Bedrock in the model settings also takes an API key instead of access keys - phones switch between canvas and chat - dark mode uses draw.io's own dark mode, with neutral greys around it With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the features above that need same origin are off. The chat logic moved from the 1500-line chat panel into a chat engine plus small UI components; zustand holds canvas, versions, settings and UI state. --- .github/workflows/electron-release.yml | 21 +- Dockerfile | 5 +- README.md | 5 +- app/[lang]/about/cn/page.tsx | 9 +- app/[lang]/about/ja/page.tsx | 10 +- app/[lang]/about/page.tsx | 12 +- app/[lang]/layout.tsx | 19 +- app/[lang]/page.tsx | 257 +- app/api/chat/route.ts | 4 +- app/api/validate-model/route.ts | 7 +- app/globals.css | 493 +- components/canvas/canvas-stage.tsx | 176 + components/canvas/drawio-frame.tsx | 265 + components/canvas/selection-ask.tsx | 71 + components/chat-example-panel.tsx | 227 - components/chat-input.tsx | 669 -- components/chat-message-display.tsx | 1382 ---- components/chat-panel.tsx | 1641 ----- components/chat/ChatLobby.tsx | 360 - components/chat/TemplatePanel.tsx | 32 +- components/chat/ToolCallCard.tsx | 267 - components/chat/attachments.ts | 77 + components/chat/chat-engine.tsx | 1952 ++++++ components/chat/chat-panel.tsx | 190 + components/chat/compare-dialog.tsx | 160 + components/chat/composer.tsx | 552 ++ components/chat/lobby-hero.tsx | 284 + components/chat/message-list.tsx | 640 ++ components/chat/message-text.ts | 64 + components/chat/session-menu.tsx | 295 + components/chat/tool-activity.tsx | 531 ++ components/chat/use-diagram-streaming.ts | 231 + components/chat/use-versions.ts | 183 + components/chat/version-strip.tsx | 91 + components/history-dialog.tsx | 114 - components/model-config-dialog.tsx | 121 +- components/model-selector.tsx | 37 +- components/provider-credentials-fields.tsx | 24 + components/reset-warning-modal.tsx | 50 - components/save-dialog.tsx | 95 +- components/settings-dialog.tsx | 1138 ++-- components/ui/resizable.tsx | 56 - components/workspace/app-toaster.tsx | 28 + components/workspace/brand-mark.tsx | 29 + components/workspace/icon-button.tsx | 80 + components/workspace/workspace.tsx | 403 ++ contexts/diagram-context.tsx | 255 +- docker-compose.yml | 12 +- docs/cn/FAQ.md | 6 +- docs/cn/README_CN.md | 5 +- docs/cn/offline-deployment.md | 28 +- docs/en/FAQ.md | 6 +- docs/en/offline-deployment.md | 28 +- docs/ja/FAQ.md | 6 +- docs/ja/README_JA.md | 5 +- docs/ja/offline-deployment.md | 28 +- env.example | 9 +- hooks/use-diagram-tool-handlers.ts | 104 +- hooks/use-panel-width.ts | 22 + hooks/use-session-manager.ts | 124 +- lib/ai-providers.ts | 56 +- lib/chat-helpers.ts | 34 + lib/chat-request.ts | 37 + lib/clipboard.ts | 36 + lib/diagram-diff.ts | 321 + lib/drawio-themes.ts | 17 - lib/drawio/drawio-config.ts | 99 + lib/drawio/editor-bridge.ts | 686 ++ lib/i18n/dictionaries/en.json | 294 +- lib/i18n/dictionaries/ja.json | 276 +- lib/i18n/dictionaries/zh-Hant.json | 278 +- lib/i18n/dictionaries/zh.json | 274 +- lib/platform.ts | 7 + lib/relative-time.ts | 20 + lib/session-storage.ts | 36 +- lib/storage.ts | 31 +- lib/use-file-processor.tsx | 27 +- lib/utils.ts | 10 +- lib/version-text.ts | 47 + package-lock.json | 53 +- package.json | 7 +- scripts/fetch-drawio.mjs | 148 + stores/canvas-store.ts | 45 + stores/settings-store.ts | 159 + stores/ui-store.ts | 62 + stores/versions-store.ts | 156 + tests/e2e/chat-errors.spec.ts | 14 +- tests/e2e/chat.spec.ts | 19 +- tests/e2e/diagram-content.spec.ts | 395 +- tests/e2e/diagram-generation.spec.ts | 4 +- tests/e2e/error-handling.spec.ts | 7 +- tests/e2e/file-upload.spec.ts | 27 +- tests/e2e/history-restore.spec.ts | 71 +- tests/e2e/iframe-interaction.spec.ts | 8 +- tests/e2e/language.spec.ts | 99 +- tests/e2e/lib/fixtures.ts | 52 +- tests/e2e/model-test.spec.ts | 4 +- tests/e2e/provider-models.spec.ts | 212 +- tests/e2e/settings.spec.ts | 25 +- tests/e2e/workspace.spec.ts | 5829 +++++++++++++++++ tests/unit/ai-providers-credentials.test.ts | 30 +- tests/unit/bedrock-api-key.test.ts | 47 + tests/unit/diagram-context.test.tsx | 2 +- tests/unit/diagram-diff.test.ts | 333 + tests/unit/redesign-helpers.test.ts | 211 + tests/unit/short-model-name.test.ts | 37 + ...l-card.test.tsx => tool-activity.test.tsx} | 35 +- ...est.tsx => use-diagram-streaming.test.tsx} | 25 +- tests/unit/use-diagram-tool-handlers.test.tsx | 104 +- tests/unit/validate-model-bedrock.test.ts | 34 + 110 files changed, 17891 insertions(+), 6944 deletions(-) create mode 100644 components/canvas/canvas-stage.tsx create mode 100644 components/canvas/drawio-frame.tsx create mode 100644 components/canvas/selection-ask.tsx delete mode 100644 components/chat-example-panel.tsx delete mode 100644 components/chat-input.tsx delete mode 100644 components/chat-message-display.tsx delete mode 100644 components/chat-panel.tsx delete mode 100644 components/chat/ChatLobby.tsx delete mode 100644 components/chat/ToolCallCard.tsx create mode 100644 components/chat/attachments.ts create mode 100644 components/chat/chat-engine.tsx create mode 100644 components/chat/chat-panel.tsx create mode 100644 components/chat/compare-dialog.tsx create mode 100644 components/chat/composer.tsx create mode 100644 components/chat/lobby-hero.tsx create mode 100644 components/chat/message-list.tsx create mode 100644 components/chat/message-text.ts create mode 100644 components/chat/session-menu.tsx create mode 100644 components/chat/tool-activity.tsx create mode 100644 components/chat/use-diagram-streaming.ts create mode 100644 components/chat/use-versions.ts create mode 100644 components/chat/version-strip.tsx delete mode 100644 components/history-dialog.tsx delete mode 100644 components/reset-warning-modal.tsx delete mode 100644 components/ui/resizable.tsx create mode 100644 components/workspace/app-toaster.tsx create mode 100644 components/workspace/brand-mark.tsx create mode 100644 components/workspace/icon-button.tsx create mode 100644 components/workspace/workspace.tsx create mode 100644 hooks/use-panel-width.ts create mode 100644 lib/chat-request.ts create mode 100644 lib/clipboard.ts create mode 100644 lib/diagram-diff.ts delete mode 100644 lib/drawio-themes.ts create mode 100644 lib/drawio/drawio-config.ts create mode 100644 lib/drawio/editor-bridge.ts create mode 100644 lib/platform.ts create mode 100644 lib/relative-time.ts create mode 100644 lib/version-text.ts create mode 100644 scripts/fetch-drawio.mjs create mode 100644 stores/canvas-store.ts create mode 100644 stores/settings-store.ts create mode 100644 stores/ui-store.ts create mode 100644 stores/versions-store.ts create mode 100644 tests/e2e/workspace.spec.ts create mode 100644 tests/unit/bedrock-api-key.test.ts create mode 100644 tests/unit/diagram-diff.test.ts create mode 100644 tests/unit/redesign-helpers.test.ts create mode 100644 tests/unit/short-model-name.test.ts rename tests/unit/{tool-call-card.test.tsx => tool-activity.test.tsx} (61%) rename tests/unit/{chat-message-display-preview.test.tsx => use-diagram-streaming.test.tsx} (74%) diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index 9e8ef191..9fed5980 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -34,15 +34,7 @@ jobs: node-version: 24 cache: "npm" - - name: Download draw.io static files for offline use - run: | - rm -rf public/drawio - git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio - mkdir -p public/drawio - cp -r /tmp/drawio/src/main/webapp/* public/drawio/ - rm -rf public/drawio/WEB-INF - rm -rf public/drawio/META-INF - + # npm run build downloads draw.io first (prebuild) - name: Install dependencies run: npm install @@ -66,16 +58,7 @@ jobs: node-version: 24 cache: "npm" - - name: Download draw.io static files for offline use - shell: bash - run: | - rm -rf public/drawio - git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio - mkdir -p public/drawio - cp -r /tmp/drawio/src/main/webapp/* public/drawio/ - rm -rf public/drawio/WEB-INF - rm -rf public/drawio/META-INF - + # npm run build downloads draw.io first (prebuild) - name: Install dependencies run: npm install diff --git a/Dockerfile b/Dockerfile index 037b48c1..54149c7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,9 @@ COPY . . # Disable Next.js telemetry during build ENV NEXT_TELEMETRY_DISABLED=1 -# Build-time argument for self-hosted draw.io URL -ARG NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net +# Build-time argument for an external draw.io URL. Leave empty to use the +# copy bundled into public/drawio (downloaded by `npm run build`) +ARG NEXT_PUBLIC_DRAWIO_BASE_URL="" ENV NEXT_PUBLIC_DRAWIO_BASE_URL=${NEXT_PUBLIC_DRAWIO_BASE_URL} # Build-time argument to show About link and Notice icon diff --git a/README.md b/README.md index 5c94015a..cbf27f7e 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,8 @@ Here are some example prompts and their generated diagrams: - **Image-Based Diagram Replication**: Upload existing diagrams or images and have the AI replicate and enhance them automatically - **PDF & Text File Upload**: Upload PDF documents and text files to extract content and generate diagrams from existing documents - **AI Reasoning Display**: View the AI's thinking process for supported models (OpenAI o1/o3, Gemini, Claude, etc.) -- **Diagram History**: Comprehensive version control that tracks all changes, allowing you to view and restore previous versions of your diagrams before the AI editing. +- **Versions and Undo**: Every AI change shows up in the chat as a version with a thumbnail. Compare it with the canvas, restore it, or undo it; Ctrl+Z on the canvas also takes back an AI change in one step. Shapes the AI just changed are highlighted. +- **Ask About a Selection**: Select shapes on the canvas and ask the AI to change just those. - **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time - **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure) - **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization @@ -263,7 +264,7 @@ The application uses the following technologies: - **Next.js**: For the frontend framework and routing - **Vercel AI SDK** (`ai` + `@ai-sdk/*`): For streaming AI responses and multi-provider support -- **react-drawio**: For diagram representation and manipulation +- **draw.io**: The editor runs from a copy bundled into `public/drawio` (downloaded by `npm run dev` / `npm run build`), so the app can drive it directly Diagrams are represented as XML that can be rendered in draw.io. The AI processes your commands and generates or modifies this XML accordingly. diff --git a/app/[lang]/about/cn/page.tsx b/app/[lang]/about/cn/page.tsx index bacd18e9..fe283122 100644 --- a/app/[lang]/about/cn/page.tsx +++ b/app/[lang]/about/cn/page.tsx @@ -149,8 +149,10 @@ export default function AboutCN() { :上传现有图表或图像,让AI自动复制和增强
  • - 图表历史记录 - :全面的版本控制,跟踪所有更改,允许您查看和恢复AI编辑前的图表版本 + 版本与撤销 + :AI + 的每次修改都会成为聊天里的一个版本,可以对比、恢复或撤销;在画布上按 + Ctrl+Z 也能撤回
  • 交互式聊天界面 @@ -294,7 +296,8 @@ export default function AboutCN() { ):用于流式AI响应和多提供商支持
  • - react-drawio:用于图表表示和操作 + draw.io + :内置在应用里的编辑器,应用可以直接控制它
  • diff --git a/app/[lang]/about/ja/page.tsx b/app/[lang]/about/ja/page.tsx index 22f53fc0..cf9d7e87 100644 --- a/app/[lang]/about/ja/page.tsx +++ b/app/[lang]/about/ja/page.tsx @@ -138,8 +138,10 @@ export default function AboutJA() { :既存のダイアグラムや画像をアップロードし、AIが自動的に複製・強化

  • - ダイアグラム履歴 - :すべての変更を追跡する包括的なバージョン管理。AI編集前のダイアグラムの以前のバージョンを表示・復元可能 + バージョンと取り消し + :AI + の変更はそれぞれチャット内のバージョンになり、比較・復元・取り消しができます。キャンバスで + Ctrl+Z を押しても元に戻せます
  • @@ -290,8 +292,8 @@ export default function AboutJA() { ):ストリーミングAIレスポンスとマルチプロバイダーサポート
  • - react-drawio - :ダイアグラムの表現と操作 + draw.io + :アプリに同梱されたエディター。アプリから直接操作できます
  • diff --git a/app/[lang]/about/page.tsx b/app/[lang]/about/page.tsx index 85a46ef5..a25d8869 100644 --- a/app/[lang]/about/page.tsx +++ b/app/[lang]/about/page.tsx @@ -150,10 +150,10 @@ export default function About() { replicate and enhance them automatically

  • - Diagram History: Comprehensive - version control that tracks all changes, allowing - you to view and restore previous versions of your - diagrams before the AI editing + 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: @@ -304,8 +304,8 @@ export default function About() { and multi-provider support
  • - react-drawio: For diagram - representation and manipulation + draw.io: The editor, bundled with + the app so it can be driven directly
  • diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index 04bd4eb5..bc2fd112 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -1,19 +1,25 @@ import { GoogleAnalytics } from "@next/third-parties/google" import type { Metadata, Viewport } from "next" -import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google" +import { Instrument_Sans, JetBrains_Mono } from "next/font/google" import { notFound } from "next/navigation" +import Script from "next/script" +import { AppToaster } from "@/components/workspace/app-toaster" import { DiagramProvider } from "@/contexts/diagram-context" import { DictionaryProvider } from "@/hooks/use-dictionary" import type { Locale } from "@/lib/i18n/config" import { i18n } from "@/lib/i18n/config" import { getDictionary, hasLocale } from "@/lib/i18n/dictionaries" +import { THEME_INIT_SCRIPT } from "@/stores/settings-store" import "../globals.css" -const plusJakarta = Plus_Jakarta_Sans({ - variable: "--font-sans", +const instrumentSans = Instrument_Sans({ + variable: "--font-latin", subsets: ["latin"], weight: ["400", "500", "600", "700"], + // Chinese and Japanese text falls through to system fonts, in the + // order globals.css picks for the page's language + fallback: [], }) const jetbrainsMono = JetBrains_Mono({ @@ -165,17 +171,22 @@ export default async function RootLayout({ return ( + {/* Apply dark mode before the first paint (no white flash) */} +