Files
openmaic/OpenMAIC/lib/agent/client/regen-snapshots.ts
2026-08-16 14:58:47 +08:00

96 lines
3.7 KiB
TypeScript

'use client';
/**
* Snapshot store for `regenerate_scene` "restore previous" support.
*
* Whole-slide regeneration applies directly to the canvas (snappy), but it
* overwrites whatever the user had — including hand-edits. Before applying, the
* runtime snapshots the pre-regenerate scene here, keyed by toolCallId, so the
* tool card can offer a "还原到重生成前 / Restore previous" button that does not
* rely on the user remembering Ctrl+Z.
*/
import { create } from 'zustand';
import type { Action } from '@/lib/types/action';
import type { SceneContent } from '@/lib/types/stage';
export interface RegenSnapshot {
sceneId: string;
content: SceneContent;
actions: Action[];
/**
* Narration-only regen (`regenerate_scene_actions`): the slide content was NOT
* changed, so Restore must revert ONLY the actions — re-applying the snapshot
* content would clobber any canvas edits the user made since, and needlessly
* reseed the slide edit session.
*/
actionsOnly?: boolean;
restored: boolean;
/**
* Post-edit state (the patch the tool applied), kept so an undo can be RESUMED
* (redo). Absent for cards restored from storage after a refresh, where resume
* isn't possible (the in-memory state is gone).
*/
redo?: { content?: SceneContent; actions?: Action[] };
}
/** Re-applies the snapshot to the stage store (injected so the store stays testable).
* `actions` is optional: omitting it preserves the scene's current actions
* (updateScene shallow-merges), whereas `actions: []` would wipe them. */
export type RestoreApplyFn = (
sceneId: string,
patch: { content?: SceneContent; actions?: Action[] },
) => void;
interface RegenSnapshotsState {
snapshots: Record<string, RegenSnapshot>;
setSnapshot: (toolCallId: string, snap: Omit<RegenSnapshot, 'restored'>) => void;
/**
* Toggle: when not yet restored, applies the pre-edit snapshot (undo) and marks
* it restored; when already restored, RE-applies the post-edit state (resume /
* redo) and toggles back. A no-op if there's no snapshot, or when resuming but
* no `redo` state was captured.
*/
restore: (toolCallId: string, apply: RestoreApplyFn) => void;
/** Drop all snapshots (e.g. on "新对话") so stale entries don't accumulate. */
clearAll: () => void;
}
export const useRegenSnapshots = create<RegenSnapshotsState>((set, get) => ({
snapshots: {},
setSnapshot: (toolCallId, snap) =>
set((s) => ({
snapshots: { ...s.snapshots, [toolCallId]: { ...snap, restored: false } },
})),
restore: (toolCallId, apply) => {
const snap = get().snapshots[toolCallId];
if (!snap) return;
if (!snap.restored) {
// Undo → pre-edit state.
apply(
snap.sceneId,
snap.actionsOnly
? { actions: snap.actions }
: { content: snap.content, actions: snap.actions },
);
set((s) => ({
snapshots: { ...s.snapshots, [toolCallId]: { ...snap, restored: true } },
}));
return;
}
// Resume (redo) → re-apply EXACTLY the post-edit patch the tool applied,
// including only the keys it actually carried. Injecting `actions: []` (or
// `content: undefined`) for a key the patch never had would clobber the
// scene's existing narration/content on resume.
const redo = snap.redo;
if (!redo) return;
const patch: { content?: SceneContent; actions?: Action[] } = {};
if (redo.content !== undefined) patch.content = redo.content;
if (redo.actions !== undefined) patch.actions = redo.actions;
apply(snap.sceneId, patch);
set((s) => ({
snapshots: { ...s.snapshots, [toolCallId]: { ...snap, restored: false } },
}));
},
clearAll: () => set({ snapshots: {} }),
}));