diff --git a/surfsense_web/components/sources/FolderWatchDialog.tsx b/surfsense_web/components/sources/FolderWatchDialog.tsx
new file mode 100644
index 000000000..1c66ea6b9
--- /dev/null
+++ b/surfsense_web/components/sources/FolderWatchDialog.tsx
@@ -0,0 +1,191 @@
+"use client";
+
+import { X } from "lucide-react";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Spinner } from "@/components/ui/spinner";
+import { Switch } from "@/components/ui/switch";
+import { documentsApiService } from "@/lib/apis/documents-api.service";
+import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
+
+export interface SelectedFolder {
+ path: string;
+ name: string;
+}
+
+interface FolderWatchDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ searchSpaceId: number;
+ onSuccess?: () => void;
+ initialFolder?: SelectedFolder | null;
+}
+
+const DEFAULT_EXCLUDE_PATTERNS = [
+ ".git",
+ "node_modules",
+ "__pycache__",
+ ".DS_Store",
+ ".obsidian",
+ ".trash",
+];
+
+export function FolderWatchDialog({
+ open,
+ onOpenChange,
+ searchSpaceId,
+ onSuccess,
+ initialFolder,
+}: FolderWatchDialogProps) {
+ const [selectedFolder, setSelectedFolder] = useState
(null);
+ const [shouldSummarize, setShouldSummarize] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (open && initialFolder) {
+ setSelectedFolder(initialFolder);
+ }
+ }, [open, initialFolder]);
+
+ const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []);
+
+ const handleSelectFolder = 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;
+ setSelectedFolder({ path: folderPath, name: folderName });
+ }, []);
+
+ const handleSubmit = useCallback(async () => {
+ if (!selectedFolder) return;
+ const api = window.electronAPI;
+ if (!api) return;
+
+ setSubmitting(true);
+ try {
+ const result = await documentsApiService.folderIndex(searchSpaceId, {
+ folder_path: selectedFolder.path,
+ folder_name: selectedFolder.name,
+ search_space_id: searchSpaceId,
+ enable_summary: shouldSummarize,
+ file_extensions: supportedExtensions,
+ });
+
+ const rootFolderId = (result as { root_folder_id?: number })?.root_folder_id ?? null;
+
+ await api.addWatchedFolder({
+ path: selectedFolder.path,
+ name: selectedFolder.name,
+ excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
+ fileExtensions: supportedExtensions,
+ rootFolderId,
+ searchSpaceId,
+ active: true,
+ });
+
+ toast.success(`Watching folder: ${selectedFolder.name}`);
+ setSelectedFolder(null);
+ setShouldSummarize(false);
+ onOpenChange(false);
+ onSuccess?.();
+ } catch (err) {
+ toast.error((err as Error)?.message || "Failed to watch folder");
+ } finally {
+ setSubmitting(false);
+ }
+ }, [
+ selectedFolder,
+ searchSpaceId,
+ shouldSummarize,
+ supportedExtensions,
+ onOpenChange,
+ onSuccess,
+ ]);
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ if (!nextOpen && !submitting) {
+ setSelectedFolder(null);
+ setShouldSummarize(false);
+ }
+ onOpenChange(nextOpen);
+ },
+ [onOpenChange, submitting]
+ );
+
+ return (
+
+ );
+}
diff --git a/surfsense_web/components/ui/tooltip.tsx b/surfsense_web/components/ui/tooltip.tsx
index fb6dd17e7..bcf1c72f8 100644
--- a/surfsense_web/components/ui/tooltip.tsx
+++ b/surfsense_web/components/ui/tooltip.tsx
@@ -72,7 +72,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
- "bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-balance pointer-events-none select-none",
+ "bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none",
className
)}
{...props}
diff --git a/surfsense_web/contracts/enums/vision-providers.ts b/surfsense_web/contracts/enums/vision-providers.ts
index 08be93b74..477fd5c53 100644
--- a/surfsense_web/contracts/enums/vision-providers.ts
+++ b/surfsense_web/contracts/enums/vision-providers.ts
@@ -107,22 +107,62 @@ export const VISION_MODELS: LLMModel[] = [
{ value: "gpt-4o", label: "GPT-4o", provider: "OPENAI", contextWindow: "128K" },
{ value: "gpt-4o-mini", label: "GPT-4o Mini", provider: "OPENAI", contextWindow: "128K" },
{ value: "gpt-4-turbo", label: "GPT-4 Turbo", provider: "OPENAI", contextWindow: "128K" },
- { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", provider: "ANTHROPIC", contextWindow: "200K" },
- { value: "claude-3-7-sonnet-20250219", label: "Claude 3.7 Sonnet", provider: "ANTHROPIC", contextWindow: "200K" },
- { value: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet", provider: "ANTHROPIC", contextWindow: "200K" },
- { value: "claude-3-opus-20240229", label: "Claude 3 Opus", provider: "ANTHROPIC", contextWindow: "200K" },
- { value: "claude-3-haiku-20240307", label: "Claude 3 Haiku", provider: "ANTHROPIC", contextWindow: "200K" },
+ {
+ value: "claude-sonnet-4-20250514",
+ label: "Claude Sonnet 4",
+ provider: "ANTHROPIC",
+ contextWindow: "200K",
+ },
+ {
+ value: "claude-3-7-sonnet-20250219",
+ label: "Claude 3.7 Sonnet",
+ provider: "ANTHROPIC",
+ contextWindow: "200K",
+ },
+ {
+ value: "claude-3-5-sonnet-20241022",
+ label: "Claude 3.5 Sonnet",
+ provider: "ANTHROPIC",
+ contextWindow: "200K",
+ },
+ {
+ value: "claude-3-opus-20240229",
+ label: "Claude 3 Opus",
+ provider: "ANTHROPIC",
+ contextWindow: "200K",
+ },
+ {
+ value: "claude-3-haiku-20240307",
+ label: "Claude 3 Haiku",
+ provider: "ANTHROPIC",
+ contextWindow: "200K",
+ },
{ value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "GOOGLE", contextWindow: "1M" },
{ value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "GOOGLE", contextWindow: "1M" },
{ value: "gemini-2.0-flash", label: "Gemini 2.0 Flash", provider: "GOOGLE", contextWindow: "1M" },
{ value: "gemini-1.5-pro", label: "Gemini 1.5 Pro", provider: "GOOGLE", contextWindow: "1M" },
{ value: "gemini-1.5-flash", label: "Gemini 1.5 Flash", provider: "GOOGLE", contextWindow: "1M" },
- { value: "pixtral-large-latest", label: "Pixtral Large", provider: "MISTRAL", contextWindow: "128K" },
+ {
+ value: "pixtral-large-latest",
+ label: "Pixtral Large",
+ provider: "MISTRAL",
+ contextWindow: "128K",
+ },
{ value: "pixtral-12b-2409", label: "Pixtral 12B", provider: "MISTRAL", contextWindow: "128K" },
{ value: "grok-2-vision-1212", label: "Grok 2 Vision", provider: "XAI", contextWindow: "32K" },
{ value: "llava", label: "LLaVA", provider: "OLLAMA" },
{ value: "bakllava", label: "BakLLaVA", provider: "OLLAMA" },
{ value: "llava-llama3", label: "LLaVA Llama 3", provider: "OLLAMA" },
- { value: "llama-4-scout-17b-16e-instruct", label: "Llama 4 Scout 17B", provider: "GROQ", contextWindow: "128K" },
- { value: "meta-llama/Llama-4-Scout-17B-16E-Instruct", label: "Llama 4 Scout 17B", provider: "TOGETHER_AI", contextWindow: "128K" },
+ {
+ value: "llama-4-scout-17b-16e-instruct",
+ label: "Llama 4 Scout 17B",
+ provider: "GROQ",
+ contextWindow: "128K",
+ },
+ {
+ value: "meta-llama/Llama-4-Scout-17B-16E-Instruct",
+ label: "Llama 4 Scout 17B",
+ provider: "TOGETHER_AI",
+ contextWindow: "128K",
+ },
];
diff --git a/surfsense_web/lib/env-config.ts b/surfsense_web/lib/env-config.ts
index e36aff10a..80db395c6 100644
--- a/surfsense_web/lib/env-config.ts
+++ b/surfsense_web/lib/env-config.ts
@@ -19,7 +19,7 @@ export const AUTH_TYPE = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "G
// Placeholder: __NEXT_PUBLIC_FASTAPI_BACKEND_URL__
export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
-// ETL Service: "DOCLING" or "UNSTRUCTURED"
+// ETL Service: "DOCLING", "UNSTRUCTURED", or "LLAMACLOUD"
// Placeholder: __NEXT_PUBLIC_ETL_SERVICE__
export const ETL_SERVICE = process.env.NEXT_PUBLIC_ETL_SERVICE || "DOCLING";
diff --git a/surfsense_web/lib/supported-extensions.ts b/surfsense_web/lib/supported-extensions.ts
new file mode 100644
index 000000000..f615b3d46
--- /dev/null
+++ b/surfsense_web/lib/supported-extensions.ts
@@ -0,0 +1,92 @@
+const audioFileTypes: Record = {
+ "audio/mpeg": [".mp3", ".mpeg", ".mpga"],
+ "audio/mp4": [".mp4", ".m4a"],
+ "audio/wav": [".wav"],
+ "audio/webm": [".webm"],
+ "text/markdown": [".md", ".markdown"],
+ "text/plain": [".txt"],
+};
+
+const commonTypes: Record = {
+ "application/pdf": [".pdf"],
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
+ "text/html": [".html", ".htm"],
+ "text/csv": [".csv"],
+ "text/tab-separated-values": [".tsv"],
+ "image/jpeg": [".jpg", ".jpeg"],
+ "image/png": [".png"],
+ "image/bmp": [".bmp"],
+ "image/webp": [".webp"],
+ "image/tiff": [".tiff"],
+};
+
+export const FILE_TYPE_CONFIG: Record> = {
+ LLAMACLOUD: {
+ ...commonTypes,
+ "application/msword": [".doc"],
+ "application/vnd.ms-word.document.macroEnabled.12": [".docm"],
+ "application/msword-template": [".dot"],
+ "application/vnd.ms-word.template.macroEnabled.12": [".dotm"],
+ "application/vnd.ms-powerpoint": [".ppt"],
+ "application/vnd.ms-powerpoint.template.macroEnabled.12": [".pptm"],
+ "application/vnd.ms-powerpoint.template": [".pot"],
+ "application/vnd.openxmlformats-officedocument.presentationml.template": [".potx"],
+ "application/vnd.ms-excel": [".xls"],
+ "application/vnd.ms-excel.sheet.macroEnabled.12": [".xlsm"],
+ "application/vnd.ms-excel.sheet.binary.macroEnabled.12": [".xlsb"],
+ "application/vnd.ms-excel.workspace": [".xlw"],
+ "application/rtf": [".rtf"],
+ "application/xml": [".xml"],
+ "application/epub+zip": [".epub"],
+ "image/gif": [".gif"],
+ "image/svg+xml": [".svg"],
+ ...audioFileTypes,
+ },
+ DOCLING: {
+ ...commonTypes,
+ "text/asciidoc": [".adoc", ".asciidoc"],
+ "text/html": [".html", ".htm", ".xhtml"],
+ "image/tiff": [".tiff", ".tif"],
+ ...audioFileTypes,
+ },
+ AZURE_DI: {
+ ...commonTypes,
+ "image/heic": [".heic"],
+ ...audioFileTypes,
+ },
+ default: {
+ ...commonTypes,
+ "application/msword": [".doc"],
+ "message/rfc822": [".eml"],
+ "application/epub+zip": [".epub"],
+ "image/heic": [".heic"],
+ "application/vnd.ms-outlook": [".msg"],
+ "application/vnd.oasis.opendocument.text": [".odt"],
+ "text/x-org": [".org"],
+ "application/pkcs7-signature": [".p7s"],
+ "application/vnd.ms-powerpoint": [".ppt"],
+ "text/x-rst": [".rst"],
+ "application/rtf": [".rtf"],
+ "application/vnd.ms-excel": [".xls"],
+ "application/xml": [".xml"],
+ ...audioFileTypes,
+ },
+};
+
+export function getAcceptedFileTypes(): Record {
+ const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE;
+ return FILE_TYPE_CONFIG[etlService || "default"] || FILE_TYPE_CONFIG.default;
+}
+
+export function getSupportedExtensions(acceptedFileTypes?: Record): string[] {
+ const types = acceptedFileTypes ?? getAcceptedFileTypes();
+ return Array.from(new Set(Object.values(types).flat())).sort();
+}
+
+export function getSupportedExtensionsSet(
+ acceptedFileTypes?: Record
+): Set {
+ return new Set(getSupportedExtensions(acceptedFileTypes).map((ext) => ext.toLowerCase()));
+}
diff --git a/surfsense_web/public/connectors/linear.svg b/surfsense_web/public/connectors/linear.svg
index e2484d708..4c80b1f14 100644
--- a/surfsense_web/public/connectors/linear.svg
+++ b/surfsense_web/public/connectors/linear.svg
@@ -1,4 +1,4 @@
-