Files
next-ai-draw-io/lib/version-text.ts
T
dayuan.jiang 23d5c83b0d feat(ui): canvas-first redesign with a self-hosted draw.io
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
- the app's own canvas toolbar, shape library and page tabs replace
  draw.io's chrome
- new start screen, session rename and switcher, open .drawio files
- settings split into Models & keys, Appearance, Advanced and About
- phones switch between canvas and chat
- dark mode keeps the diagram on a white sheet and no longer flashes

With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the app falls back to
draw.io's own toolbar and 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.
2026-10-06 16:17:11 +09:00

36 lines
1.2 KiB
TypeScript

import type { ChangeSummary } from "@/lib/diagram-diff"
import { formatMessage } from "@/lib/i18n/utils"
type VersionDict = Record<string, string>
const PARTS: [keyof ChangeSummary, string][] = [
["shapesAdded", "shapesAdded"],
["edgesAdded", "edgesAdded"],
["shapesChanged", "shapesChanged"],
["edgesChanged", "edgesChanged"],
["shapesRemoved", "shapesRemoved"],
["edgesRemoved", "edgesRemoved"],
]
/**
* "Added 2 shapes, changed 1 connector". Each dictionary entry has a
* singular (...One) and plural (...Other) form; languages without plurals
* use the same text for both.
*/
export function describeChanges(
summary: ChangeSummary,
t: VersionDict,
): string {
const pieces = PARTS.filter(([field]) => summary[field] > 0).map(
([field, key]) => {
const count = summary[field]
const template = count === 1 ? t[`${key}One`] : t[`${key}Other`]
return formatMessage(template ?? "", { count })
},
)
if (pieces.length === 0) return t.noChanges ?? ""
const text = pieces.join(t.listSeparator ?? ", ")
// Sentence case for scripts with letter case
return text.charAt(0).toUpperCase() + text.slice(1)
}