summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorblackhao <13851610112@163.com>2025-12-14 01:53:35 -0600
committerblackhao <13851610112@163.com>2025-12-14 01:53:35 -0600
commit41a08c6f1818d991108725bf671b8934e5cc13bf (patch)
tree386367a826ec98a5d8256b24bbdb48370f21be7e /frontend
parent2956d4b67ca24991e0962f2be7f849ec5b682be6 (diff)
auto save
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/components/LeftSidebar.tsx42
-rw-r--r--frontend/src/store/flowStore.ts51
2 files changed, 89 insertions, 4 deletions
diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx
index 8f9f202..6806ca3 100644
--- a/frontend/src/components/LeftSidebar.tsx
+++ b/frontend/src/components/LeftSidebar.tsx
@@ -36,7 +36,9 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => {
renameProjectItem,
deleteProjectItem,
setCurrentBlueprintPath,
- clearBlueprint
+ clearBlueprint,
+ autoSave,
+ setAutoSave
} = useFlowStore();
const { user, logout } = useAuthStore();
const { setViewport, getViewport } = useReactFlow();
@@ -66,6 +68,14 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => {
const [keysMessage, setKeysMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const { getAuthHeader } = useAuthStore();
+ // Load auto-save preference from localStorage on mount
+ useEffect(() => {
+ const saved = localStorage.getItem('contextflow-autosave');
+ if (saved === 'true') {
+ setAutoSave(true);
+ }
+ }, [setAutoSave]);
+
// Load API keys when settings modal opens
useEffect(() => {
if (showUserSettings) {
@@ -668,7 +678,7 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => {
<div>
<p className={`font-medium mb-1 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>🎯 Basic Operations</p>
<ul className="list-disc list-inside space-y-1 ml-1">
- <li><span className="font-medium">Double-click</span> canvas to create a node</li>
+ <li><span className="font-medium">Right-click</span> canvas to create a node</li>
<li><span className="font-medium">Drag</span> from handle to connect nodes</li>
<li><span className="font-medium">Click</span> node to edit in right sidebar</li>
<li><span className="font-medium">Delete</span> key to remove selected node</li>
@@ -705,11 +715,37 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => {
<div className={`p-2 rounded ${isDark ? 'bg-blue-900/30' : 'bg-blue-50'}`}>
<p className={`font-medium ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>💡 Tip</p>
- <p>Use <span className="font-medium">Ctrl+S</span> to save, or enable auto-save in blueprints.</p>
+ <p>Use <span className="font-medium">Ctrl+S</span> to save, or enable auto-save below.</p>
</div>
</div>
)}
</div>
+
+ {/* Auto-Save Toggle */}
+ <div className={`border-t pt-4 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
+ <div className="flex items-center justify-between">
+ <span className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
+ Auto-Save
+ </span>
+ <button
+ onClick={() => setAutoSave(!autoSave)}
+ className={`relative w-11 h-6 rounded-full transition-colors ${
+ autoSave
+ ? 'bg-blue-600'
+ : isDark ? 'bg-gray-600' : 'bg-gray-300'
+ }`}
+ >
+ <span
+ className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${
+ autoSave ? 'translate-x-5' : 'translate-x-0'
+ }`}
+ />
+ </button>
+ </div>
+ <p className={`text-xs mt-1 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
+ Automatically save blueprint after changes (2s delay)
+ </p>
+ </div>
</div>
{/* Footer */}
diff --git a/frontend/src/store/flowStore.ts b/frontend/src/store/flowStore.ts
index a919498..e2cd5d1 100644
--- a/frontend/src/store/flowStore.ts
+++ b/frontend/src/store/flowStore.ts
@@ -151,6 +151,7 @@ interface FlowState {
currentBlueprintPath?: string;
lastViewport?: ViewportState;
saveStatus: SaveStatus;
+ autoSave: boolean;
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
@@ -205,6 +206,8 @@ interface FlowState {
setCurrentBlueprintPath: (path?: string) => void;
setLastViewport: (viewport: ViewportState) => void;
saveCurrentBlueprint: (path?: string, viewport?: ViewportState) => Promise<void>;
+ setAutoSave: (enabled: boolean) => void;
+ triggerAutoSave: () => void;
clearBlueprint: () => void;
loadArchivedNodes: () => Promise<void>;
saveArchivedNodes: () => Promise<void>;
@@ -305,6 +308,7 @@ const useFlowStore = create<FlowState>((set, get) => {
currentBlueprintPath: undefined,
lastViewport: undefined,
saveStatus: 'idle',
+ autoSave: false,
toggleTheme: () => {
const newTheme = get().theme === 'light' ? 'dark' : 'light';
@@ -430,8 +434,9 @@ const useFlowStore = create<FlowState>((set, get) => {
},
onNodesChange: (changes: NodeChange[]) => {
- // Check if any nodes are being removed
+ // Check if any nodes are being removed or positions are changing
const hasRemovals = changes.some(c => c.type === 'remove');
+ const hasPositionChanges = changes.some(c => c.type === 'position' && c.dragging === false);
set({
nodes: applyNodeChanges(changes, get().nodes) as LLMNode[],
@@ -445,12 +450,24 @@ const useFlowStore = create<FlowState>((set, get) => {
});
get().propagateTraces();
}
+
+ // Trigger auto-save for removals or position changes (after drag ends)
+ if (hasRemovals || hasPositionChanges) {
+ get().triggerAutoSave();
+ }
},
onEdgesChange: (changes: EdgeChange[]) => {
+ const hasRemovals = changes.some(c => c.type === 'remove');
+
set({
edges: applyEdgeChanges(changes, get().edges),
});
get().propagateTraces();
+
+ // Trigger auto-save for edge changes
+ if (hasRemovals) {
+ get().triggerAutoSave();
+ }
},
onConnect: (connection: Connection) => {
const { nodes, edges } = get();
@@ -901,11 +918,13 @@ const useFlowStore = create<FlowState>((set, get) => {
}, get().edges),
});
setTimeout(() => get().propagateTraces(), 0);
+ get().triggerAutoSave();
},
addNode: (node: LLMNode) => {
set((state) => ({ nodes: [...state.nodes, node] }));
setTimeout(() => get().propagateTraces(), 0);
+ get().triggerAutoSave();
},
updateNodeData: (nodeId: string, data: Partial<NodeData>) => {
@@ -923,6 +942,11 @@ const useFlowStore = create<FlowState>((set, get) => {
if (data.response !== undefined) {
get().propagateTraces();
}
+
+ // Trigger auto-save for important changes (not just userPrompt typing)
+ if (data.response !== undefined || data.status !== undefined || data.attachedFileIds !== undefined) {
+ get().triggerAutoSave();
+ }
},
setSelectedNode: (nodeId: string | null) => {
@@ -1565,6 +1589,31 @@ const useFlowStore = create<FlowState>((set, get) => {
}
},
+ setAutoSave: (enabled: boolean) => {
+ set({ autoSave: enabled });
+ // Save preference to localStorage
+ localStorage.setItem('contextflow-autosave', enabled ? 'true' : 'false');
+ },
+
+ triggerAutoSave: (() => {
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
+ return () => {
+ const state = useFlowStore.getState();
+ if (!state.autoSave || !state.currentBlueprintPath) return;
+
+ // Debounce: wait 2 seconds after last change before saving
+ if (timeoutId) clearTimeout(timeoutId);
+ timeoutId = setTimeout(async () => {
+ try {
+ await state.saveCurrentBlueprint();
+ console.log('[AutoSave] Blueprint saved');
+ } catch (e) {
+ console.error('[AutoSave] Failed to save:', e);
+ }
+ }, 2000);
+ };
+ })(),
+
clearBlueprint: () => {
set({
nodes: [],