diff options
Diffstat (limited to 'frontend/src/store/flowStore.ts')
| -rw-r--r-- | frontend/src/store/flowStore.ts | 109 |
1 files changed, 93 insertions, 16 deletions
diff --git a/frontend/src/store/flowStore.ts b/frontend/src/store/flowStore.ts index a049a8a..498937e 100644 --- a/frontend/src/store/flowStore.ts +++ b/frontend/src/store/flowStore.ts @@ -85,6 +85,7 @@ export interface NodeData { mergeStrategy: 'raw' | 'smart'; enableGoogleSearch?: boolean; reasoningEffort: 'low' | 'medium' | 'high'; // For OpenAI reasoning models + attachedFileIds?: string[]; // IDs of files attached to this node disabled?: boolean; // Greyed out, no interaction // Traces logic @@ -118,6 +119,7 @@ export interface ArchivedNode { userPrompt?: string; response?: string; enableGoogleSearch?: boolean; + attachedFileIds?: string[]; mergeStrategy?: 'raw' | 'smart'; } @@ -129,6 +131,14 @@ export interface FileMeta { created_at: number; provider?: string; provider_file_id?: string; + scopes?: string[]; // "project_path/node_id" composite keys +} + +export type UploadFileProvider = 'local' | 'openai' | 'google'; + +export interface UploadFileOptions { + provider?: UploadFileProvider; + purpose?: string; } interface FlowState { @@ -137,6 +147,7 @@ interface FlowState { selectedNodeId: string | null; archivedNodes: ArchivedNode[]; // Stored node templates files: FileMeta[]; + uploadingFileIds?: string[]; theme: 'light' | 'dark'; projectTree: FSItem[]; currentBlueprintPath?: string; @@ -200,9 +211,12 @@ interface FlowState { loadArchivedNodes: () => Promise<void>; saveArchivedNodes: () => Promise<void>; refreshFiles: () => Promise<void>; - uploadFile: (file: File) => Promise<FileMeta>; + uploadFile: (file: File, options?: UploadFileOptions) => Promise<FileMeta>; deleteFile: (fileId: string) => Promise<void>; setFiles: (files: FileMeta[]) => void; + setUploading: (ids: string[]) => void; + addFileScope: (fileId: string, scope: string) => Promise<void>; + removeFileScope: (fileId: string, scope: string) => Promise<void>; // Merge trace functions createMergedTrace: ( @@ -263,6 +277,7 @@ const useFlowStore = create<FlowState>((set, get) => { selectedNodeId: null, archivedNodes: [], files: [], + uploadingFileIds: [], theme: 'light' as const, projectTree: [], currentBlueprintPath: undefined, @@ -287,6 +302,9 @@ const useFlowStore = create<FlowState>((set, get) => { setFiles: (files: FileMeta[]) => { set({ files }); }, + setUploading: (ids: string[]) => { + set({ uploadingFileIds: ids }); + }, findNonOverlappingPosition: (baseX: number, baseY: number) => { const { nodes } = get(); // Estimate larger dimensions to be safe, considering dynamic handles @@ -879,7 +897,9 @@ const useFlowStore = create<FlowState>((set, get) => { }), })); - if (data.response !== undefined || data.userPrompt !== undefined) { + // Only propagate traces when response changes (affects downstream context) + // Do NOT propagate on userPrompt changes to avoid resetting activeTraceIds during typing + if (data.response !== undefined) { get().propagateTraces(); } }, @@ -1137,7 +1157,8 @@ const useFlowStore = create<FlowState>((set, get) => { userPrompt: node.data.userPrompt, response: node.data.response, enableGoogleSearch: node.data.enableGoogleSearch, - mergeStrategy: node.data.mergeStrategy + mergeStrategy: node.data.mergeStrategy, + attachedFileIds: node.data.attachedFileIds || [] }; set(state => ({ @@ -1175,6 +1196,7 @@ const useFlowStore = create<FlowState>((set, get) => { forkedTraces: [], mergedTraces: [], activeTraceIds: [], + attachedFileIds: archived.attachedFileIds || [], response: archived.response || '', status: 'idle', inputs: 1 @@ -1392,6 +1414,7 @@ const useFlowStore = create<FlowState>((set, get) => { forkedTraces: [], mergedTraces: [], activeTraceIds: [], + attachedFileIds: [], response, status: 'success', inputs: 1, @@ -1556,27 +1579,64 @@ const useFlowStore = create<FlowState>((set, get) => { set({ files: res.files || [] }); }, - uploadFile: async (file: File) => { + uploadFile: async (file: File, options?: UploadFileOptions) => { + const provider = options?.provider ?? 'local'; + const purpose = options?.purpose; + const tempId = `${file.name}-${Date.now()}`; + const prev = get().uploadingFileIds || []; + set({ uploadingFileIds: [...prev, tempId] }); const form = new FormData(); form.append('file', file); - const res = await fetch(`${API_BASE}/api/files/upload?user=${encodeURIComponent(DEFAULT_USER)}`, { + form.append('provider', provider); + if (purpose) { + form.append('purpose', purpose); + } + try { + const res = await fetch(`${API_BASE}/api/files/upload?user=${encodeURIComponent(DEFAULT_USER)}`, { + method: 'POST', + body: form, + }); + if (!res.ok) { + throw new Error(await res.text()); + } + const data = await res.json(); + if (!data.file) { + throw new Error('Upload succeeded but no file info returned'); + } + await get().refreshFiles(); + return data.file as FileMeta; + } finally { + set({ uploadingFileIds: (get().uploadingFileIds || []).filter(id => id !== tempId) }); + } + }, + + deleteFile: async (fileId: string) => { + const res = await fetch(`${API_BASE}/api/files/delete?user=${encodeURIComponent(DEFAULT_USER)}&file_id=${encodeURIComponent(fileId)}`, { method: 'POST', - body: form, }); if (!res.ok) { throw new Error(await res.text()); } - const data = await res.json(); - if (!data.file) { - throw new Error('Upload succeeded but no file info returned'); + await get().refreshFiles(); + }, + + addFileScope: async (fileId: string, scope: string) => { + const res = await fetch(`${API_BASE}/api/files/add_scope`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user: DEFAULT_USER, file_id: fileId, scope }), + }); + if (!res.ok) { + throw new Error(await res.text()); } await get().refreshFiles(); - return data.file as FileMeta; }, - deleteFile: async (fileId: string) => { - const res = await fetch(`${API_BASE}/api/files/delete?user=${encodeURIComponent(DEFAULT_USER)}&file_id=${encodeURIComponent(fileId)}`, { + removeFileScope: async (fileId: string, scope: string) => { + const res = await fetch(`${API_BASE}/api/files/remove_scope`, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user: DEFAULT_USER, file_id: fileId, scope }), }); if (!res.ok) { throw new Error(await res.text()); @@ -2279,11 +2339,28 @@ const useFlowStore = create<FlowState>((set, get) => { ? cleanedForks : (n.data.forkedTraces || []); - // Also update activeTraceIds to remove deleted merged traces and orphaned fork traces + // Update activeTraceIds: remove deleted merged traces and truly orphaned fork traces + // Preserve all other valid trace selections const activeForkIds = filteredForkedTraces.map(f => f.id); - const filteredActiveTraceIds = (n.data.activeTraceIds || []).filter( - (id: string) => !mergedToDelete.includes(id) && - (activeForkIds.includes(id) || !id.startsWith('fork-')) + const incomingTraceIds = traces.map(t => t.id); + const outgoingTraceIds = outTraces.map(t => t.id); + const originalActiveIds = n.data.activeTraceIds || []; + + const filteredActiveTraceIds = originalActiveIds.filter( + (id: string) => { + // Remove deleted merged traces + if (mergedToDelete.includes(id)) return false; + // For fork traces: only remove if truly orphaned + // A fork is NOT orphaned if it's in forkedTraces, incoming traces, or outgoing traces + if (id.startsWith('fork-')) { + const isInForkedTraces = activeForkIds.includes(id); + const isIncomingTrace = incomingTraceIds.includes(id); + const isOutgoingTrace = outgoingTraceIds.includes(id); + if (!isInForkedTraces && !isIncomingTrace && !isOutgoingTrace) return false; + } + // Keep all other selections + return true; + } ); return { |
