diff options
| author | blackhao <13851610112@163.com> | 2025-12-09 18:04:33 -0600 |
|---|---|---|
| committer | blackhao <13851610112@163.com> | 2025-12-09 18:04:33 -0600 |
| commit | 0dcaf9d7da9fa5041fbd5489a60886ceb416b1d4 (patch) | |
| tree | ad3d31fea269aa49392c1ae013bee91d1fdfa18d /frontend | |
| parent | 6abff8474c593118fc52afaa9e0b432346aeffa5 (diff) | |
upload files to backend
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/components/LeftSidebar.tsx | 136 | ||||
| -rw-r--r-- | frontend/src/store/flowStore.ts | 55 |
2 files changed, 187 insertions, 4 deletions
diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx index 1b7ccb2..a75df39 100644 --- a/frontend/src/components/LeftSidebar.tsx +++ b/frontend/src/components/LeftSidebar.tsx @@ -4,7 +4,7 @@ import { Folder, FileText, Archive, ChevronLeft, ChevronRight, Trash2, MessageSquare, MoreVertical, Download, Upload, Plus, RefreshCw, Edit3 } from 'lucide-react'; -import useFlowStore, { type FSItem, type BlueprintDocument } from '../store/flowStore'; +import useFlowStore, { type FSItem, type BlueprintDocument, type FileMeta } from '../store/flowStore'; interface LeftSidebarProps { isOpen: boolean; @@ -18,11 +18,15 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { removeFromArchive, createNodeFromArchive, theme, + files, projectTree, currentBlueprintPath, saveStatus, refreshProjectTree, loadArchivedNodes, + refreshFiles, + uploadFile, + deleteFile, readBlueprintFile, loadBlueprint, saveBlueprintFile, @@ -37,6 +41,7 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { const { setViewport, getViewport } = useReactFlow(); const isDark = theme === 'dark'; const fileInputRef = useRef<HTMLInputElement | null>(null); + const fileUploadRef = useRef<HTMLInputElement | null>(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; item?: FSItem } | null>(null); const [currentFolder, setCurrentFolder] = useState<string>('.'); const [dragItem, setDragItem] = useState<FSItem | null>(null); @@ -93,6 +98,13 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { loadArchivedNodes().catch(() => {}); }, [loadArchivedNodes]); + // Load files when entering files tab + useEffect(() => { + if (activeTab === 'files') { + refreshFiles().catch(() => {}); + } + }, [activeTab, refreshFiles]); + // Context menu handlers const openContextMenu = (e: React.MouseEvent, item?: FSItem) => { e.preventDefault(); @@ -217,6 +229,52 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { await refreshProjectTree(); }; + // Files tab handlers + const handleFilesUpload = async (list: FileList) => { + let ok = 0; + let failed: string[] = []; + for (const f of Array.from(list)) { + try { + await uploadFile(f); + ok += 1; + } catch (e) { + console.error(e); + failed.push(`${f.name}: ${(e as Error).message}`); + } + } + await refreshFiles(); + if (failed.length) { + alert(`Some files failed:\n${failed.join('\n')}`); + } else if (ok > 0) { + // Optional: brief feedback + console.info(`Uploaded ${ok} file(s)`); + } + }; + + const handleFilesInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => { + const files = e.target.files; + if (files && files.length > 0) { + await handleFilesUpload(files); + e.target.value = ''; + } + }; + + const handleDownloadFile = (file: FileMeta) => { + const url = `${import.meta.env.VITE_BACKEND_URL || 'http://localhost:8000'}/api/files/download?user=test&file_id=${encodeURIComponent(file.id)}`; + const a = document.createElement('a'); + a.href = url; + a.download = file.name; + a.click(); + }; + + const formatSize = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; + }; + // Fade-out for "Saved" indicator useEffect(() => { if (saveStatus === 'saved') { @@ -517,9 +575,79 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { </div> )} {activeTab === 'files' && ( - <div className="flex flex-col items-center justify-center h-full opacity-50"> - <FileText size={48} className="mb-2" /> - <p>File manager coming soon</p> + <div + className="flex flex-col h-full gap-2" + onDragOver={(e) => { if (e.dataTransfer.types.includes('Files')) e.preventDefault(); }} + onDrop={async (e) => { + if (e.dataTransfer.files?.length) { + e.preventDefault(); + await handleFilesUpload(e.dataTransfer.files); + } + }} + > + <div className="flex items-center gap-2"> + <button + onClick={() => refreshFiles()} + className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`} + title="Refresh files" + > + <RefreshCw size={14} /> + </button> + <button + onClick={() => fileUploadRef.current?.click()} + className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`} + title="Upload files (drag & drop supported)" + > + <Upload size={14} /> + </button> + <input + ref={fileUploadRef} + type="file" + multiple + className="hidden" + onChange={handleFilesInputChange} + /> + <span className={`text-xs ${isDark ? 'text-gray-500' : 'text-gray-500'}`}>Drag files here or click upload</span> + </div> + + {files.length === 0 ? ( + <div className="flex flex-col items-center justify-center h-full opacity-50 border border-dashed border-gray-300 dark:border-gray-700 rounded"> + <FileText size={32} className="mb-2" /> + <p className="text-xs text-center">No files uploaded yet.</p> + </div> + ) : ( + <div className="flex-1 overflow-y-auto space-y-1"> + {files.map(f => ( + <div + key={f.id} + className={`flex items-center justify-between px-2 py-1 rounded border ${isDark ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`} + > + <div className="flex flex-col"> + <span className="text-sm font-medium">{f.name}</span> + <span className={`text-[11px] ${isDark ? 'text-gray-500' : 'text-gray-500'}`}> + {formatSize(f.size)} • {new Date(f.created_at * 1000).toLocaleString()} + </span> + </div> + <div className="flex items-center gap-2"> + <button + onClick={() => handleDownloadFile(f)} + className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`} + title="Download" + > + <Download size={14} /> + </button> + <button + onClick={async () => { if (confirm('Delete this file?')) { await deleteFile(f.id); } }} + className={`p-1 rounded ${isDark ? 'hover:bg-red-900 text-red-300' : 'hover:bg-red-50 text-red-600'}`} + title="Delete" + > + <Trash2 size={14} /> + </button> + </div> + </div> + ))} + </div> + )} </div> )} {activeTab === 'archive' && ( diff --git a/frontend/src/store/flowStore.ts b/frontend/src/store/flowStore.ts index 56ade75..a049a8a 100644 --- a/frontend/src/store/flowStore.ts +++ b/frontend/src/store/flowStore.ts @@ -121,11 +121,22 @@ export interface ArchivedNode { mergeStrategy?: 'raw' | 'smart'; } +export interface FileMeta { + id: string; + name: string; + size: number; + mime: string; + created_at: number; + provider?: string; + provider_file_id?: string; +} + interface FlowState { nodes: LLMNode[]; edges: Edge[]; selectedNodeId: string | null; archivedNodes: ArchivedNode[]; // Stored node templates + files: FileMeta[]; theme: 'light' | 'dark'; projectTree: FSItem[]; currentBlueprintPath?: string; @@ -188,6 +199,10 @@ interface FlowState { clearBlueprint: () => void; loadArchivedNodes: () => Promise<void>; saveArchivedNodes: () => Promise<void>; + refreshFiles: () => Promise<void>; + uploadFile: (file: File) => Promise<FileMeta>; + deleteFile: (fileId: string) => Promise<void>; + setFiles: (files: FileMeta[]) => void; // Merge trace functions createMergedTrace: ( @@ -247,6 +262,7 @@ const useFlowStore = create<FlowState>((set, get) => { edges: [], selectedNodeId: null, archivedNodes: [], + files: [], theme: 'light' as const, projectTree: [], currentBlueprintPath: undefined, @@ -268,6 +284,9 @@ const useFlowStore = create<FlowState>((set, get) => { set({ lastViewport: viewport }); }, + setFiles: (files: FileMeta[]) => { + set({ files }); + }, findNonOverlappingPosition: (baseX: number, baseY: number) => { const { nodes } = get(); // Estimate larger dimensions to be safe, considering dynamic handles @@ -1529,6 +1548,42 @@ const useFlowStore = create<FlowState>((set, get) => { }); }, + // Files management + refreshFiles: async () => { + const res = await jsonFetch<{ files: FileMeta[] }>( + `${API_BASE}/api/files?user=${encodeURIComponent(DEFAULT_USER)}` + ); + set({ files: res.files || [] }); + }, + + uploadFile: async (file: File) => { + const form = new FormData(); + form.append('file', file); + 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; + }, + + deleteFile: async (fileId: string) => { + const res = await fetch(`${API_BASE}/api/files/delete?user=${encodeURIComponent(DEFAULT_USER)}&file_id=${encodeURIComponent(fileId)}`, { + method: 'POST', + }); + if (!res.ok) { + throw new Error(await res.text()); + } + await get().refreshFiles(); + }, + // -------------------------------------------------------- // Compute merged messages based on strategy |
