Files
memind/src/hooks/usePageDraftHistory.ts
T
John 25d9c8c364 Add MindSpace visual page editor and Plaza local Tunnel deployment.
In-preview HTML editing with undo/redo sync replaces Agent patch auto-save; Plaza runs on local Mac via Cloudflare Tunnel with embed height and CSP fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:57:22 -07:00

118 lines
3.2 KiB
TypeScript

import { useCallback, useRef, useState } from 'react';
export type PageDraftSnapshot = {
title: string;
summary: string;
content: string;
};
const MAX_HISTORY = 50;
const HISTORY_DEBOUNCE_MS = 600;
export function usePageDraftHistory(initial: PageDraftSnapshot) {
const [snapshot, setSnapshot] = useState(initial);
const stableRef = useRef(initial);
const pastRef = useRef<PageDraftSnapshot[]>([]);
const futureRef = useRef<PageDraftSnapshot[]>([]);
const debounceRef = useRef<number | null>(null);
const [historyRevision, setHistoryRevision] = useState(0);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const bump = () => {
setCanUndo(pastRef.current.length > 0);
setCanRedo(futureRef.current.length > 0);
setHistoryRevision((value) => value + 1);
};
const flushDebounced = useCallback(() => {
if (debounceRef.current != null) {
window.clearTimeout(debounceRef.current);
debounceRef.current = null;
}
}, []);
const recordStable = useCallback((next: PageDraftSnapshot) => {
if (JSON.stringify(stableRef.current) === JSON.stringify(next)) return;
pastRef.current = [...pastRef.current.slice(-(MAX_HISTORY - 1)), stableRef.current];
futureRef.current = [];
stableRef.current = next;
bump();
}, []);
const update = useCallback(
(next: PageDraftSnapshot) => {
setSnapshot(next);
bump();
flushDebounced();
debounceRef.current = window.setTimeout(() => {
debounceRef.current = null;
recordStable(next);
}, HISTORY_DEBOUNCE_MS);
},
[flushDebounced, recordStable],
);
const replace = useCallback(
(next: PageDraftSnapshot, options?: { recordHistory?: boolean }) => {
flushDebounced();
if (options?.recordHistory) {
pastRef.current = [...pastRef.current.slice(-(MAX_HISTORY - 1)), snapshot];
futureRef.current = [];
}
stableRef.current = next;
setSnapshot(next);
bump();
},
[flushDebounced, snapshot],
);
const undo = useCallback((): PageDraftSnapshot | null => {
flushDebounced();
const previous = pastRef.current[pastRef.current.length - 1];
if (!previous) return null;
pastRef.current = pastRef.current.slice(0, -1);
futureRef.current = [snapshot, ...futureRef.current];
stableRef.current = previous;
setSnapshot(previous);
bump();
return previous;
}, [flushDebounced, snapshot]);
const redo = useCallback((): PageDraftSnapshot | null => {
flushDebounced();
const next = futureRef.current[0];
if (!next) return null;
futureRef.current = futureRef.current.slice(1);
pastRef.current = [...pastRef.current, snapshot];
stableRef.current = next;
setSnapshot(next);
bump();
return next;
}, [flushDebounced, snapshot]);
const reset = useCallback(
(next: PageDraftSnapshot) => {
flushDebounced();
pastRef.current = [];
futureRef.current = [];
stableRef.current = next;
setSnapshot(next);
bump();
},
[flushDebounced],
);
return {
snapshot,
update,
replace,
undo,
redo,
reset,
canUndo,
canRedo,
historyRevision,
};
}