From b46c5532b3fb02c3fd7277021d128e4f2f8a3180 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 3 Apr 2026 00:28:24 +0530 Subject: [PATCH] feat: add unified file and folder browsing functionality with IPC channel integration --- surfsense_desktop/src/ipc/channels.ts | 2 + surfsense_desktop/src/ipc/handlers.ts | 8 + .../src/modules/folder-watcher.ts | 68 +++++ surfsense_desktop/src/preload.ts | 4 + .../(manage)/components/DocumentsFilters.tsx | 23 +- .../layout/ui/sidebar/DocumentsSidebar.tsx | 51 +--- .../components/sources/DocumentUploadTab.tsx | 284 ++++++++++++++---- surfsense_web/types/window.d.ts | 15 + 8 files changed, 335 insertions(+), 120 deletions(-) diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 66788d90e..19c26607d 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -17,4 +17,6 @@ export const IPC_CHANNELS = { FOLDER_SYNC_PAUSE: 'folder-sync:pause', FOLDER_SYNC_RESUME: 'folder-sync:resume', FOLDER_SYNC_RENDERER_READY: 'folder-sync:renderer-ready', + BROWSE_FILE_OR_FOLDER: 'browse:file-or-folder', + READ_LOCAL_FILES: 'browse:read-local-files', } as const; diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts index 19051e871..246f0f6ac 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -9,6 +9,8 @@ import { pauseWatcher, resumeWatcher, markRendererReady, + browseFileOrFolder, + readLocalFiles, } from '../modules/folder-watcher'; export function registerIpcHandlers(): void { @@ -49,4 +51,10 @@ export function registerIpcHandlers(): void { ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_RENDERER_READY, () => { markRendererReady(); }); + + ipcMain.handle(IPC_CHANNELS.BROWSE_FILE_OR_FOLDER, () => browseFileOrFolder()); + + ipcMain.handle(IPC_CHANNELS.READ_LOCAL_FILES, (_event, paths: string[]) => + readLocalFiles(paths) + ); } diff --git a/surfsense_desktop/src/modules/folder-watcher.ts b/surfsense_desktop/src/modules/folder-watcher.ts index 81a835c22..1324858a0 100644 --- a/surfsense_desktop/src/modules/folder-watcher.ts +++ b/surfsense_desktop/src/modules/folder-watcher.ts @@ -391,3 +391,71 @@ export async function unregisterFolderWatcher(): Promise { } watchers.clear(); } + +export interface BrowseResult { + type: 'files' | 'folder'; + paths: string[]; +} + +export async function browseFileOrFolder(): Promise { + const result = await dialog.showOpenDialog({ + properties: ['openFile', 'openDirectory', 'multiSelections'], + title: 'Select files or a folder', + }); + if (result.canceled || result.filePaths.length === 0) return null; + + const stat = fs.statSync(result.filePaths[0]); + if (stat.isDirectory()) { + return { type: 'folder', paths: [result.filePaths[0]] }; + } + return { type: 'files', paths: result.filePaths }; +} + +const MIME_MAP: Record = { + '.pdf': 'application/pdf', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.html': 'text/html', '.htm': 'text/html', + '.csv': 'text/csv', + '.txt': 'text/plain', + '.md': 'text/markdown', '.markdown': 'text/markdown', + '.mp3': 'audio/mpeg', '.mpeg': 'audio/mpeg', '.mpga': 'audio/mpeg', + '.mp4': 'audio/mp4', '.m4a': 'audio/mp4', + '.wav': 'audio/wav', + '.webm': 'audio/webm', + '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.bmp': 'image/bmp', + '.webp': 'image/webp', + '.tiff': 'image/tiff', + '.doc': 'application/msword', + '.rtf': 'application/rtf', + '.xml': 'application/xml', + '.epub': 'application/epub+zip', + '.xls': 'application/vnd.ms-excel', + '.ppt': 'application/vnd.ms-powerpoint', + '.eml': 'message/rfc822', + '.odt': 'application/vnd.oasis.opendocument.text', + '.msg': 'application/vnd.ms-outlook', +}; + +export interface LocalFileData { + name: string; + data: ArrayBuffer; + mimeType: string; + size: number; +} + +export function readLocalFiles(filePaths: string[]): LocalFileData[] { + return filePaths.map((p) => { + const buf = fs.readFileSync(p); + const ext = path.extname(p).toLowerCase(); + return { + name: path.basename(p), + data: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength), + mimeType: MIME_MAP[ext] || 'application/octet-stream', + size: buf.byteLength, + }; + }); +} diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 7c190db10..08ca87f8f 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -45,4 +45,8 @@ contextBridge.exposeInMainWorld('electronAPI', { pauseWatcher: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_PAUSE), resumeWatcher: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_RESUME), signalRendererReady: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_RENDERER_READY), + + // Unified browse (files + folders) + browseFileOrFolder: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILE_OR_FOLDER), + readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths), }); diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx index fcd3a39da..150c119de 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx @@ -1,6 +1,6 @@ "use client"; -import { Eye, FolderPlus, ListFilter, Search, Upload, X } from "lucide-react"; +import { FolderPlus, ListFilter, Search, Upload, X } from "lucide-react"; import { useTranslations } from "next-intl"; import React, { useCallback, useMemo, useRef, useState } from "react"; import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; @@ -19,7 +19,6 @@ export function DocumentsFilters({ onToggleType, activeTypes, onCreateFolder, - onWatchFolder, }: { typeCounts: Partial>; onSearch: (v: string) => void; @@ -27,7 +26,6 @@ export function DocumentsFilters({ onToggleType: (type: DocumentTypeEnum, checked: boolean) => void; activeTypes: DocumentTypeEnum[]; onCreateFolder?: () => void; - onWatchFolder?: () => void; }) { const t = useTranslations("documents"); const id = React.useId(); @@ -216,24 +214,7 @@ export function DocumentsFilters({ )} - {/* Watch Folder Button (desktop only) */} - {onWatchFolder && ( - - - - - Watch folder - - )} - - {/* Upload Button */} + {/* Upload Button */} + ) : ( + )} + + )} + + + + + {selectedFolder && ( + + +
+
+ +
+ + {selectedFolder.name} + + + {selectedFolder.path} +
- )} +
+
+
+ +
+ + +
+ + + +
+ )} {files.length > 0 && ( diff --git a/surfsense_web/types/window.d.ts b/surfsense_web/types/window.d.ts index b399664d6..826a575c7 100644 --- a/surfsense_web/types/window.d.ts +++ b/surfsense_web/types/window.d.ts @@ -26,6 +26,18 @@ interface FolderSyncWatcherReadyEvent { folderPath: string; } +interface BrowseResult { + type: "files" | "folder"; + paths: string[]; +} + +interface LocalFileData { + name: string; + data: ArrayBuffer; + mimeType: string; + size: number; +} + interface ElectronAPI { versions: { electron: string; @@ -51,6 +63,9 @@ interface ElectronAPI { pauseWatcher: () => Promise; resumeWatcher: () => Promise; signalRendererReady: () => Promise; + // Unified browse + browseFileOrFolder: () => Promise; + readLocalFiles: (paths: string[]) => Promise; } declare global {