mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
## 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 `<svg:svg>` 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
184 lines
6.8 KiB
TypeScript
184 lines
6.8 KiB
TypeScript
import { useEffect, useRef, useState } from "react"
|
|
import { DrawioFrame } from "@/components/canvas/drawio-frame"
|
|
import { useLocale } from "@/components/canvas/locale-context"
|
|
import { useDiagram } from "@/contexts/diagram-context"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import {
|
|
DRAWIO_CSS,
|
|
type DrawioSource,
|
|
getDrawioSrc,
|
|
webDrawioSource,
|
|
} from "@/lib/drawio/drawio-config"
|
|
import { keepCenter, refitIfRecent } from "@/lib/drawio/editor-bridge"
|
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
|
import { STORAGE_KEYS } from "@/lib/storage"
|
|
import { cn } from "@/lib/utils"
|
|
import { useCanvasStore } from "@/stores/canvas-store"
|
|
import { useSettingsStore } from "@/stores/settings-store"
|
|
|
|
/** Size of the canvas area, for overlays that stay inside it */
|
|
export interface CanvasSize {
|
|
width: number
|
|
height: number
|
|
}
|
|
|
|
/**
|
|
* The draw.io canvas, with draw.io's own toolbar, menus and page tabs, and
|
|
* an overlay laid over it (the web app's "ask AI" button; only when the
|
|
* page can drive draw.io directly, not with an external cross-origin
|
|
* draw.io).
|
|
*/
|
|
export function CanvasStage({
|
|
className,
|
|
style,
|
|
hideOverlays = false,
|
|
overlay,
|
|
drawioSource,
|
|
waitForSavedLocale = false,
|
|
}: {
|
|
className?: string
|
|
style?: React.CSSProperties
|
|
/** Hide the overlay (e.g. while the start screen covers the canvas) */
|
|
hideOverlays?: boolean
|
|
/** Rendered over the canvas once draw.io is ready */
|
|
overlay?: (size: CanvasSize) => React.ReactNode
|
|
/** Where the editor comes from; none: the web app's own copy (the MCP
|
|
* shell passes what its server configured) */
|
|
drawioSource?: DrawioSource
|
|
/** Web app: the page is about to switch to the language picked last
|
|
* time (useSavedLocale in the workspace); do not load draw.io in the
|
|
* URL's language meanwhile */
|
|
waitForSavedLocale?: boolean
|
|
}) {
|
|
const dict = useDictionary()
|
|
const lang = useLocale()
|
|
const {
|
|
drawioRef,
|
|
handleDiagramAutoSave,
|
|
handleDiagramExport,
|
|
onDrawioLoad,
|
|
resetDrawioReady,
|
|
isDrawioReady,
|
|
} = useDiagram()
|
|
const isDark = useSettingsStore((s) => s.isDark)
|
|
const hasEditor = useCanvasStore((s) => s.hasEditor)
|
|
// Canvas width when the chat panel starts sliding in or out
|
|
const slideStartWidthRef = useRef(0)
|
|
|
|
const [src, setSrc] = useState<string | null>(null)
|
|
// null until draw.io tells us whether we can drive it directly
|
|
const [editorAccess, setEditorAccess] = useState<boolean | null>(null)
|
|
const stageRef = useRef<HTMLDivElement>(null)
|
|
const [size, setSize] = useState<CanvasSize>({ width: 0, height: 0 })
|
|
|
|
useEffect(() => {
|
|
// Load draw.io once, in the language the page will have
|
|
if (waitForSavedLocale) {
|
|
const saved = localStorage.getItem(STORAGE_KEYS.locale)
|
|
if (
|
|
saved &&
|
|
saved !== lang &&
|
|
i18n.locales.includes(saved as Locale)
|
|
) {
|
|
return
|
|
}
|
|
}
|
|
// The theme class is set before the page renders; the settings store
|
|
// may not have read it yet
|
|
const dark = document.documentElement.classList.contains("dark")
|
|
setSrc(getDrawioSrc(drawioSource ?? webDrawioSource(lang), dark))
|
|
}, [lang])
|
|
|
|
// A new iframe starts over: draw.io reports load and access again
|
|
useEffect(() => {
|
|
resetDrawioReady()
|
|
setEditorAccess(null)
|
|
}, [src])
|
|
|
|
// Cross-origin draw.io only takes its dark mode when it starts: reload
|
|
// it when that is not the theme now (also one switched while it
|
|
// loaded). Same-origin switches it in place.
|
|
useEffect(() => {
|
|
if (editorAccess !== false || !src) return
|
|
const startedDark = new URL(src).searchParams.get("dark") === "1"
|
|
if (startedDark === isDark) return
|
|
setSrc(getDrawioSrc(drawioSource ?? webDrawioSource(lang), isDark))
|
|
}, [isDark, editorAccess, lang, src])
|
|
|
|
useEffect(() => {
|
|
const stage = stageRef.current
|
|
if (!stage) return
|
|
// React passes no transitionstart
|
|
const onSlideStart = (event: TransitionEvent) => {
|
|
if (event.target !== stage || event.propertyName !== "right") return
|
|
slideStartWidthRef.current = stage.clientWidth
|
|
}
|
|
stage.addEventListener("transitionstart", onSlideStart)
|
|
const observer = new ResizeObserver(([entry]) => {
|
|
setSize({
|
|
width: entry.contentRect.width,
|
|
height: entry.contentRect.height,
|
|
})
|
|
refitIfRecent()
|
|
})
|
|
observer.observe(stage)
|
|
return () => {
|
|
observer.disconnect()
|
|
stage.removeEventListener("transitionstart", onSlideStart)
|
|
}
|
|
}, [])
|
|
|
|
const showOverlays = hasEditor && isDrawioReady && !hideOverlays
|
|
|
|
return (
|
|
<div
|
|
ref={stageRef}
|
|
className={cn("absolute overflow-hidden bg-canvas", className)}
|
|
style={style}
|
|
data-testid="canvas-stage"
|
|
// The chat panel slides in or out (the canvas's right edge moves):
|
|
// once the canvas has its final size, fit a diagram just drawn,
|
|
// or keep the view's middle
|
|
onTransitionEnd={(event) => {
|
|
if (event.target !== event.currentTarget) return
|
|
if (event.propertyName !== "right") return
|
|
const stage = event.currentTarget
|
|
// Let draw.io take the new size first
|
|
setTimeout(() => {
|
|
if (refitIfRecent(4000)) return
|
|
keepCenter(slideStartWidthRef.current, stage.clientWidth)
|
|
}, 60)
|
|
}}
|
|
>
|
|
{src && (
|
|
<DrawioFrame
|
|
key={src}
|
|
ref={drawioRef}
|
|
src={src}
|
|
css={DRAWIO_CSS}
|
|
dark={isDark}
|
|
onLoad={onDrawioLoad}
|
|
onAutoSave={handleDiagramAutoSave}
|
|
onExport={handleDiagramExport}
|
|
onEditorAccess={setEditorAccess}
|
|
/>
|
|
)}
|
|
|
|
{/* Covers the iframe until draw.io has loaded and been styled */}
|
|
{(!isDrawioReady || editorAccess === null) && (
|
|
<div
|
|
className="absolute inset-0 flex items-center justify-center bg-canvas"
|
|
data-testid="canvas-loading"
|
|
>
|
|
<div className="flex items-center gap-2.5 text-[13px] text-muted-foreground">
|
|
<span className="size-3.5 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground" />
|
|
{dict.workspace.canvasLoading}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showOverlays && overlay?.(size)}
|
|
</div>
|
|
)
|
|
}
|