summaryrefslogtreecommitdiff
path: root/frontend/src/store/flowStore.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/store/flowStore.ts')
-rw-r--r--frontend/src/store/flowStore.ts266
1 files changed, 247 insertions, 19 deletions
diff --git a/frontend/src/store/flowStore.ts b/frontend/src/store/flowStore.ts
index 5ed66e6..56ade75 100644
--- a/frontend/src/store/flowStore.ts
+++ b/frontend/src/store/flowStore.ts
@@ -15,7 +15,32 @@ import {
getOutgoers
} from 'reactflow';
+// --- Project / Blueprint types ---
+export interface ViewportState {
+ x: number;
+ y: number;
+ zoom: number;
+}
+
+export interface BlueprintDocument {
+ version: number;
+ nodes: Node<NodeData>[];
+ edges: Edge[];
+ viewport?: ViewportState;
+ theme?: 'light' | 'dark';
+}
+
+export interface FSItem {
+ name: string;
+ path: string; // path relative to user root
+ type: 'file' | 'folder';
+ size?: number | null;
+ mtime?: number | null;
+ children?: FSItem[];
+}
+
export type NodeStatus = 'idle' | 'loading' | 'success' | 'error';
+export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
export interface Message {
id?: string;
@@ -90,6 +115,10 @@ export interface ArchivedNode {
systemPrompt: string;
temperature: number;
reasoningEffort: 'low' | 'medium' | 'high';
+ userPrompt?: string;
+ response?: string;
+ enableGoogleSearch?: boolean;
+ mergeStrategy?: 'raw' | 'smart';
}
interface FlowState {
@@ -98,6 +127,10 @@ interface FlowState {
selectedNodeId: string | null;
archivedNodes: ArchivedNode[]; // Stored node templates
theme: 'light' | 'dark';
+ projectTree: FSItem[];
+ currentBlueprintPath?: string;
+ lastViewport?: ViewportState;
+ saveStatus: SaveStatus;
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
@@ -140,6 +173,22 @@ interface FlowState {
config: Partial<NodeData>
) => string; // Returns new node ID
+ // Blueprint serialization / persistence
+ serializeBlueprint: (viewport?: ViewportState) => BlueprintDocument;
+ loadBlueprint: (doc: BlueprintDocument) => ViewportState | undefined;
+ saveBlueprintFile: (path: string, viewport?: ViewportState) => Promise<void>;
+ readBlueprintFile: (path: string) => Promise<BlueprintDocument>;
+ refreshProjectTree: () => Promise<FSItem[]>;
+ createProjectFolder: (path: string) => Promise<void>;
+ renameProjectItem: (path: string, newName?: string, newPath?: string) => Promise<void>;
+ deleteProjectItem: (path: string, isFolder?: boolean) => Promise<void>;
+ setCurrentBlueprintPath: (path?: string) => void;
+ setLastViewport: (viewport: ViewportState) => void;
+ saveCurrentBlueprint: (path?: string, viewport?: ViewportState) => Promise<void>;
+ clearBlueprint: () => void;
+ loadArchivedNodes: () => Promise<void>;
+ saveArchivedNodes: () => Promise<void>;
+
// Merge trace functions
createMergedTrace: (
nodeId: string,
@@ -172,12 +221,37 @@ const getStableColor = (str: string) => {
return `hsl(${hue}, 70%, 60%)`;
};
-const useFlowStore = create<FlowState>((set, get) => ({
+const API_BASE = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000';
+const DEFAULT_USER = 'test';
+
+const jsonFetch = async <T>(url: string, options?: RequestInit): Promise<T> => {
+ const res = await fetch(url, options);
+ if (!res.ok) {
+ const detail = await res.text();
+ throw new Error(detail || `Request failed: ${res.status}`);
+ }
+ return res.json() as Promise<T>;
+};
+
+const useFlowStore = create<FlowState>((set, get) => {
+
+ const validateBlueprint = (doc: any): BlueprintDocument => {
+ if (!doc || typeof doc !== 'object') throw new Error('Invalid blueprint: not an object');
+ if (typeof doc.version !== 'number') throw new Error('Invalid blueprint: missing version');
+ if (!Array.isArray(doc.nodes) || !Array.isArray(doc.edges)) throw new Error('Invalid blueprint: nodes/edges missing');
+ return doc as BlueprintDocument;
+ };
+
+ return {
nodes: [],
edges: [],
selectedNodeId: null,
archivedNodes: [],
theme: 'light' as const,
+ projectTree: [],
+ currentBlueprintPath: undefined,
+ lastViewport: undefined,
+ saveStatus: 'idle',
toggleTheme: () => {
const newTheme = get().theme === 'light' ? 'dark' : 'light';
@@ -190,6 +264,10 @@ const useFlowStore = create<FlowState>((set, get) => ({
}
},
+ setLastViewport: (viewport: ViewportState) => {
+ set({ lastViewport: viewport });
+ },
+
findNonOverlappingPosition: (baseX: number, baseY: number) => {
const { nodes } = get();
// Estimate larger dimensions to be safe, considering dynamic handles
@@ -1036,18 +1114,24 @@ const useFlowStore = create<FlowState>((set, get) => ({
model: node.data.model,
systemPrompt: node.data.systemPrompt,
temperature: node.data.temperature,
- reasoningEffort: node.data.reasoningEffort || 'medium'
+ reasoningEffort: node.data.reasoningEffort || 'medium',
+ userPrompt: node.data.userPrompt,
+ response: node.data.response,
+ enableGoogleSearch: node.data.enableGoogleSearch,
+ mergeStrategy: node.data.mergeStrategy
};
set(state => ({
archivedNodes: [...state.archivedNodes, archived]
}));
+ setTimeout(() => get().saveArchivedNodes().catch(() => {}), 0);
},
removeFromArchive: (archiveId: string) => {
set(state => ({
archivedNodes: state.archivedNodes.filter(a => a.id !== archiveId)
}));
+ setTimeout(() => get().saveArchivedNodes().catch(() => {}), 0);
},
createNodeFromArchive: (archiveId: string, position: { x: number; y: number }) => {
@@ -1063,15 +1147,16 @@ const useFlowStore = create<FlowState>((set, get) => ({
model: archived.model,
temperature: archived.temperature,
systemPrompt: archived.systemPrompt,
- userPrompt: '',
- mergeStrategy: 'smart',
+ userPrompt: archived.userPrompt || '',
reasoningEffort: archived.reasoningEffort,
+ enableGoogleSearch: archived.enableGoogleSearch,
+ mergeStrategy: archived.mergeStrategy || 'smart',
traces: [],
outgoingTraces: [],
forkedTraces: [],
mergedTraces: [],
activeTraceIds: [],
- response: '',
+ response: archived.response || '',
status: 'idle',
inputs: 1
}
@@ -1313,6 +1398,139 @@ const useFlowStore = create<FlowState>((set, get) => ({
return newNodeId;
},
+ // -------- Blueprint serialization / persistence --------
+ setCurrentBlueprintPath: (path?: string) => {
+ set({ currentBlueprintPath: path });
+ },
+
+ serializeBlueprint: (viewport?: ViewportState): BlueprintDocument => {
+ return {
+ version: 1,
+ nodes: get().nodes,
+ edges: get().edges,
+ viewport: viewport || get().lastViewport,
+ theme: get().theme,
+ };
+ },
+
+ loadBlueprint: (doc: BlueprintDocument): ViewportState | undefined => {
+ set({
+ nodes: (doc.nodes || []) as LLMNode[],
+ edges: (doc.edges || []) as Edge[],
+ theme: doc.theme || get().theme,
+ selectedNodeId: null,
+ lastViewport: doc.viewport || get().lastViewport,
+ });
+ // Recompute traces after loading
+ setTimeout(() => get().propagateTraces(), 0);
+ return doc.viewport;
+ },
+
+ saveBlueprintFile: async (path: string, viewport?: ViewportState) => {
+ const payload = get().serializeBlueprint(viewport);
+ await jsonFetch(`${API_BASE}/api/projects/save_blueprint`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ user: DEFAULT_USER,
+ path,
+ content: payload,
+ }),
+ });
+ set({ currentBlueprintPath: path, lastViewport: payload.viewport });
+ await get().refreshProjectTree();
+ },
+
+ readBlueprintFile: async (path: string): Promise<BlueprintDocument> => {
+ const res = await jsonFetch<{ content: BlueprintDocument }>(
+ `${API_BASE}/api/projects/file?user=${encodeURIComponent(DEFAULT_USER)}&path=${encodeURIComponent(path)}`
+ );
+ return validateBlueprint(res.content);
+ },
+
+ refreshProjectTree: async () => {
+ const tree = await jsonFetch<FSItem[]>(
+ `${API_BASE}/api/projects/tree?user=${encodeURIComponent(DEFAULT_USER)}`
+ );
+ set({ projectTree: tree });
+ return tree;
+ },
+
+ createProjectFolder: async (path: string) => {
+ await jsonFetch(`${API_BASE}/api/projects/create_folder`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user: DEFAULT_USER, path }),
+ });
+ await get().refreshProjectTree();
+ },
+
+ renameProjectItem: async (path: string, newName?: string, newPath?: string) => {
+ if (!newName && !newPath) {
+ throw new Error('newName or newPath is required');
+ }
+ await jsonFetch(`${API_BASE}/api/projects/rename`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user: DEFAULT_USER, path, new_name: newName, new_path: newPath }),
+ });
+ await get().refreshProjectTree();
+ },
+
+ deleteProjectItem: async (path: string, isFolder = false) => {
+ await jsonFetch(`${API_BASE}/api/projects/delete`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user: DEFAULT_USER, path, is_folder: isFolder }),
+ });
+ await get().refreshProjectTree();
+ },
+
+ saveCurrentBlueprint: async (path?: string, viewport?: ViewportState) => {
+ const targetPath = path || get().currentBlueprintPath;
+ if (!targetPath) {
+ throw new Error('No blueprint path. Please provide a file name.');
+ }
+ set({ saveStatus: 'saving' });
+ try {
+ await get().saveBlueprintFile(targetPath, viewport);
+ set({ saveStatus: 'saved', currentBlueprintPath: targetPath });
+ } catch (e) {
+ console.error(e);
+ set({ saveStatus: 'error' });
+ throw e;
+ }
+ },
+
+ clearBlueprint: () => {
+ set({
+ nodes: [],
+ edges: [],
+ selectedNodeId: null,
+ currentBlueprintPath: undefined,
+ lastViewport: undefined,
+ saveStatus: 'idle',
+ });
+ },
+
+ loadArchivedNodes: async () => {
+ const res = await jsonFetch<{ archived: ArchivedNode[] }>(
+ `${API_BASE}/api/projects/archived?user=${encodeURIComponent(DEFAULT_USER)}`
+ );
+ set({ archivedNodes: res.archived || [] });
+ },
+
+ saveArchivedNodes: async () => {
+ const payload = { user: DEFAULT_USER, archived: get().archivedNodes };
+ await jsonFetch(`${API_BASE}/api/projects/archived`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+ },
+
+ // --------------------------------------------------------
+
// Compute merged messages based on strategy
// Optional tracesOverride parameter to use latest traces during propagation
computeMergedMessages: (nodeId: string, sourceTraceIds: string[], strategy: MergeStrategy, tracesOverride?: Trace[]): Message[] => {
@@ -1487,10 +1705,13 @@ const useFlowStore = create<FlowState>((set, get) => ({
const node = nodes.find(n => n.id === nodeId);
if (!node) return '';
- // Get colors from source traces
- const colors = sourceTraceIds
- .map(id => node.data.traces.find((t: Trace) => t.id === id)?.color)
- .filter((c): c is string => c !== undefined);
+ // Get colors from source traces (preserve multi-colors for merged parents)
+ const colors = sourceTraceIds.flatMap(id => {
+ const t = node.data.traces.find((tr: Trace) => tr.id === id);
+ if (!t) return [];
+ if (t.mergedColors && t.mergedColors.length > 0) return t.mergedColors;
+ return t.color ? [t.color] : [];
+ });
// Compute merged messages
const messages = computeMergedMessages(nodeId, sourceTraceIds, strategy);
@@ -1533,12 +1754,15 @@ const useFlowStore = create<FlowState>((set, get) => ({
const newSourceTraceIds = updates.sourceTraceIds || current.sourceTraceIds;
const newStrategy = updates.strategy || current.strategy;
- // Recompute colors if source traces changed
+ // Recompute colors if source traces changed (preserve multi-colors)
let newColors = current.colors;
if (updates.sourceTraceIds) {
- newColors = updates.sourceTraceIds
- .map(id => node.data.traces.find((t: Trace) => t.id === id)?.color)
- .filter((c): c is string => c !== undefined);
+ newColors = updates.sourceTraceIds.flatMap(id => {
+ const t = node.data.traces.find((tr: Trace) => tr.id === id);
+ if (!t) return [];
+ if (t.mergedColors && t.mergedColors.length > 0) return t.mergedColors;
+ return t.color ? [t.color] : [];
+ });
}
// Recompute messages if source or strategy changed
@@ -1919,10 +2143,13 @@ const useFlowStore = create<FlowState>((set, get) => ({
// Get prepend messages for this merged trace
const mergedPrepend = prependMessages.get(merged.id) || [];
- // Update colors from current traces
- const updatedColors = merged.sourceTraceIds
- .map(id => uniqueIncoming.find(t => t.id === id)?.color)
- .filter((c): c is string => c !== undefined);
+ // Update colors from current traces (preserve multi-colors)
+ const updatedColors = merged.sourceTraceIds.flatMap(id => {
+ const t = uniqueIncoming.find(trace => trace.id === id);
+ if (!t) return [];
+ if (t.mergedColors && t.mergedColors.length > 0) return t.mergedColors;
+ return t.color ? [t.color] : [];
+ });
// Combine all messages for this merged trace
const mergedMessages = [...mergedPrepend, ...filteredMessages, ...myResponseMsg];
@@ -2017,7 +2244,8 @@ const useFlowStore = create<FlowState>((set, get) => ({
};
})
}));
- }
-}));
+ },
+ };
+});
export default useFlowStore;