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.ts109
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 {