diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 2761960f7..2000964c7 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -19,6 +19,6 @@ export const IPC_CHANNELS = { FOLDER_SYNC_RENDERER_READY: 'folder-sync:renderer-ready', FOLDER_SYNC_GET_PENDING_EVENTS: 'folder-sync:get-pending-events', FOLDER_SYNC_ACK_EVENTS: 'folder-sync:ack-events', - BROWSE_FILE_OR_FOLDER: 'browse:file-or-folder', + BROWSE_FILES: 'browse:files', 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 7194aaaff..c4251b30b 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -11,7 +11,7 @@ import { pauseWatcher, resumeWatcher, markRendererReady, - browseFileOrFolder, + browseFiles, readLocalFiles, } from '../modules/folder-watcher'; @@ -62,7 +62,7 @@ export function registerIpcHandlers(): void { acknowledgeFileEvents(eventIds) ); - ipcMain.handle(IPC_CHANNELS.BROWSE_FILE_OR_FOLDER, () => browseFileOrFolder()); + ipcMain.handle(IPC_CHANNELS.BROWSE_FILES, () => browseFiles()); 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 9cbdd9775..969dabe97 100644 --- a/surfsense_desktop/src/modules/folder-watcher.ts +++ b/surfsense_desktop/src/modules/folder-watcher.ts @@ -475,23 +475,13 @@ export async function unregisterFolderWatcher(): Promise { watchers.clear(); } -export interface BrowseResult { - type: 'files' | 'folder'; - paths: string[]; -} - -export async function browseFileOrFolder(): Promise { +export async function browseFiles(): Promise { const result = await dialog.showOpenDialog({ - properties: ['openFile', 'openDirectory', 'multiSelections'], - title: 'Select files or a folder', + properties: ['openFile', 'multiSelections'], + title: 'Select files', }); 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 }; + return result.filePaths; } const MIME_MAP: Record = { diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 6a2610dc8..6fbfd354a 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -48,7 +48,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getPendingFileEvents: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_GET_PENDING_EVENTS), acknowledgeFileEvents: (eventIds: string[]) => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_ACK_EVENTS, eventIds), - // Unified browse (files + folders) - browseFileOrFolder: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILE_OR_FOLDER), + // Browse files via native dialog + browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES), readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths), }); diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index ed3a78786..f8b774d26 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -277,8 +277,6 @@ export function DocumentsSidebar({ [createFolderParentId, searchSpaceId, setExpandedFolderMap] ); - const isElectron = typeof window !== "undefined" && !!window.electronAPI; - const handleRescanFolder = useCallback( async (folder: FolderDisplay) => { const api = window.electronAPI; diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 3fdf576b5..d5ac2770a 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -1,7 +1,7 @@ "use client"; import { useAtom } from "jotai"; -import { CheckCircle2, FileType, FolderOpen, Info, Upload, X } from "lucide-react"; +import { CheckCircle2, ChevronDown, File as FileIcon, FileType, FolderOpen, Info, Upload, X } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useRef, useState } from "react"; @@ -19,6 +19,12 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; @@ -146,7 +152,7 @@ export function DocumentUploadTab({ const [selectedFolder, setSelectedFolder] = useState(null); const [watchFolder, setWatchFolder] = useState(true); const [folderSubmitting, setFolderSubmitting] = useState(false); - const isElectron = typeof window !== "undefined" && !!window.electronAPI?.browseFileOrFolder; + const isElectron = typeof window !== "undefined" && !!window.electronAPI?.browseFiles; const acceptedFileTypes = useMemo(() => { const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE; @@ -193,7 +199,7 @@ export function DocumentUploadTab({ onDrop, accept: acceptedFileTypes, maxSize: 50 * 1024 * 1024, // 50MB per file - noClick: !isElectron, + noClick: isElectron, disabled: files.length >= MAX_FILES, }); @@ -201,52 +207,51 @@ export function DocumentUploadTab({ e.stopPropagation(); }, []); - const handleBrowse = useCallback(async (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - + const handleBrowseFiles = useCallback(async () => { const api = window.electronAPI; - if (!api?.browseFileOrFolder) { - fileInputRef.current?.click(); - return; - } + if (!api?.browseFiles) return; - const result = await api.browseFileOrFolder(); - if (!result) return; + const paths = await api.browseFiles(); + if (!paths || paths.length === 0) return; - if (result.type === "folder") { - const folderPath = result.paths[0]; - const folderName = folderPath.split("/").pop() || folderPath.split("\\").pop() || folderPath; - setFiles([]); - setSelectedFolder({ path: folderPath, name: folderName }); - setWatchFolder(true); - } else { - setSelectedFolder(null); - const fileDataList = await api.readLocalFiles(result.paths); - const newFiles: FileWithId[] = fileDataList.map((fd) => ({ - id: crypto.randomUUID?.() ?? `file-${Date.now()}-${Math.random().toString(36)}`, - file: new File([fd.data], fd.name, { type: fd.mimeType }), - })); - setFiles((prev) => { - const merged = [...prev, ...newFiles]; - if (merged.length > MAX_FILES) { - toast.error(t("max_files_exceeded"), { - description: t("max_files_exceeded_desc", { max: MAX_FILES }), - }); - return prev; - } - const totalSize = merged.reduce((sum, e) => sum + e.file.size, 0); - if (totalSize > MAX_TOTAL_SIZE_BYTES) { - toast.error(t("max_size_exceeded"), { - description: t("max_size_exceeded_desc", { max: MAX_TOTAL_SIZE_MB }), - }); - return prev; - } - return merged; - }); - } + setSelectedFolder(null); + const fileDataList = await api.readLocalFiles(paths); + const newFiles: FileWithId[] = fileDataList.map((fd) => ({ + id: crypto.randomUUID?.() ?? `file-${Date.now()}-${Math.random().toString(36)}`, + file: new File([fd.data], fd.name, { type: fd.mimeType }), + })); + setFiles((prev) => { + const merged = [...prev, ...newFiles]; + if (merged.length > MAX_FILES) { + toast.error(t("max_files_exceeded"), { + description: t("max_files_exceeded_desc", { max: MAX_FILES }), + }); + return prev; + } + const totalSize = merged.reduce((sum, e) => sum + e.file.size, 0); + if (totalSize > MAX_TOTAL_SIZE_BYTES) { + toast.error(t("max_size_exceeded"), { + description: t("max_size_exceeded_desc", { max: MAX_TOTAL_SIZE_MB }), + }); + return prev; + } + return merged; + }); }, [t]); + const handleBrowseFolder = useCallback(async () => { + const api = window.electronAPI; + if (!api?.selectFolder) return; + + const folderPath = await api.selectFolder(); + if (!folderPath) return; + + const folderName = folderPath.split("/").pop() || folderPath.split("\\").pop() || folderPath; + setFiles([]); + setSelectedFolder({ path: folderPath, name: folderName }); + setWatchFolder(true); + }, []); + const formatFileSize = (bytes: number) => { if (bytes === 0) return "0 Bytes"; const k = 1024; @@ -280,10 +285,11 @@ export function DocumentUploadTab({ setFolderSubmitting(true); try { - const result = await documentsApiService.folderIndex(Number(searchSpaceId), { + const numericSpaceId = Number(searchSpaceId); + const result = await documentsApiService.folderIndex(numericSpaceId, { folder_path: selectedFolder.path, folder_name: selectedFolder.name, - search_space_id: searchSpaceId, + search_space_id: numericSpaceId, enable_summary: shouldSummarize, }); @@ -409,33 +415,43 @@ export function DocumentUploadTab({ )} )} - {!isFileCountLimitReached && ( -
- {isElectron ? ( - - ) : ( - - )} -
- )} + {!isFileCountLimitReached && ( +
+ {isElectron ? ( + + e.stopPropagation()}> + + + e.stopPropagation()}> + + + Files + + + + Folder + + + + ) : ( + + )} +
+ )} diff --git a/surfsense_web/types/window.d.ts b/surfsense_web/types/window.d.ts index 719373e02..0842ed655 100644 --- a/surfsense_web/types/window.d.ts +++ b/surfsense_web/types/window.d.ts @@ -27,11 +27,6 @@ interface FolderSyncWatcherReadyEvent { folderPath: string; } -interface BrowseResult { - type: "files" | "folder"; - paths: string[]; -} - interface LocalFileData { name: string; data: ArrayBuffer; @@ -66,8 +61,8 @@ interface ElectronAPI { signalRendererReady: () => Promise; getPendingFileEvents: () => Promise; acknowledgeFileEvents: (eventIds: string[]) => Promise<{ acknowledged: number }>; - // Unified browse - browseFileOrFolder: () => Promise; + // Browse files/folders via native dialogs + browseFiles: () => Promise; readLocalFiles: (paths: string[]) => Promise; }