Files
next-ai-draw-io/stores/ui-store.ts
T
dayuan.jiang 366d9ee4ff 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 22:01:09 +09:00

63 lines
2.3 KiB
TypeScript

import { create } from "zustand"
export type SettingsTab = "models" | "appearance" | "advanced" | "about"
export type MobileView = "canvas" | "chat"
interface UiState {
/** Desktop: the floating chat panel is open */
panelOpen: boolean
/** The start screen was put away (drawing by hand, an opened file) */
heroDismissed: boolean
/** Phones: which half of the app is visible */
mobileView: MobileView
settingsOpen: boolean
settingsTab: SettingsTab
/** Version shown in the compare dialog, or null when closed */
compareVersionId: string | null
saveDialogOpen: boolean
/** Bumped to ask the composer to take focus */
focusComposerToken: number
setPanelOpen: (open: boolean) => void
setHeroDismissed: (dismissed: boolean) => void
togglePanel: () => void
setMobileView: (view: MobileView) => void
openSettings: (tab?: SettingsTab) => void
setSettingsOpen: (open: boolean) => void
setSettingsTab: (tab: SettingsTab) => void
openCompare: (versionId: string) => void
closeCompare: () => void
setSaveDialogOpen: (open: boolean) => void
focusComposer: () => void
}
export const useUiStore = create<UiState>((set) => ({
panelOpen: true,
heroDismissed: false,
mobileView: "chat",
settingsOpen: false,
settingsTab: "models",
compareVersionId: null,
saveDialogOpen: false,
focusComposerToken: 0,
setPanelOpen: (open) => set({ panelOpen: open }),
setHeroDismissed: (dismissed) => set({ heroDismissed: dismissed }),
togglePanel: () => set((state) => ({ panelOpen: !state.panelOpen })),
setMobileView: (view) => set({ mobileView: view }),
openSettings: (tab) =>
set((state) => ({
settingsOpen: true,
settingsTab: tab ?? state.settingsTab,
})),
setSettingsOpen: (open) => set({ settingsOpen: open }),
setSettingsTab: (tab) => set({ settingsTab: tab }),
openCompare: (versionId) => set({ compareVersionId: versionId }),
closeCompare: () => set({ compareVersionId: null }),
setSaveDialogOpen: (open) => set({ saveDialogOpen: open }),
focusComposer: () =>
set((state) => ({
panelOpen: true,
mobileView: "chat",
focusComposerToken: state.focusComposerToken + 1,
})),
}))