import { nanoid } from "nanoid" import { create } from "zustand" import type { VersionItem } from "@/components/canvas/versions-context" /** * One diagram state produced by an AI change, shown as a card in the chat. * What the card displays is the VersionItem part; the rest ties the version * to its chat turn. The number stays when older versions are dropped; the * "before" document is kept only on the latest version; older sessions * have no pageId (the first page). */ export interface DiagramVersion extends VersionItem { /** Index of the user message that started this turn */ turnIndex: number toolCallId?: string /** Saved with the session: this version is what the canvas shows */ onCanvas?: boolean /** Saved with the session: this version's change was undone */ undone?: boolean } // The two flags live on the versions so they are saved with the session function withFlags( versions: DiagramVersion[], onCanvasId: string | null, undoneId: string | null, ): DiagramVersion[] { return versions.map(({ onCanvas, undone, ...rest }) => ({ ...rest, ...(rest.id === onCanvasId && { onCanvas: true }), ...(rest.id === undoneId && { undone: true }), })) } // Versions are stored with the session; keep the list bounded export const MAX_VERSIONS = 20 interface VersionsState { versions: DiagramVersion[] /** The change on the latest version was undone with its card's button */ undoneVersionId: string | null /** The version the canvas shows (null after an undo or hand edits) */ onCanvasVersionId: string | null setVersions: (versions: DiagramVersion[]) => void addVersion: (version: Omit) => string updateVersion: (id: string, patch: Partial) => void /** Drop versions from turns that are being re-run (edit or regenerate) */ removeFromTurn: (turnIndex: number) => void /** The change of this version was undone; null: no undone change */ setUndone: (id: string | null) => void /** This version is on the canvas again (restored or redone) */ setOnCanvas: (id: string) => void /** The canvas was changed by hand (undo included): no version is on it */ clearCanvasFlags: () => void clear: () => void } export const useVersionsStore = create((set) => ({ versions: [], undoneVersionId: null, onCanvasVersionId: null, setVersions: (versions) => set({ // A missing thumbnail could not be made: no "rendering" forever. versions: versions.map((v) => ({ ...v, svg: v.svg ?? "", })), undoneVersionId: versions.find((v) => v.undone)?.id ?? null, onCanvasVersionId: versions.find((v) => v.onCanvas)?.id ?? null, }), addVersion: (version) => { const id = nanoid(10) set((state) => { // Only the newest version keeps its "before" XML const older = state.versions.map(({ beforeXml, ...rest }) => rest) const number = (state.versions.at(-1)?.number ?? 0) + 1 const next = [...older, { ...version, id, number }].slice( -MAX_VERSIONS, ) return { versions: withFlags(next, id, null), undoneVersionId: null, onCanvasVersionId: id, } }) return id }, updateVersion: (id, patch) => set((state) => ({ versions: state.versions.map((v) => v.id === id ? { ...v, ...patch } : v, ), })), removeFromTurn: (turnIndex) => set((state) => { const versions = state.versions.filter( (v) => v.turnIndex < turnIndex, ) // The canvas goes back to the snapshot before that turn, which // can hold hand edits made after the latest kept version return { versions: withFlags(versions, null, null), undoneVersionId: null, onCanvasVersionId: null, } }), setUndone: (id) => set((state) => ({ versions: withFlags(state.versions, null, id), undoneVersionId: id, onCanvasVersionId: null, })), setOnCanvas: (id) => set((state) => ({ versions: withFlags(state.versions, id, null), undoneVersionId: null, onCanvasVersionId: id, })), clearCanvasFlags: () => set((state) => state.onCanvasVersionId === null && state.undoneVersionId === null ? state : { versions: withFlags(state.versions, null, null), undoneVersionId: null, onCanvasVersionId: null, }, ), clear: () => set({ versions: [], undoneVersionId: null, onCanvasVersionId: null }), }))