feat: implement snapshot restore functionality and enhance graph state management
This commit is contained in:
@@ -25,9 +25,12 @@ const KnowledgeGraph = () => {
|
||||
const [showExcludeNodes, setShowExcludeNodes] = useState(false);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [analyzeError, setAnalyzeError] = useState(null);
|
||||
const [isRestoring, setIsRestoring] = useState(false);
|
||||
|
||||
const { topics, relations, reload, updateTopic, deleteTopic, addRelation, removeRelation, bulkSave } =
|
||||
useGraphData();
|
||||
const {
|
||||
topics, relations, snapshotMeta,
|
||||
reload, updateTopic, deleteTopic, addRelation, removeRelation, bulkSave, restoreSnapshot,
|
||||
} = useGraphData();
|
||||
|
||||
// ── Canvas sizing ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -177,6 +180,24 @@ const KnowledgeGraph = () => {
|
||||
}
|
||||
}, [selectedNode, deleteTopic]);
|
||||
|
||||
// ── Snapshot restore ─────────────────────────────────────────────────────────
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
if (
|
||||
!confirm(
|
||||
`Restore the graph to the snapshot from ${new Date(snapshotMeta?.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}?\n\nThis will undo the last Analyze & Optimize run. The snapshot will be cleared after restoring.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
setIsRestoring(true);
|
||||
try {
|
||||
await restoreSnapshot();
|
||||
setSelectedNode(null);
|
||||
} finally {
|
||||
setIsRestoring(false);
|
||||
}
|
||||
}, [restoreSnapshot, snapshotMeta]);
|
||||
|
||||
// ── AI graph analysis ────────────────────────────────────────────────────────
|
||||
|
||||
const analyzeGraph = useCallback(async () => {
|
||||
@@ -309,6 +330,9 @@ Do not return the entire graph — only the actions to take.`;
|
||||
analyzeError={analyzeError}
|
||||
disabled={topics.length === 0}
|
||||
onApplied={reload}
|
||||
snapshotMeta={snapshotMeta}
|
||||
onRestore={handleRestore}
|
||||
isRestoring={isRestoring}
|
||||
/>
|
||||
<NodeDetailPanel
|
||||
selectedNode={selectedNode}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { RotateCcw, RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import Button from '../../ui/Button';
|
||||
import SuggestionsQueue from '../SuggestionsQueue';
|
||||
|
||||
/**
|
||||
* Sidebar controls panel: the exclude-nodes toggle, the AI analysis button,
|
||||
* and the R42 suggestions queue.
|
||||
* the snapshot restore row, and the R42 suggestions queue.
|
||||
*
|
||||
* Kept intentionally thin — all async logic lives in the KnowledgeGraph
|
||||
* orchestrator and is injected via props.
|
||||
@@ -17,7 +17,14 @@ export default function GraphControls({
|
||||
analyzeError,
|
||||
disabled,
|
||||
onApplied,
|
||||
snapshotMeta,
|
||||
onRestore,
|
||||
isRestoring,
|
||||
}) {
|
||||
const snapshotLabel = snapshotMeta
|
||||
? new Date(snapshotMeta.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mb-6 pb-4 border-b border-bg-warm space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer">
|
||||
@@ -30,18 +37,33 @@ export default function GraphControls({
|
||||
Show Excluded Nodes (Reference Material)
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
onClick={onAnalyze}
|
||||
disabled={isAnalyzing || disabled}
|
||||
disabled={isAnalyzing || isRestoring || disabled}
|
||||
className="w-full flex justify-center items-center gap-2"
|
||||
>
|
||||
<RefreshCw size={16} className={isAnalyzing ? 'animate-spin' : ''} />
|
||||
{isAnalyzing ? 'Analyzing Graph…' : 'Analyze & Optimize Graph'}
|
||||
</Button>
|
||||
|
||||
{/* Restore row — only visible after a bulkSave has written a snapshot */}
|
||||
{snapshotMeta && (
|
||||
<button
|
||||
onClick={onRestore}
|
||||
disabled={isAnalyzing || isRestoring}
|
||||
className="w-full flex items-center justify-center gap-1.5 text-xs text-fg-muted hover:text-teal disabled:opacity-40 disabled:cursor-not-allowed py-1 transition-colors"
|
||||
title={`Restore graph to state from ${snapshotLabel} (${snapshotMeta.topicCount} topics, ${snapshotMeta.relationCount} relations)`}
|
||||
>
|
||||
<RotateCcw size={12} className={isRestoring ? 'animate-spin' : ''} />
|
||||
{isRestoring
|
||||
? 'Restoring…'
|
||||
: `Restore to ${snapshotLabel} · ${snapshotMeta.topicCount}t ${snapshotMeta.relationCount}r`}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{analyzeError && (
|
||||
<p className="mt-2 text-xs text-red-600 flex items-start gap-1">
|
||||
<p className="text-xs text-red-600 flex items-start gap-1">
|
||||
<AlertCircle size={14} className="shrink-0 mt-0.5" />
|
||||
{analyzeError}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user