diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 647c93282..1335c8bcb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -20,6 +20,7 @@ import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup"; export function DashboardClientLayout({ children, @@ -240,32 +241,34 @@ export function DashboardClientLayout({ } return ( - - {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} - - -
-
-
-
- -
- - + + + {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} + + +
+
+
+
+ +
+ + +
+
+
+
-
- -
-
-
-
{children}
-
-
-
+ +
{children}
+ + + + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx deleted file mode 100644 index 614958018..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx +++ /dev/null @@ -1,857 +0,0 @@ -"use client"; - -import { format } from "date-fns"; -import { useAtomValue } from "jotai"; -import { - Calendar as CalendarIcon, - Clock, - Edit, - Folder, - HardDrive, - Info, - Loader2, - Plus, - RefreshCw, - Trash2, -} from "lucide-react"; -import { motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { - deleteConnectorMutationAtom, - indexConnectorMutationAtom, - updateConnectorMutationAtom, -} from "@/atoms/connectors/connector-mutation.atoms"; -import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { cn } from "@/lib/utils"; -import { GoogleDriveFolderTree } from "@/components/connectors/google-drive-folder-tree"; - -export default function ConnectorsPage() { - const t = useTranslations("connectors"); - const tCommon = useTranslations("common"); - - // Helper function to format date with time - const formatDateTime = (dateString: string | null): string => { - if (!dateString) return t("never"); - - const date = new Date(dateString); - return new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }).format(date); - }; - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const today = new Date(); - - const { data: connectors = [], isLoading, error } = useAtomValue(connectorsAtom); - - const { mutateAsync: deleteConnector } = useAtomValue(deleteConnectorMutationAtom); - const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom); - const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom); - - const [connectorToDelete, setConnectorToDelete] = useState(null); - const [indexingConnectorId, setIndexingConnectorId] = useState(null); - const [datePickerOpen, setDatePickerOpen] = useState(false); - const [selectedConnectorForIndexing, setSelectedConnectorForIndexing] = useState( - null - ); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - - // Periodic indexing state - const [periodicDialogOpen, setPeriodicDialogOpen] = useState(false); - const [selectedConnectorForPeriodic, setSelectedConnectorForPeriodic] = useState( - null - ); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const [customFrequency, setCustomFrequency] = useState(""); - const [isSavingPeriodic, setIsSavingPeriodic] = useState(false); - - // Google Drive folder and file selection state - const [driveFolderDialogOpen, setDriveFolderDialogOpen] = useState(false); - const [selectedFolders, setSelectedFolders] = useState>([]); - const [selectedFiles, setSelectedFiles] = useState>([]); - - useEffect(() => { - if (error) { - toast.error(t("failed_load")); - console.error("Error fetching connectors:", error); - } - }, [error, t]); - - // Handle connector deletion - const handleDeleteConnector = async () => { - if (connectorToDelete === null) return; - - try { - await deleteConnector({ id: connectorToDelete }); - } catch (error) { - console.error("Error deleting connector:", error); - } finally { - setConnectorToDelete(null); - } - }; - - // Handle opening date picker for indexing - const handleOpenDatePicker = (connectorId: number) => { - // Check if this is a Google Drive connector - const connector = connectors.find((c) => c.id === connectorId); - if (connector?.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR) { - // Open folder selection dialog for Google Drive - handleOpenDriveFolderDialog(connectorId); - } else { - // Open date picker for other connectors - setSelectedConnectorForIndexing(connectorId); - setDatePickerOpen(true); - } - }; - - const handleOpenDriveFolderDialog = (connectorId: number) => { - setSelectedConnectorForIndexing(connectorId); - setDriveFolderDialogOpen(true); - }; - - // Handle Google Drive folder and file indexing - const handleIndexGoogleDrive = async () => { - if (selectedConnectorForIndexing === null || (selectedFolders.length === 0 && selectedFiles.length === 0)) { - toast.error("Please select at least one folder or file"); - return; - } - - setDriveFolderDialogOpen(false); - - try { - setIndexingConnectorId(selectedConnectorForIndexing); - - await indexConnector({ - connector_id: selectedConnectorForIndexing, - body: { - folders: selectedFolders, - files: selectedFiles, - }, - queryParams: { - search_space_id: searchSpaceId, - }, - }); - toast.success(t("indexing_started")); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : t("indexing_failed")); - } finally { - setIndexingConnectorId(null); - setSelectedConnectorForIndexing(null); - setSelectedFolders([]); - setSelectedFiles([]); - } - }; - - // Handle connector indexing with dates - const handleIndexConnector = async () => { - if (selectedConnectorForIndexing === null) return; - - setDatePickerOpen(false); - - try { - setIndexingConnectorId(selectedConnectorForIndexing); - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - - await indexConnector({ - connector_id: selectedConnectorForIndexing, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, - }); - toast.success(t("indexing_started")); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : t("indexing_failed")); - } finally { - setIndexingConnectorId(null); - setSelectedConnectorForIndexing(null); - setStartDate(undefined); - setEndDate(undefined); - } - }; - - // Handle indexing without date picker (for quick indexing) - const handleQuickIndexConnector = async (connectorId: number) => { - setIndexingConnectorId(connectorId); - try { - await indexConnector({ - connector_id: connectorId, - queryParams: { - search_space_id: searchSpaceId, - }, - }); - toast.success(t("indexing_started")); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : t("indexing_failed")); - } finally { - setIndexingConnectorId(null); - } - }; - - // Handle opening periodic indexing dialog - const handleOpenPeriodicDialog = (connectorId: number) => { - const connector = connectors.find((c) => c.id === connectorId); - if (!connector) return; - - setSelectedConnectorForPeriodic(connectorId); - setPeriodicEnabled(connector.periodic_indexing_enabled); - - if (connector.indexing_frequency_minutes) { - // Check if it's a preset value - const presetValues = ["15", "60", "360", "720", "1440", "10080"]; - if (presetValues.includes(connector.indexing_frequency_minutes.toString())) { - setFrequencyMinutes(connector.indexing_frequency_minutes.toString()); - setCustomFrequency(""); - } else { - setFrequencyMinutes("custom"); - setCustomFrequency(connector.indexing_frequency_minutes.toString()); - } - } else { - setFrequencyMinutes("1440"); - setCustomFrequency(""); - } - - setPeriodicDialogOpen(true); - }; - - // Handle saving periodic indexing configuration - const handleSavePeriodicIndexing = async () => { - if (selectedConnectorForPeriodic === null) return; - - const connector = connectors.find((c) => c.id === selectedConnectorForPeriodic); - if (!connector) return; - - setIsSavingPeriodic(true); - try { - // Determine the frequency value - let frequency: number | null = null; - if (periodicEnabled) { - if (frequencyMinutes === "custom") { - frequency = parseInt(customFrequency, 10); - if (isNaN(frequency) || frequency <= 0) { - toast.error("Please enter a valid frequency in minutes"); - setIsSavingPeriodic(false); - return; - } - } else { - frequency = parseInt(frequencyMinutes, 10); - } - } - - await updateConnector({ - id: selectedConnectorForPeriodic, - data: { - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: frequency, - }, - }); - - toast.success( - periodicEnabled - ? "Periodic indexing enabled successfully" - : "Periodic indexing disabled successfully" - ); - setPeriodicDialogOpen(false); - } catch (error) { - console.error("Error updating periodic indexing:", error); - toast.error(error instanceof Error ? error.message : "Failed to update periodic indexing"); - } finally { - setIsSavingPeriodic(false); - setSelectedConnectorForPeriodic(null); - } - }; - - // Format frequency for display - const formatFrequency = (minutes: number): string => { - if (minutes < 60) return `${minutes}m`; - if (minutes < 1440) return `${Math.floor(minutes / 60)}h`; - if (minutes < 10080) return `${Math.floor(minutes / 1440)}d`; - return `${Math.floor(minutes / 10080)}w`; - }; - - return ( -
- -
-

{t("title")}

-

{t("subtitle")}

-
- -
- - {isLoading ? ( -
-
-
-
-
-
- ) : connectors.length === 0 ? ( -
-

{t("no_connectors")}

-

{t("no_connectors_desc")}

- -
- ) : ( -
- - - - {t("name")} - {t("type")} - {t("last_indexed")} - {t("periodic")} - {t("actions")} - - - - {connectors.map((connector) => ( - - {connector.name} - {getConnectorIcon(connector.connector_type)} - - {connector.is_indexable - ? formatDateTime(connector.last_indexed_at) - : t("not_indexable")} - - - {connector.is_indexable ? ( - connector.periodic_indexing_enabled ? ( - - - -
- - - {connector.indexing_frequency_minutes - ? formatFrequency(connector.indexing_frequency_minutes) - : "Enabled"} - -
-
- -

- Runs every {connector.indexing_frequency_minutes} minutes - {connector.next_scheduled_at && ( - <> -
- Next: {formatDateTime(connector.next_scheduled_at)} - - )} -

-
-
-
- ) : ( - Disabled - ) - ) : ( - - - )} -
- -
- {connector.is_indexable && ( -
- - - - - - -

- {connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR - ? "Select folder to index" - : t("index_date_range")} -

-
-
-
- {/* Hide quick index button for Google Drive (requires folder selection) */} - {connector.connector_type !== EnumConnectorName.GOOGLE_DRIVE_CONNECTOR && ( - - - - - - -

{t("quick_index_auto")}

-
-
-
- )} -
- )} - {connector.is_indexable && ( - - - - - - -

Configure Periodic Indexing

-
-
-
- )} - - - - - - - - {t("delete_connector")} - - {t("delete_confirm")} - - - - setConnectorToDelete(null)}> - {tCommon("cancel")} - - - {tCommon("delete")} - - - - -
-
-
- ))} -
-
-
- )} - - {/* Date Picker Dialog */} - - - - {t("select_date_range")} - {t("select_date_range_desc")} - -
-
-
- - - - - - - - - -
-
- - - - - - - - - -
-
-
- - - -
-
- - - - -
-
- - {/* Google Drive Folder Selection Dialog */} - - - - Select Google Drive Folders & Files - - - - Select folders and/or individual files to index. For folders, only files directly in each folder will be - processed—subfolders must be selected separately. - - - -
-
- - {selectedConnectorForIndexing && ( - { - setSelectedFolders(folders); - }} - selectedFiles={selectedFiles} - onSelectFiles={(files) => { - setSelectedFiles(files); - }} - /> - )} -
- {(selectedFolders.length > 0 || selectedFiles.length > 0) && ( -
- {selectedFolders.length > 0 && ( -
-

- Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}: -

-
- {selectedFolders.map((folder) => ( -

- 📁 {folder.name} -

- ))} -
-
- )} - {selectedFiles.length > 0 && ( -
-

- Selected {selectedFiles.length} file{selectedFiles.length > 1 ? "s" : ""}: -

-
- {selectedFiles.map((file) => ( -

- 📄 {file.name} -

- ))} -
-
- )} -
- )} -
- - - - -
-
- - {/* Periodic Indexing Configuration Dialog */} - - - - Configure Periodic Indexing - - Set up automatic indexing at regular intervals for this connector. - - -
-
-
- -

- Automatically index this connector at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
- - {frequencyMinutes === "custom" && ( -
- - setCustomFrequency(e.target.value)} - /> -

- Enter the number of minutes between each indexing run -

-
- )} - -
-

Preview:

-

- {frequencyMinutes === "custom" && customFrequency - ? `Will run every ${customFrequency} minutes` - : frequencyMinutes === "15" - ? "Will run every 15 minutes" - : frequencyMinutes === "60" - ? "Will run every hour" - : frequencyMinutes === "360" - ? "Will run every 6 hours" - : frequencyMinutes === "720" - ? "Will run every 12 hours" - : frequencyMinutes === "1440" - ? "Will run daily (every 24 hours)" - : frequencyMinutes === "10080" - ? "Will run weekly (every 7 days)" - : "Select a frequency above"} -

-
-
- )} -
- - - - -
-
-
- ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx index 99d7a7b8d..e483dea12 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx @@ -3,8 +3,6 @@ import type React from "react"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -type IconComponent = React.ComponentType<{ size?: number; className?: string }>; - export function getDocumentTypeIcon(type: string): React.ReactNode { return getConnectorIcon(type); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index 8964ef3d6..94c0626e6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -2,9 +2,10 @@ import { ChevronDown, ChevronUp, FileX, Plus } from "lucide-react"; import { motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import React from "react"; +import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { DocumentViewer } from "@/components/document-viewer"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -69,9 +70,9 @@ export function DocumentsTableShell({ onSortChange: (key: SortKey) => void; }) { const t = useTranslations("documents"); - const router = useRouter(); const params = useParams(); const searchSpaceId = params.search_space_id; + const { openDialog } = useDocumentUploadDialog(); const sorted = React.useMemo( () => sortDocuments(documents, sortKey, sortDesc), @@ -137,19 +138,16 @@ export function DocumentsTableShell({
-
-

{t("no_documents")}

-

- Get started by uploading your first document. -

-
- +
+

{t("no_documents")}

+

+ Get started by uploading your first document. +

+
+ ) : ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx deleted file mode 100644 index d980c73e0..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { Upload } from "lucide-react"; -import { motion } from "motion/react"; -import { useParams } from "next/navigation"; -import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab"; - -export default function UploadDocumentsPage() { - const params = useParams(); - const search_space_id = params.search_space_id as string; - - return ( -
- - {/* Header */} -
-

- - Upload Documents -

-

- Upload documents to your search space for AI-powered search and chat -

-
- - {/* Document Upload */} - -
-
- ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index eb7bc23fc..239fdc5c1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -517,4 +517,4 @@ export default function EditorPage() { ); -} \ No newline at end of file +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index a44592ab2..1631f00b9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -30,28 +30,19 @@ export default function DashboardLayout({ { title: "Chat", url: `/dashboard/${search_space_id}/new-chat`, - icon: "SquareTerminal", + icon: "MessageCircle", items: [], }, { - title: "Sources", - url: "#", - icon: "Database", - items: [ - { - title: "Manage Documents", - url: `/dashboard/${search_space_id}/documents`, - }, - { - title: "Manage Connectors", - url: `/dashboard/${search_space_id}/connectors`, - }, - ], + title: "Documents", + url: `/dashboard/${search_space_id}/documents`, + icon: "SquareLibrary", + items: [], }, { title: "Logs", url: `/dashboard/${search_space_id}/logs`, - icon: "FileText", + icon: "Logs", items: [], }, ]; diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index 62fbe0dd4..0fd70800a 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -9,7 +9,10 @@ import { CheckIcon, CopyIcon, DownloadIcon, RefreshCwIcon } from "lucide-react"; import type { FC } from "react"; import { useContext } from "react"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; -import { ThinkingStepsContext, ThinkingStepsDisplay } from "@/components/assistant-ui/thinking-steps"; +import { + ThinkingStepsContext, + ThinkingStepsDisplay, +} from "@/components/assistant-ui/thinking-steps"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { BranchPicker } from "@/components/assistant-ui/branch-picker"; @@ -115,4 +118,3 @@ const AssistantActionBar: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/attachment.tsx b/surfsense_web/components/assistant-ui/attachment.tsx index c08736d7c..a117e745d 100644 --- a/surfsense_web/components/assistant-ui/attachment.tsx +++ b/surfsense_web/components/assistant-ui/attachment.tsx @@ -9,8 +9,8 @@ import { } from "@assistant-ui/react"; import { FileText, Loader2, Paperclip, PlusIcon, Upload, XIcon } from "lucide-react"; import Image from "next/image"; -import { useParams, useRouter } from "next/navigation"; import { type FC, type PropsWithChildren, useRef, useEffect, useState } from "react"; +import { useDocumentUploadDialog } from "./document-upload-popup"; import { useShallow } from "zustand/shallow"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; @@ -319,30 +319,33 @@ export const ComposerAttachments: FC = () => { }; export const ComposerAddAttachment: FC = () => { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; const chatAttachmentInputRef = useRef(null); + const { openDialog } = useDocumentUploadDialog(); const handleFileUpload = () => { - router.push(`/dashboard/${searchSpaceId}/documents/upload`); + openDialog(); }; const handleChatAttachment = () => { chatAttachmentInputRef.current?.click(); }; + // Prevent event bubbling when file input is clicked + const handleFileInputClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + return ( <> @@ -350,11 +353,11 @@ export const ComposerAddAttachment: FC = () => { - Add attachment(s) + Add attachment - File upload + Upload Files @@ -365,6 +368,7 @@ export const ComposerAddAttachment: FC = () => { multiple className="hidden" accept="image/*,application/pdf,.doc,.docx,.txt" + onClick={handleFileInputClick} /> diff --git a/surfsense_web/components/assistant-ui/branch-picker.tsx b/surfsense_web/components/assistant-ui/branch-picker.tsx index 1d9041309..ee4addd2a 100644 --- a/surfsense_web/components/assistant-ui/branch-picker.tsx +++ b/surfsense_web/components/assistant-ui/branch-picker.tsx @@ -30,4 +30,3 @@ export const BranchPicker: FC = ({ className, ); }; - diff --git a/surfsense_web/components/assistant-ui/composer-action.tsx b/surfsense_web/components/assistant-ui/composer-action.tsx index ba27f40c2..8d18ae2a9 100644 --- a/surfsense_web/components/assistant-ui/composer-action.tsx +++ b/surfsense_web/components/assistant-ui/composer-action.tsx @@ -1,7 +1,6 @@ import { AssistantIf, ComposerPrimitive, useAssistantState } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { AlertCircle, ArrowUpIcon, Loader2, Plus, Plug2, SquareIcon } from "lucide-react"; -import Link from "next/link"; import type { FC } from "react"; import { useCallback, useMemo, useRef, useState } from "react"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; @@ -38,11 +37,10 @@ const ConnectorIndicator: FC = () => { ? Object.entries(documentTypeCounts).filter(([_, count]) => count > 0) : []; - const nonIndexableConnectors = connectors.filter((connector) => !connector.is_indexable); - - const hasConnectors = nonIndexableConnectors.length > 0; + // Count only active connectors (matching what's shown in the Active tab) + const activeConnectorsCount = connectors.length; + const hasConnectors = activeConnectorsCount > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; - const totalSourceCount = nonIndexableConnectors.length + activeDocumentTypes.length; const handleMouseEnter = useCallback(() => { // Clear any pending close timeout @@ -76,7 +74,9 @@ const ConnectorIndicator: FC = () => { "text-muted-foreground" )} aria-label={ - hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector" + hasConnectors + ? `View ${activeConnectorsCount} active connectors` + : "Add your first connector" } onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} @@ -86,9 +86,9 @@ const ConnectorIndicator: FC = () => { ) : ( <> - {totalSourceCount > 0 && ( + {activeConnectorsCount > 0 && ( - {totalSourceCount > 99 ? "99+" : totalSourceCount} + {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} )} @@ -104,44 +104,64 @@ const ConnectorIndicator: FC = () => { > {hasSources ? (
-
-

Connected Sources

- - {totalSourceCount} - -
-
- {activeDocumentTypes.map(([docType, count]) => ( -
- {getConnectorIcon(docType, "size-3.5")} - {getDocumentTypeLabel(docType)} - - {count > 999 ? "999+" : count} - + {activeConnectorsCount > 0 && ( +
+

Active Connectors

+ + {activeConnectorsCount} + +
+ )} + {activeConnectorsCount > 0 && ( +
+ {connectors.map((connector) => ( +
+ {getConnectorIcon(connector.connector_type, "size-3.5")} + {connector.name} +
+ ))} +
+ )} + {activeDocumentTypes.length > 0 && ( + <> + {activeConnectorsCount > 0 && ( +
+

Documents

+
+ )} +
+ {activeDocumentTypes.map(([docType, count]) => ( +
+ {getConnectorIcon(docType, "size-3.5")} + + {getDocumentTypeLabel(docType)} + + + {count > 999 ? "999+" : count} + +
+ ))}
- ))} - {nonIndexableConnectors.map((connector) => ( -
- {getConnectorIcon(connector.connector_type, "size-3.5")} - {connector.name} -
- ))} -
+ + )}
- { + /* Connector popup should be opened via the connector indicator button */ + }} > Add more sources - +
) : ( @@ -150,13 +170,16 @@ const ConnectorIndicator: FC = () => {

Add documents or connect data sources to enhance search results.

- { + /* Connector popup should be opened via the connector indicator button */ + }} > Add Connector - +
)} @@ -268,4 +291,3 @@ export const ComposerAction: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/composer.tsx b/surfsense_web/components/assistant-ui/composer.tsx index 1973726da..8f8ee5e0b 100644 --- a/surfsense_web/components/assistant-ui/composer.tsx +++ b/surfsense_web/components/assistant-ui/composer.tsx @@ -8,10 +8,7 @@ import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, } from "@/atoms/chat/mentioned-documents.atom"; -import { - ComposerAddAttachment, - ComposerAttachments, -} from "@/components/assistant-ui/attachment"; +import { ComposerAddAttachment, ComposerAttachments } from "@/components/assistant-ui/attachment"; import { ComposerAction } from "@/components/assistant-ui/composer-action"; import { InlineMentionEditor, @@ -237,4 +234,3 @@ export const Composer: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 9b3d138b3..621861529 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -10,14 +10,8 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer import { useQuery, useQueryClient } from "@tanstack/react-query"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { connectorsApiService } from "@/lib/apis/connectors-api.service"; -import { - Dialog, - DialogContent, -} from "@/components/ui/dialog"; -import { - Tabs, - TabsContent, -} from "@/components/ui/tabs"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Tabs, TabsContent } from "@/components/ui/tabs"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { cn } from "@/lib/utils"; import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab"; @@ -35,19 +29,15 @@ export const ConnectorIndicator: FC = () => { const searchParams = useSearchParams(); const { data: documentTypeCounts, isLoading: documentTypesLoading } = useAtomValue(documentTypeCountsAtom); - + // Check if YouTube view is active const isYouTubeView = searchParams.get("view") === "youtube"; // Track active indexing tasks - const { summary: logsSummary } = useLogsSummary( - searchSpaceId ? Number(searchSpaceId) : 0, - 24, - { - enablePolling: true, - refetchInterval: 5000, - } - ); + const { summary: logsSummary } = useLogsSummary(searchSpaceId ? Number(searchSpaceId) : 0, 24, { + enablePolling: true, + refetchInterval: 5000, + }); // Use the custom hook for dialog state management const { @@ -91,6 +81,7 @@ export const ConnectorIndicator: FC = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleQuickIndexConnector, connectorConfig, setConnectorConfig, setIndexingConnectorConfig, @@ -159,6 +150,7 @@ export const ConnectorIndicator: FC = () => { const hasConnectors = connectors.length > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; const totalSourceCount = connectors.length + activeDocumentTypes.length; + const activeConnectorsCount = connectors.length; // Only actual connectors, not document types // Check which connectors are already connected const connectedTypes = new Set( @@ -170,7 +162,7 @@ export const ConnectorIndicator: FC = () => { return ( { "border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none" )} aria-label={ - hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector" + hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector" } onClick={() => handleOpenChange(true)} > @@ -188,9 +180,9 @@ export const ConnectorIndicator: FC = () => { ) : ( <> - {totalSourceCount > 0 && ( + {activeConnectorsCount > 0 && ( - {totalSourceCount > 99 ? "99+" : totalSourceCount} + {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} )} @@ -200,10 +192,7 @@ export const ConnectorIndicator: FC = () => { {/* YouTube Crawler View - shown when adding YouTube videos */} {isYouTubeView && searchSpaceId ? ( - + ) : connectingConnectorType ? ( { frequencyMinutes={frequencyMinutes} isSaving={isSaving} isDisconnecting={isDisconnecting} + isIndexing={indexingConnectorIds.has(editingConnector.id)} onStartDateChange={setStartDate} onEndDateChange={setEndDate} onPeriodicEnabledChange={setPeriodicEnabled} @@ -231,16 +221,25 @@ export const ConnectorIndicator: FC = () => { onSave={() => handleSaveConnector(() => refreshConnectors())} onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())} onBack={handleBackFromEdit} + onQuickIndex={ + editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" + ? () => handleQuickIndexConnector(editingConnector.id) + : undefined + } onConfigChange={setConnectorConfig} onNameChange={setConnectorName} /> ) : indexingConfig ? ( { onSkip={handleSkipIndexing} /> ) : ( - + {/* Header */} {
- - - + + + void; @@ -33,6 +35,20 @@ function extractIndexedCount(message: string | undefined): number | null { return match ? parseInt(match[1], 10) : null; } +/** + * Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs") + */ +function formatDocumentCount(count: number | undefined): string { + if (count === undefined || count === 0) return "0 docs"; + if (count < 1000) return `${count} docs`; + if (count < 1000000) { + const k = (count / 1000).toFixed(1); + return `${k.replace(/\.0$/, "")}k docs`; + } + const m = (count / 1000000).toFixed(1); + return `${m.replace(/\.0$/, "")}M docs`; +} + export const ConnectorCard: FC = ({ id, title, @@ -41,6 +57,7 @@ export const ConnectorCard: FC = ({ isConnected = false, isConnecting = false, documentCount, + lastIndexedAt, isIndexing = false, activeTask, onConnect, @@ -55,11 +72,7 @@ export const ConnectorCard: FC = ({ return (
- {indexingCount !== null ? ( - <>{indexingCount.toLocaleString()} indexed - ) : ( - "Syncing..." - )} + {indexingCount !== null ? <>{indexingCount.toLocaleString()} indexed : "Syncing..."} {/* Indeterminate progress bar with animation */}
@@ -70,18 +83,16 @@ export const ConnectorCard: FC = ({ } if (isConnected) { - if (documentCount !== undefined && documentCount > 0) { + // Show last indexed date for connected connectors + if (lastIndexedAt) { return ( - - - - {documentCount.toLocaleString()} document{documentCount !== 1 ? "s" : ""} - + + Last indexed: {format(new Date(lastIndexedAt), "MMM d, yyyy")} ); } - // Fallback for connected but no documents yet - return No documents indexed; + // Fallback for connected but never indexed + return Never indexed; } return description; @@ -102,16 +113,20 @@ export const ConnectorCard: FC = ({
{title}
-
- {getStatusContent()} -
+
{getStatusContent()}
+ {isConnected && documentCount !== undefined && ( +

+ {formatDocumentCount(documentCount)} +

+ )}
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx index c21c305b7..a18c79a1f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-dialog-header.tsx @@ -1,16 +1,9 @@ "use client"; -import { Search } from "lucide-react"; +import { Search, X } from "lucide-react"; import type { FC } from "react"; -import { - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - TabsList, - TabsTrigger, -} from "@/components/ui/tabs"; +import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; interface ConnectorDialogHeaderProps { @@ -74,14 +67,26 @@ export const ConnectorDialogHeader: FC = ({ onSearchChange(e.target.value)} /> + {searchQuery && ( + + )}
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/date-range-selector.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/date-range-selector.tsx index 9c84e7f03..bbb2ea482 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/date-range-selector.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/date-range-selector.tsx @@ -43,13 +43,16 @@ export const DateRangeSelector: FC = ({

Select Date Range

- Choose how far back you want to sync your data. You can always re-index later with different dates. + Choose how far back you want to sync your data. You can always re-index later with different + dates.

{/* Start Date */}
- +
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx index 1d22e6890..5b8bd698e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx @@ -37,9 +37,11 @@ export const PeriodicSyncConfig: FC = ({
{enabled && ( -
+
- +
@@ -62,4 +76,3 @@ export const PeriodicSyncConfig: FC = ({
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx index d84bd7797..677b92c5c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx @@ -32,10 +32,7 @@ const baiduSearchApiFormSchema = z.object({ type BaiduSearchApiFormValues = z.infer; -export const BaiduSearchApiConnectForm: FC = ({ - onSubmit, - isSubmitting, -}) => { +export const BaiduSearchApiConnectForm: FC = ({ onSubmit, isSubmitting }) => { const isSubmittingRef = useRef(false); const form = useForm({ resolver: zodResolver(baiduSearchApiFormSchema), @@ -77,7 +74,8 @@ export const BaiduSearchApiConnectForm: FC = ({
API Key Required - You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing up at{" "} + You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing + up at{" "} = ({
- + = ({ Connector Name - @@ -122,12 +124,12 @@ export const BaiduSearchApiConnectForm: FC = ({ Baidu AppBuilder API Key - @@ -155,4 +157,3 @@ export const BaiduSearchApiConnectForm: FC = ({
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx index ff3dc07b5..8f6f3202f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx @@ -53,10 +53,7 @@ const bookstackConnectorFormSchema = z.object({ type BookStackConnectorFormValues = z.infer; -export const BookStackConnectForm: FC = ({ - onSubmit, - isSubmitting, -}) => { +export const BookStackConnectForm: FC = ({ onSubmit, isSubmitting }) => { const isSubmittingRef = useRef(false); const [startDate, setStartDate] = useState(undefined); const [endDate, setEndDate] = useState(undefined); @@ -110,14 +107,19 @@ export const BookStackConnectForm: FC = ({
API Token Required - You'll need a BookStack API Token to use this connector. You can create one from your BookStack instance settings. + You'll need a BookStack API Token to use this connector. You can create one from your + BookStack instance settings.
- + = ({ Connector Name - @@ -147,16 +149,17 @@ export const BookStackConnectForm: FC = ({ BookStack Base URL - - The base URL of your BookStack instance (e.g., https://your-bookstack-instance.com). + The base URL of your BookStack instance (e.g., + https://your-bookstack-instance.com). @@ -170,11 +173,11 @@ export const BookStackConnectForm: FC = ({ Token ID - @@ -192,12 +195,12 @@ export const BookStackConnectForm: FC = ({ Token Secret - @@ -211,7 +214,7 @@ export const BookStackConnectForm: FC = ({ {/* Indexing Configuration */}

Indexing Configuration

- + {/* Date Range Selector */} = ({ Automatically re-index at regular intervals

- +
{periodicEnabled && (
- - = ({ - Every 15 minutes - Every hour - Every 6 hours - Every 12 hours - Daily - Weekly + + Every 15 minutes + + + Every hour + + + Every 6 hours + + + Every 12 hours + + + Daily + + + Weekly +
@@ -264,7 +289,9 @@ export const BookStackConnectForm: FC = ({ {/* What you get section */} {getConnectorBenefits(EnumConnectorName.BOOKSTACK_CONNECTOR) && (
-

What you get with BookStack integration:

+

+ What you get with BookStack integration: +

); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx index ca296e482..c31a4645a 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx @@ -77,4 +77,3 @@ export function getConnectorConfigComponent( return null; } } - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 21e84a67b..2b5987f9e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -126,17 +126,17 @@ export const ConnectorConnectView: FC = ({ {/* Fixed Footer - Action buttons */}
- -
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 5fda12a7a..e57861d55 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowLeft, Info, Loader2, Trash2 } from "lucide-react"; +import { ArrowLeft, Info, Loader2, RefreshCw, Trash2 } from "lucide-react"; import { type FC, useState, useCallback, useRef, useEffect, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; @@ -18,6 +18,7 @@ interface ConnectorEditViewProps { frequencyMinutes: string; isSaving: boolean; isDisconnecting: boolean; + isIndexing?: boolean; onStartDateChange: (date: Date | undefined) => void; onEndDateChange: (date: Date | undefined) => void; onPeriodicEnabledChange: (enabled: boolean) => void; @@ -25,7 +26,8 @@ interface ConnectorEditViewProps { onSave: () => void; onDisconnect: () => void; onBack: () => void; - onConfigChange?: (config: Record) => void; + onQuickIndex?: () => void; + onConfigChange?: (config: Record) => void; onNameChange?: (name: string) => void; } @@ -37,6 +39,7 @@ export const ConnectorEditView: FC = ({ frequencyMinutes, isSaving, isDisconnecting, + isIndexing = false, onStartDateChange, onEndDateChange, onPeriodicEnabledChange, @@ -44,6 +47,7 @@ export const ConnectorEditView: FC = ({ onSave, onDisconnect, onBack, + onQuickIndex, onConfigChange, onNameChange, }) => { @@ -59,12 +63,13 @@ export const ConnectorEditView: FC = ({ const checkScrollState = useCallback(() => { if (!scrollContainerRef.current) return; - + const target = scrollContainerRef.current; const scrolled = target.scrollTop > 0; - const hasMore = target.scrollHeight > target.clientHeight && + const hasMore = + target.scrollHeight > target.clientHeight && target.scrollTop + target.clientHeight < target.scrollHeight - 10; - + setIsScrolled(scrolled); setHasMoreContent(hasMore); }, []); @@ -79,11 +84,11 @@ export const ConnectorEditView: FC = ({ const resizeObserver = new ResizeObserver(() => { checkScrollState(); }); - + if (scrollContainerRef.current) { resizeObserver.observe(scrollContainerRef.current); } - + return () => { resizeObserver.disconnect(); }; @@ -105,10 +110,12 @@ export const ConnectorEditView: FC = ({ return (
{/* Fixed Header */} -
+
{/* Back button */} {/* Connector header */} -
-
- {getConnectorIcon(connector.connector_type, "size-7")} -
-
-

- {connector.name} -

-

- Manage your connector settings and sync configuration -

+
+
+
+ {getConnectorIcon(connector.connector_type, "size-7")} +
+
+

{connector.name}

+

+ Manage your connector settings and sync configuration +

+
+ {/* Quick Index Button - only show for indexable connectors, but not for Google Drive (requires folder selection) */} + {connector.is_indexable && + onQuickIndex && + connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && ( + + )}
{/* Scrollable Content */}
-
@@ -156,21 +187,25 @@ export const ConnectorEditView: FC = ({ {connector.is_indexable && ( <> {/* Date range selector - not shown for Google Drive (uses folder selection) or Webcrawler (uses config) */} - {connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && connector.connector_type !== "WEBCRAWLER_CONNECTOR" && ( - + )} + + {/* Periodic sync - not shown for Google Drive */} + {connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && ( + )} - - )} @@ -181,9 +216,12 @@ export const ConnectorEditView: FC = ({
-

Re-indexing runs in the background

+

+ Re-indexing runs in the background +

- You can continue using SurfSense while we sync your data. Check the Active tab to see progress. + You can continue using SurfSense while we sync your data. Check the Active tab + to see progress.

@@ -201,49 +239,56 @@ export const ConnectorEditView: FC = ({
{/* Fixed Footer - Action buttons */} -
+
{showDisconnectConfirm ? ( -
- Are you sure? - - +
+ + Are you sure? + +
+ + +
) : ( )} -
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx index 2a82b1b75..e8ffde2cf 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx @@ -45,7 +45,7 @@ export const IndexingConfigurationView: FC = ({ }) => { // Get connector-specific config component const ConnectorConfigComponent = useMemo( - () => connector ? getConnectorConfigComponent(connector.connector_type) : null, + () => (connector ? getConnectorConfigComponent(connector.connector_type) : null), [connector] ); const [isScrolled, setIsScrolled] = useState(false); @@ -54,12 +54,13 @@ export const IndexingConfigurationView: FC = ({ const checkScrollState = useCallback(() => { if (!scrollContainerRef.current) return; - + const target = scrollContainerRef.current; const scrolled = target.scrollTop > 0; - const hasMore = target.scrollHeight > target.clientHeight && + const hasMore = + target.scrollHeight > target.clientHeight && target.scrollTop + target.clientHeight < target.scrollHeight - 10; - + setIsScrolled(scrolled); setHasMoreContent(hasMore); }, []); @@ -74,11 +75,11 @@ export const IndexingConfigurationView: FC = ({ const resizeObserver = new ResizeObserver(() => { checkScrollState(); }); - + if (scrollContainerRef.current) { resizeObserver.observe(scrollContainerRef.current); } - + return () => { resizeObserver.disconnect(); }; @@ -87,10 +88,12 @@ export const IndexingConfigurationView: FC = ({ return (
{/* Fixed Header */} -
+
{/* Back button */} -
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 46f6cb130..06860fb8f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -150,4 +150,3 @@ export const OTHER_CONNECTORS = [ // Re-export IndexingConfigState from schemas for backward compatibility export type { IndexingConfigState } from "./connector-popup.schemas"; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 89d8553b6..3fcdf352f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -80,7 +80,7 @@ export function parseConnectorPopupQueryParams( params: URLSearchParams | Record ): ConnectorPopupQueryParams { const obj: Record = {}; - + if (params instanceof URLSearchParams) { params.forEach((value, key) => { obj[key] = value || undefined; @@ -90,7 +90,7 @@ export function parseConnectorPopupQueryParams( obj[key] = value || undefined; }); } - + return connectorPopupQueryParamsSchema.parse(obj); } @@ -107,4 +107,3 @@ export function parseOAuthAuthResponse(data: unknown): OAuthAuthResponse { export function validateIndexingConfigState(data: unknown): IndexingConfigState { return indexingConfigStateSchema.parse(data); } - diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index a24ada2e3..fa35dda02 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -2,7 +2,12 @@ import { useAtomValue } from "jotai"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import { createConnectorMutationAtom, deleteConnectorMutationAtom, indexConnectorMutationAtom, updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms"; +import { + createConnectorMutationAtom, + deleteConnectorMutationAtom, + indexConnectorMutationAtom, + updateConnectorMutationAtom, +} from "@/atoms/connectors/connector-mutation.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { authenticatedFetch } from "@/lib/auth-utils"; @@ -39,20 +44,23 @@ export const useConnectorDialog = () => { const [searchQuery, setSearchQuery] = useState(""); const [indexingConfig, setIndexingConfig] = useState(null); const [indexingConnector, setIndexingConnector] = useState(null); - const [indexingConnectorConfig, setIndexingConnectorConfig] = useState | null>(null); + const [indexingConnectorConfig, setIndexingConnectorConfig] = useState | null>(null); const [startDate, setStartDate] = useState(undefined); const [endDate, setEndDate] = useState(undefined); const [isStartingIndexing, setIsStartingIndexing] = useState(false); const [periodicEnabled, setPeriodicEnabled] = useState(false); const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - + // Edit mode state const [editingConnector, setEditingConnector] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false); const [connectorConfig, setConnectorConfig] = useState | null>(null); const [connectorName, setConnectorName] = useState(null); - + // Connect mode state (for non-OAuth connectors) const [connectingConnectorType, setConnectingConnectorType] = useState(null); const [isCreatingConnector, setIsCreatingConnector] = useState(false); @@ -61,13 +69,20 @@ export const useConnectorDialog = () => { // Helper function to get frequency label const getFrequencyLabel = useCallback((minutes: string): string => { switch (minutes) { - case "15": return "15 minutes"; - case "60": return "hour"; - case "360": return "6 hours"; - case "720": return "12 hours"; - case "1440": return "day"; - case "10080": return "week"; - default: return `${minutes} minutes`; + case "15": + return "15 minutes"; + case "60": + return "hour"; + case "360": + return "6 hours"; + case "720": + return "12 hours"; + case "1440": + return "day"; + case "10080": + return "week"; + default: + return `${minutes} minutes`; } }, []); @@ -75,42 +90,42 @@ export const useConnectorDialog = () => { useEffect(() => { try { const params = parseConnectorPopupQueryParams(searchParams); - + if (params.modal === "connectors") { setIsOpen(true); - + if (params.tab === "active" || params.tab === "all") { setActiveTab(params.tab); } - + // Clear indexing config if view is not "configure" anymore if (params.view !== "configure" && indexingConfig) { setIndexingConfig(null); } - + // Clear editing connector if view is not "edit" anymore if (params.view !== "edit" && editingConnector) { setEditingConnector(null); setConnectorName(null); } - + // Clear connecting connector type if view is not "connect" anymore if (params.view !== "connect" && connectingConnectorType) { setConnectingConnectorType(null); } - + // Handle connect view if (params.view === "connect" && params.connectorType && !connectingConnectorType) { setConnectingConnectorType(params.connectorType); } - + // Handle YouTube view if (params.view === "youtube") { // YouTube view is active - no additional state needed } - + if (params.view === "configure" && params.connector && !indexingConfig) { - const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === params.connector); + const oauthConnector = OAUTH_CONNECTORS.find((c) => c.id === params.connector); if (oauthConnector && allConnectors) { const existingConnector = allConnectors.find( (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType @@ -131,7 +146,7 @@ export const useConnectorDialog = () => { } } } - + // Handle edit view if (params.view === "edit" && params.connectorId && allConnectors && !editingConnector) { const connectorId = parseInt(params.connectorId, 10); @@ -141,11 +156,14 @@ export const useConnectorDialog = () => { if (connectorValidation.success) { setEditingConnector(connector); setConnectorConfig(connector.config); - // Load existing periodic sync settings - setPeriodicEnabled(connector.periodic_indexing_enabled); - setFrequencyMinutes( - connector.indexing_frequency_minutes?.toString() || "1440" + setConnectorName(connector.name); + // Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors) + setPeriodicEnabled( + connector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !connector.is_indexable + ? false + : connector.periodic_indexing_enabled ); + setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); // Reset dates - user can set new ones for re-indexing setStartDate(undefined); setEndDate(undefined); @@ -195,13 +213,18 @@ export const useConnectorDialog = () => { useEffect(() => { try { const params = parseConnectorPopupQueryParams(searchParams); - - if (params.success === "true" && params.connector && searchSpaceId && params.modal === "connectors") { - const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === params.connector); + + if ( + params.success === "true" && + params.connector && + searchSpaceId && + params.modal === "connectors" + ) { + const oauthConnector = OAUTH_CONNECTORS.find((c) => c.id === params.connector); if (oauthConnector) { refetchAllConnectors().then((result) => { if (!result.data) return; - + const newConnector = result.data.find( (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType ); @@ -255,10 +278,10 @@ export const useConnectorDialog = () => { } const data = await response.json(); - + // Validate OAuth response with Zod const validatedData = parseOAuthAuthResponse(data); - + // Don't clear connectingId here - let the redirect happen with button still disabled // The component will unmount on redirect anyway window.location.href = validatedData.auth_url; @@ -279,7 +302,7 @@ export const useConnectorDialog = () => { // Handle creating YouTube crawler (not a connector, shows view in popup) const handleCreateYouTubeCrawler = useCallback(() => { if (!searchSpaceId) return; - + // Update URL to show YouTube view const url = new URL(window.location.href); url.searchParams.set("modal", "connectors"); @@ -293,7 +316,7 @@ export const useConnectorDialog = () => { setConnectingId("webcrawler-connector"); try { - const newConnector = await createConnector({ + await createConnector({ data: { name: "Web Pages", connector_type: EnumConnectorName.WEBCRAWLER_CONNECTOR, @@ -343,23 +366,26 @@ export const useConnectorDialog = () => { }, [searchSpaceId, createConnector, refetchAllConnectors]); // Handle connecting non-OAuth connectors (like Tavily API) - const handleConnectNonOAuth = useCallback((connectorType: string) => { - if (!searchSpaceId) return; - - // Set connecting state - setConnectingConnectorType(connectorType); - - // Update URL to show connect view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "connect"); - url.searchParams.set("connectorType", connectorType); - window.history.pushState({ modal: true }, "", url.toString()); - }, [searchSpaceId]); + const handleConnectNonOAuth = useCallback( + (connectorType: string) => { + if (!searchSpaceId) return; + + // Set connecting state + setConnectingConnectorType(connectorType); + + // Update URL to show connect view + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "connect"); + url.searchParams.set("connectorType", connectorType); + window.history.pushState({ modal: true }, "", url.toString()); + }, + [searchSpaceId] + ); // Handle submitting connect form - const handleSubmitConnectForm = useCallback(async ( - formData: { + const handleSubmitConnectForm = useCallback( + async (formData: { name: string; connector_type: string; config: Record; @@ -372,165 +398,220 @@ export const useConnectorDialog = () => { endDate?: Date; periodicEnabled?: boolean; frequencyMinutes?: string; - } - ) => { - if (!searchSpaceId || !connectingConnectorType) return; - - // Prevent multiple submissions using ref for immediate check - if (isCreatingConnectorRef.current) return; - isCreatingConnectorRef.current = true; + }) => { + if (!searchSpaceId || !connectingConnectorType) return; - setIsCreatingConnector(true); - try { - // Extract UI-only fields before sending to backend - const { startDate, endDate, periodicEnabled, frequencyMinutes, ...connectorData } = formData; - - // Create connector - ensure types match the schema - const newConnector = await createConnector({ - data: { - ...connectorData, - connector_type: connectorData.connector_type as EnumConnectorName, - next_scheduled_at: connectorData.next_scheduled_at as string | null, - }, - queryParams: { - search_space_id: searchSpaceId, - }, - }); + // Prevent multiple submissions using ref for immediate check + if (isCreatingConnectorRef.current) return; + isCreatingConnectorRef.current = true; - // Refetch connectors to get the new one - const result = await refetchAllConnectors(); - if (result.data) { - const connector = result.data.find( - (c: SearchSourceConnector) => c.id === newConnector.id - ); - if (connector) { - // Validate connector data - const connectorValidation = searchSourceConnector.safeParse(connector); - if (connectorValidation.success) { - // Store connectingConnectorType before clearing it - const currentConnectorType = connectingConnectorType; - - // Find connector title from constants - const connectorInfo = OTHER_CONNECTORS.find( - c => c.connectorType === currentConnectorType - ); - const connectorTitle = connectorInfo?.title || connector.name; - - // Set up indexing config - const config = validateIndexingConfigState({ - connectorType: currentConnectorType as EnumConnectorName, - connectorId: connector.id, - connectorTitle, - }); - - // Clear connecting state to allow view transition - setConnectingConnectorType(null); - - // Set indexing config state - setIndexingConfig(config); - setIndexingConnector(connector); - setIndexingConnectorConfig(connector.config || {}); - - // Pre-populate indexing configuration with values from form if provided - if (formData.startDate !== undefined) { - setStartDate(formData.startDate); - } - if (formData.endDate !== undefined) { - setEndDate(formData.endDate); - } - if (formData.periodicEnabled !== undefined) { - setPeriodicEnabled(formData.periodicEnabled); - } - if (formData.frequencyMinutes !== undefined) { - setFrequencyMinutes(formData.frequencyMinutes); - } - - // Auto-start indexing for non-OAuth reindexable connectors - // This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch, Linear) - // Non-reindexable connectors (e.g., Tavily) have is_indexable: false, so they won't trigger this - // Backend will use default date ranges (365 days ago to today) if dates are not provided - if (connector.is_indexable) { - // Get indexing configuration from form (or use defaults) - const startDateForIndexing = formData.startDate; - const endDateForIndexing = formData.endDate; - const periodicEnabledForIndexing = formData.periodicEnabled || false; - const frequencyMinutesForIndexing = formData.frequencyMinutes || "1440"; - - // Update connector with periodic sync settings if enabled - if (periodicEnabledForIndexing) { - const frequency = parseInt(frequencyMinutesForIndexing, 10); - await updateConnector({ - id: connector.id, - data: { - periodic_indexing_enabled: true, - indexing_frequency_minutes: frequency, + setIsCreatingConnector(true); + try { + // Extract UI-only fields before sending to backend + const { startDate, endDate, periodicEnabled, frequencyMinutes, ...connectorData } = + formData; + + // Create connector - ensure types match the schema + const newConnector = await createConnector({ + data: { + ...connectorData, + connector_type: connectorData.connector_type as EnumConnectorName, + next_scheduled_at: connectorData.next_scheduled_at as string | null, + }, + queryParams: { + search_space_id: searchSpaceId, + }, + }); + + // Refetch connectors to get the new one + const result = await refetchAllConnectors(); + if (result.data) { + const connector = result.data.find( + (c: SearchSourceConnector) => c.id === newConnector.id + ); + if (connector) { + // Validate connector data + const connectorValidation = searchSourceConnector.safeParse(connector); + if (connectorValidation.success) { + // Store connectingConnectorType before clearing it + const currentConnectorType = connectingConnectorType; + + // Find connector title from constants + const connectorInfo = OTHER_CONNECTORS.find( + (c) => c.connectorType === currentConnectorType + ); + const connectorTitle = connectorInfo?.title || connector.name; + + // Set up indexing config + const config = validateIndexingConfigState({ + connectorType: currentConnectorType as EnumConnectorName, + connectorId: connector.id, + connectorTitle, + }); + + // Clear connecting state to allow view transition + setConnectingConnectorType(null); + + // Set indexing config state + setIndexingConfig(config); + setIndexingConnector(connector); + setIndexingConnectorConfig(connector.config || {}); + + // Pre-populate indexing configuration with values from form if provided + if (formData.startDate !== undefined) { + setStartDate(formData.startDate); + } + if (formData.endDate !== undefined) { + setEndDate(formData.endDate); + } + if (formData.periodicEnabled !== undefined) { + setPeriodicEnabled(formData.periodicEnabled); + } + if (formData.frequencyMinutes !== undefined) { + setFrequencyMinutes(formData.frequencyMinutes); + } + + // Auto-start indexing for non-OAuth reindexable connectors + // This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch, Linear) + // Non-reindexable connectors (e.g., Tavily) have is_indexable: false, so they won't trigger this + // Backend will use default date ranges (365 days ago to today) if dates are not provided + if (connector.is_indexable) { + // Get indexing configuration from form (or use defaults) + const startDateForIndexing = formData.startDate; + const endDateForIndexing = formData.endDate; + const periodicEnabledForIndexing = formData.periodicEnabled || false; + const frequencyMinutesForIndexing = formData.frequencyMinutes || "1440"; + + // Update connector with periodic sync settings if enabled + if (periodicEnabledForIndexing) { + const frequency = parseInt(frequencyMinutesForIndexing, 10); + await updateConnector({ + id: connector.id, + data: { + periodic_indexing_enabled: true, + indexing_frequency_minutes: frequency, + }, + }); + } + + // Start indexing (backend will use defaults if dates are undefined) + const startDateStr = startDateForIndexing + ? format(startDateForIndexing, "yyyy-MM-dd") + : undefined; + const endDateStr = endDateForIndexing + ? format(endDateForIndexing, "yyyy-MM-dd") + : undefined; + + await indexConnector({ + connector_id: connector.id, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, }, }); + + toast.success(`${connectorTitle} connected and indexing started!`, { + description: periodicEnabledForIndexing + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.` + : "You can continue working while we sync your data.", + }); + + // Close modal and return to main view + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + + // Clear indexing config state since we're not showing the view + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); + + // Invalidate queries to refresh data + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + + // Refresh connectors list + await refetchAllConnectors(); + } else { + // Non-indexable connector + // For Circleback, transition to edit view to show webhook URL + // For other non-indexable connectors, just close the modal + if (currentConnectorType === "CIRCLEBACK_CONNECTOR") { + // Clear connecting state and indexing config state + setConnectingConnectorType(null); + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); + + // Set up edit view state + setEditingConnector(connector); + setConnectorName(connector.name); + setConnectorConfig(connector.config || {}); + setPeriodicEnabled(false); + setFrequencyMinutes("1440"); + setStartDate(undefined); + setEndDate(undefined); + + toast.success(`${connectorTitle} connected successfully!`, { + description: "Configure the webhook URL in your Circleback settings.", + }); + + // Transition to edit view + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "edit"); + url.searchParams.set("connectorId", connector.id.toString()); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + + // Refresh connectors list + await refetchAllConnectors(); + } else { + // Other non-indexable connectors - just show success message and close + toast.success(`${connectorTitle} connected successfully!`); + + // Close modal and return to main view + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + + // Clear indexing config state + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); + } } - - // Start indexing (backend will use defaults if dates are undefined) - const startDateStr = startDateForIndexing ? format(startDateForIndexing, "yyyy-MM-dd") : undefined; - const endDateStr = endDateForIndexing ? format(endDateForIndexing, "yyyy-MM-dd") : undefined; - - await indexConnector({ - connector_id: connector.id, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, - }); - - toast.success(`${connectorTitle} connected and indexing started!`, { - description: periodicEnabledForIndexing - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.` - : "You can continue working while we sync your data.", - }); - - // Close modal and return to main view - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); - - // Clear indexing config state since we're not showing the view - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); - - // Invalidate queries to refresh data - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - - // Refresh connectors list - await refetchAllConnectors(); - } else { - // Non-indexable connector - just show success message - toast.success(`${connectorTitle} connected successfully!`); - - // Close modal and return to main view - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); } } } + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + isCreatingConnectorRef.current = false; + setIsCreatingConnector(false); + // Don't clear connectingConnectorType here - it's cleared above when transitioning to config view } - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - isCreatingConnectorRef.current = false; - setIsCreatingConnector(false); - // Don't clear connectingConnectorType here - it's cleared above when transitioning to config view - } - }, [connectingConnectorType, searchSpaceId, createConnector, refetchAllConnectors, updateConnector, indexConnector, router, getFrequencyLabel, queryClient]); + }, + [ + connectingConnectorType, + searchSpaceId, + createConnector, + refetchAllConnectors, + updateConnector, + indexConnector, + router, + getFrequencyLabel, + ] + ); // Handle going back from connect view const handleBackFromConnect = useCallback(() => { @@ -552,119 +633,151 @@ export const useConnectorDialog = () => { }, [router]); // Handle starting indexing - const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => { - if (!indexingConfig || !searchSpaceId) return; + const handleStartIndexing = useCallback( + async (refreshConnectors: () => void) => { + if (!indexingConfig || !searchSpaceId) return; - // Validate date range (skip for Google Drive and Webcrawler) - if (indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR") { - const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); - if (!dateRangeValidation.success) { - const firstIssueMsg = - dateRangeValidation.error.issues && dateRangeValidation.error.issues.length > 0 - ? dateRangeValidation.error.issues[0].message - : "Invalid date range"; - toast.error(firstIssueMsg); - return; - } - } - - // Validate frequency minutes if periodic is enabled - if (periodicEnabled) { - const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); - if (!frequencyValidation.success) { - toast.error("Invalid frequency value"); - return; - } - } - - setIsStartingIndexing(true); - try { - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - - // Update connector with periodic sync settings and config changes - if (periodicEnabled || indexingConnectorConfig) { - const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : undefined; - await updateConnector({ - id: indexingConfig.connectorId, - data: { - ...(periodicEnabled && { - periodic_indexing_enabled: true, - indexing_frequency_minutes: frequency, - }), - ...(indexingConnectorConfig && { - config: indexingConnectorConfig, - }), - }, - }); + // Validate date range (skip for Google Drive and Webcrawler) + if ( + indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && + indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR" + ) { + const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); + if (!dateRangeValidation.success) { + const firstIssueMsg = + dateRangeValidation.error.issues && dateRangeValidation.error.issues.length > 0 + ? dateRangeValidation.error.issues[0].message + : "Invalid date range"; + toast.error(firstIssueMsg); + return; + } } - // Handle Google Drive folder selection - if (indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && indexingConnectorConfig) { - const selectedFolders = indexingConnectorConfig.selected_folders as Array<{ id: string; name: string }> | undefined; - if (selectedFolders && selectedFolders.length > 0) { - // Index with folder selection - const folderIds = selectedFolders.map((f) => f.id).join(","); - const folderNames = selectedFolders.map((f) => f.name).join(", "); + // Validate frequency minutes if periodic is enabled + if (periodicEnabled) { + const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); + if (!frequencyValidation.success) { + toast.error("Invalid frequency value"); + return; + } + } + + setIsStartingIndexing(true); + try { + const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; + const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; + + // Update connector with periodic sync settings and config changes + // Note: Periodic sync is disabled for Google Drive connectors + if (periodicEnabled || indexingConnectorConfig) { + const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : undefined; + await updateConnector({ + id: indexingConfig.connectorId, + data: { + ...(periodicEnabled && + indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && { + periodic_indexing_enabled: true, + indexing_frequency_minutes: frequency, + }), + ...(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && { + periodic_indexing_enabled: false, + indexing_frequency_minutes: null, + }), + ...(indexingConnectorConfig && { + config: indexingConnectorConfig, + }), + }, + }); + } + + // Handle Google Drive folder selection + if (indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && indexingConnectorConfig) { + const selectedFolders = indexingConnectorConfig.selected_folders as + | Array<{ id: string; name: string }> + | undefined; + const selectedFiles = indexingConnectorConfig.selected_files as + | Array<{ id: string; name: string }> + | undefined; + if ( + (selectedFolders && selectedFolders.length > 0) || + (selectedFiles && selectedFiles.length > 0) + ) { + // Index with folder/file selection + await indexConnector({ + connector_id: indexingConfig.connectorId, + queryParams: { + search_space_id: searchSpaceId, + }, + body: { + folders: selectedFolders || [], + files: selectedFiles || [], + }, + }); + } else { + // Google Drive requires folder selection - show error if none selected + toast.error("Please select at least one folder to index"); + setIsStartingIndexing(false); + return; + } + } else if (indexingConfig.connectorType === "WEBCRAWLER_CONNECTOR") { + // Webcrawler doesn't use date ranges, just uses config (API key and URLs) await indexConnector({ connector_id: indexingConfig.connectorId, queryParams: { search_space_id: searchSpaceId, - folder_ids: folderIds, - folder_names: folderNames, }, }); } else { - // Google Drive requires folder selection - show error if none selected - toast.error("Please select at least one folder to index"); - setIsStartingIndexing(false); - return; + await indexConnector({ + connector_id: indexingConfig.connectorId, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, + }, + }); } - } else if (indexingConfig.connectorType === "WEBCRAWLER_CONNECTOR") { - // Webcrawler doesn't use date ranges, just uses config (API key and URLs) - await indexConnector({ - connector_id: indexingConfig.connectorId, - queryParams: { - search_space_id: searchSpaceId, - }, + + toast.success(`${indexingConfig.connectorTitle} indexing started`, { + description: periodicEnabled + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` + : "You can continue working while we sync your data.", }); - } else { - await indexConnector({ - connector_id: indexingConfig.connectorId, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, + + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("success"); + url.searchParams.delete("connector"); + url.searchParams.delete("view"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); + } catch (error) { + console.error("Error starting indexing:", error); + toast.error("Failed to start indexing"); + } finally { + setIsStartingIndexing(false); } - - toast.success(`${indexingConfig.connectorTitle} indexing started`, { - description: periodicEnabled - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` - : "You can continue working while we sync your data.", - }); - - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error starting indexing:", error); - toast.error("Failed to start indexing"); - } finally { - setIsStartingIndexing(false); - } - }, [indexingConfig, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router, indexingConnectorConfig]); + }, + [ + indexingConfig, + searchSpaceId, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + getFrequencyLabel, + router, + indexingConnectorConfig, + ] + ); // Handle skipping indexing const handleSkipIndexing = useCallback(() => { @@ -679,201 +792,256 @@ export const useConnectorDialog = () => { }, [router]); // Handle starting edit mode - const handleStartEdit = useCallback((connector: SearchSourceConnector) => { - if (!searchSpaceId) return; - - // Check if this is an OAuth connector - const isOAuthConnector = OAUTH_CONNECTORS.some( - (oauthConnector) => oauthConnector.connectorType === connector.connector_type - ); - - // Check if this is webcrawler, Tavily API, SearxNG, Linkup, Baidu, Linear, Elasticsearch, Slack, Discord, or Notion (can be managed in popup) - const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR; - const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API; - const isSearxng = connector.connector_type === EnumConnectorName.SEARXNG_API; - const isLinkup = connector.connector_type === EnumConnectorName.LINKUP_API; - const isBaidu = connector.connector_type === EnumConnectorName.BAIDU_SEARCH_API; - const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR; - const isElasticsearch = connector.connector_type === EnumConnectorName.ELASTICSEARCH_CONNECTOR; - const isSlack = connector.connector_type === EnumConnectorName.SLACK_CONNECTOR; - const isDiscord = connector.connector_type === EnumConnectorName.DISCORD_CONNECTOR; - const isNotion = connector.connector_type === EnumConnectorName.NOTION_CONNECTOR; - - // If not OAuth, not webcrawler, not Tavily API, not SearxNG, not Linkup, not Baidu, not Linear, not Elasticsearch, not Slack, not Discord, and not Notion, redirect to old connector edit page - if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isSearxng && !isLinkup && !isBaidu && !isLinear && !isElasticsearch && !isSlack && !isDiscord && !isNotion) { - router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`); - return; - } - - // Validate connector data - const connectorValidation = searchSourceConnector.safeParse(connector); - if (!connectorValidation.success) { - toast.error("Invalid connector data"); - return; - } - - setEditingConnector(connector); - setConnectorName(connector.name); - // Load existing periodic sync settings - setPeriodicEnabled(connector.periodic_indexing_enabled); - setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); - // Reset dates - user can set new ones for re-indexing - setStartDate(undefined); - setEndDate(undefined); - - // Update URL - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "edit"); - url.searchParams.set("connectorId", connector.id.toString()); - window.history.pushState({ modal: true }, "", url.toString()); - }, [searchSpaceId, router]); + const handleStartEdit = useCallback( + (connector: SearchSourceConnector) => { + if (!searchSpaceId) return; + + // All connector types should be handled in the popup edit view + // Validate connector data + const connectorValidation = searchSourceConnector.safeParse(connector); + if (!connectorValidation.success) { + toast.error("Invalid connector data"); + return; + } + + setEditingConnector(connector); + setConnectorName(connector.name); + // Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors) + setPeriodicEnabled( + connector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !connector.is_indexable + ? false + : connector.periodic_indexing_enabled + ); + setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); + // Reset dates - user can set new ones for re-indexing + setStartDate(undefined); + setEndDate(undefined); + + // Update URL + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "edit"); + url.searchParams.set("connectorId", connector.id.toString()); + window.history.pushState({ modal: true }, "", url.toString()); + }, + [searchSpaceId] + ); // Handle saving connector changes - const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => { - if (!editingConnector || !searchSpaceId) return; + const handleSaveConnector = useCallback( + async (refreshConnectors: () => void) => { + if (!editingConnector || !searchSpaceId) return; - // Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors) - if (editingConnector.is_indexable && editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR") { - const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); - if (!dateRangeValidation.success) { - toast.error(dateRangeValidation.error.issues[0]?.message || "Invalid date range"); + // Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors) + if ( + editingConnector.is_indexable && + editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && + editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR" + ) { + const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); + if (!dateRangeValidation.success) { + toast.error(dateRangeValidation.error.issues[0]?.message || "Invalid date range"); + return; + } + } + + // Prevent periodic indexing for non-indexable connectors + if (periodicEnabled && !editingConnector.is_indexable) { + toast.error("Periodic indexing is not available for this connector type"); return; } - } - // Prevent periodic indexing for non-indexable connectors - if (periodicEnabled && !editingConnector.is_indexable) { - toast.error("Periodic indexing is not available for this connector type"); - return; - } - - // Validate frequency minutes if periodic is enabled (only for indexable connectors) - if (periodicEnabled && editingConnector.is_indexable) { - const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); - if (!frequencyValidation.success) { - toast.error("Invalid frequency value"); - return; + // Validate frequency minutes if periodic is enabled (only for indexable connectors) + if (periodicEnabled && editingConnector.is_indexable) { + const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); + if (!frequencyValidation.success) { + toast.error("Invalid frequency value"); + return; + } } - } - setIsSaving(true); - try { - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; + setIsSaving(true); + try { + const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; + const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - // Update connector with periodic sync settings, config changes, and name - const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : null; - await updateConnector({ - id: editingConnector.id, - data: { - name: connectorName || editingConnector.name, - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: frequency, - config: connectorConfig || editingConnector.config, - }, - }); + // Update connector with periodic sync settings, config changes, and name + // Note: Periodic sync is disabled for Google Drive connectors and non-indexable connectors + const frequency = + periodicEnabled && editingConnector.is_indexable ? parseInt(frequencyMinutes, 10) : null; + await updateConnector({ + id: editingConnector.id, + data: { + name: connectorName || editingConnector.name, + periodic_indexing_enabled: + editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || + !editingConnector.is_indexable + ? false + : periodicEnabled, + indexing_frequency_minutes: + editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || + !editingConnector.is_indexable + ? null + : frequency, + config: connectorConfig || editingConnector.config, + }, + }); - // Re-index based on connector type (only for indexable connectors) - let indexingDescription = "Settings saved."; - if (!editingConnector.is_indexable) { - // Non-indexable connectors (like Tavily API) don't need re-indexing - indexingDescription = "Settings saved."; - } else if (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR") { - // Google Drive uses folder selection from config, not date ranges - const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as Array<{ id: string; name: string }> | undefined; - if (selectedFolders && selectedFolders.length > 0) { - const folderIds = selectedFolders.map((f) => f.id).join(","); - const folderNames = selectedFolders.map((f) => f.name).join(", "); + // Re-index based on connector type (only for indexable connectors) + let indexingDescription = "Settings saved."; + if (!editingConnector.is_indexable) { + // Non-indexable connectors (like Tavily API) don't need re-indexing + indexingDescription = "Settings saved."; + } else if (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR") { + // Google Drive uses folder selection from config, not date ranges + const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as + | Array<{ id: string; name: string }> + | undefined; + const selectedFiles = (connectorConfig || editingConnector.config)?.selected_files as + | Array<{ id: string; name: string }> + | undefined; + if ( + (selectedFolders && selectedFolders.length > 0) || + (selectedFiles && selectedFiles.length > 0) + ) { + await indexConnector({ + connector_id: editingConnector.id, + queryParams: { + search_space_id: searchSpaceId, + }, + body: { + folders: selectedFolders || [], + files: selectedFiles || [], + }, + }); + const totalItems = (selectedFolders?.length || 0) + (selectedFiles?.length || 0); + indexingDescription = `Re-indexing started for ${totalItems} item(s).`; + } + } else if (editingConnector.connector_type === "WEBCRAWLER_CONNECTOR") { + // Webcrawler uses config (API key and URLs), not date ranges await indexConnector({ connector_id: editingConnector.id, queryParams: { search_space_id: searchSpaceId, - folder_ids: folderIds, - folder_names: folderNames, }, }); - indexingDescription = `Re-indexing started for ${selectedFolders.length} folder(s).`; + indexingDescription = "Re-indexing started with updated configuration."; + } else if (startDateStr || endDateStr) { + // Other connectors use date ranges + await indexConnector({ + connector_id: editingConnector.id, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, + }, + }); + indexingDescription = "Re-indexing started with new date range."; } - } else if (editingConnector.connector_type === "WEBCRAWLER_CONNECTOR") { - // Webcrawler uses config (API key and URLs), not date ranges - await indexConnector({ - connector_id: editingConnector.id, - queryParams: { - search_space_id: searchSpaceId, - }, + + toast.success(`${editingConnector.name} updated successfully`, { + description: periodicEnabled + ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` + : indexingDescription, }); - indexingDescription = "Re-indexing started with updated configuration."; - } else if (startDateStr || endDateStr) { - // Other connectors use date ranges - await indexConnector({ - connector_id: editingConnector.id, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, + + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorId"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); - indexingDescription = "Re-indexing started with new date range."; + } catch (error) { + console.error("Error saving connector:", error); + toast.error("Failed to save connector changes"); + } finally { + setIsSaving(false); } - - toast.success(`${editingConnector.name} updated successfully`, { - description: periodicEnabled - ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` - : indexingDescription, - }); - - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error saving connector:", error); - toast.error("Failed to save connector changes"); - } finally { - setIsSaving(false); - } - }, [editingConnector, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router, connectorConfig, connectorName]); + }, + [ + editingConnector, + searchSpaceId, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + getFrequencyLabel, + router, + connectorConfig, + connectorName, + ] + ); // Handle disconnecting connector - const handleDisconnectConnector = useCallback(async (refreshConnectors: () => void) => { - if (!editingConnector || !searchSpaceId) return; + const handleDisconnectConnector = useCallback( + async (refreshConnectors: () => void) => { + if (!editingConnector || !searchSpaceId) return; - setIsDisconnecting(true); - try { - await deleteConnector({ - id: editingConnector.id, - }); + setIsDisconnecting(true); + try { + await deleteConnector({ + id: editingConnector.id, + }); - toast.success(`${editingConnector.name} disconnected successfully`); + toast.success(`${editingConnector.name} disconnected successfully`); - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error disconnecting connector:", error); - toast.error("Failed to disconnect connector"); - } finally { - setIsDisconnecting(false); - } - }, [editingConnector, searchSpaceId, deleteConnector, router]); + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorId"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error disconnecting connector:", error); + toast.error("Failed to disconnect connector"); + } finally { + setIsDisconnecting(false); + } + }, + [editingConnector, searchSpaceId, deleteConnector, router] + ); + + // Handle quick index (index without date picker, uses backend defaults) + const handleQuickIndexConnector = useCallback( + async (connectorId: number) => { + if (!searchSpaceId) return; + + try { + await indexConnector({ + connector_id: connectorId, + queryParams: { + search_space_id: searchSpaceId, + }, + }); + toast.success("Indexing started", { + description: "You can continue working while we sync your data.", + }); + + // Invalidate queries to refresh data + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error indexing connector content:", error); + toast.error(error instanceof Error ? error.message : "Failed to start indexing"); + } + }, + [searchSpaceId, indexConnector] + ); // Handle going back from edit view const handleBackFromEdit = useCallback(() => { @@ -924,22 +1092,19 @@ export const useConnectorDialog = () => { ); // Handle tab change - const handleTabChange = useCallback( - (value: string) => { - setActiveTab(value); - const url = new URL(window.location.href); - url.searchParams.set("tab", value); - window.history.replaceState({ modal: true }, "", url.toString()); - }, - [] - ); + const handleTabChange = useCallback((value: string) => { + setActiveTab(value); + const url = new URL(window.location.href); + url.searchParams.set("tab", value); + window.history.replaceState({ modal: true }, "", url.toString()); + }, []); // Handle scroll const handleScroll = useCallback((e: React.UIEvent) => { setIsScrolled(e.currentTarget.scrollTop > 0); }, []); - return { + return { // State isOpen, activeTab, @@ -961,7 +1126,7 @@ export const useConnectorDialog = () => { frequencyMinutes, searchSpaceId, allConnectors, - + // Setters setSearchQuery, setStartDate, @@ -969,7 +1134,7 @@ export const useConnectorDialog = () => { setPeriodicEnabled, setFrequencyMinutes, setConnectorName, - + // Handlers handleOpenChange, handleTabChange, @@ -987,9 +1152,9 @@ export const useConnectorDialog = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleQuickIndexConnector, connectorConfig, setConnectorConfig, setIndexingConnectorConfig, }; }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/index.ts b/surfsense_web/components/assistant-ui/connector-popup/index.ts index 7d7b737fd..e2e2d8b30 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/index.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/index.ts @@ -35,4 +35,3 @@ export type { // Hooks export { useConnectorDialog } from "./hooks/use-connector-dialog"; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 5016e1626..2c50530ce 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -1,19 +1,20 @@ "use client"; import { format } from "date-fns"; -import { Cable, FileText, Loader2 } from "lucide-react"; +import { ArrowRight, Cable, Loader2 } from "lucide-react"; import type { FC } from "react"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types"; import { cn } from "@/lib/utils"; -import { - TabsContent, -} from "@/components/ui/tabs"; +import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; +import { TabsContent } from "@/components/ui/tabs"; interface ActiveConnectorsTabProps { + searchQuery: string; hasSources: boolean; totalSourceCount: number; activeDocumentTypes: Array<[string, number]>; @@ -26,107 +27,186 @@ interface ActiveConnectorsTabProps { } export const ActiveConnectorsTab: FC = ({ + searchQuery, hasSources, activeDocumentTypes, connectors, indexingConnectorIds, logsSummary, + searchSpaceId, onTabChange, onManage, }) => { + const router = useRouter(); + + const handleViewAllDocuments = () => { + router.push(`/dashboard/${searchSpaceId}/documents`); + }; + + // Convert activeDocumentTypes array to Record for utility function + const documentTypeCounts = activeDocumentTypes.reduce( + (acc, [docType, count]) => { + acc[docType] = count; + return acc; + }, + {} as Record + ); + + // Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs") + const formatDocumentCount = (count: number | undefined): string => { + if (count === undefined || count === 0) return "0 docs"; + if (count < 1000) return `${count} docs`; + if (count < 1000000) { + const k = (count / 1000).toFixed(1); + return `${k.replace(/\.0$/, "")}k docs`; + } + const m = (count / 1000000).toFixed(1); + return `${m.replace(/\.0$/, "")}M docs`; + }; + + // Document types that should be shown as cards (not from connectors) + // These are: EXTENSION (browser extension), FILE (uploaded files), NOTE (editor notes), + // YOUTUBE_VIDEO (YouTube videos), and CRAWLED_URL (web pages - shown separately even though it can come from WEBCRAWLER_CONNECTOR) + const standaloneDocumentTypes = ["EXTENSION", "FILE", "NOTE", "YOUTUBE_VIDEO", "CRAWLED_URL"]; + + // Filter to only show standalone document types that have documents (count > 0) + const standaloneDocuments = activeDocumentTypes + .filter(([docType, count]) => standaloneDocumentTypes.includes(docType) && count > 0) + .map(([docType, count]) => ({ + type: docType, + count, + label: getDocumentTypeLabel(docType), + })) + .filter((doc) => { + if (!searchQuery) return true; + return doc.label.toLowerCase().includes(searchQuery.toLowerCase()); + }); + + // Filter connectors based on search query + const filteredConnectors = connectors.filter((connector) => { + if (!searchQuery) return true; + const searchLower = searchQuery.toLowerCase(); + return ( + connector.name.toLowerCase().includes(searchLower) || + connector.connector_type.toLowerCase().includes(searchLower) + ); + }); return ( {hasSources ? (
-
-

- Currently Active -

-
-
- {activeDocumentTypes.map(([docType, count]) => ( -
-
- {getConnectorIcon(docType, "size-6")} -
-
-

- {getDocumentTypeLabel(docType)} -

-

- - - {(count as number).toLocaleString()} document{count !== 1 ? "s" : ""} - -

-
+ {/* Active Connectors Section */} + {filteredConnectors.length > 0 && ( +
+
+

Active Connectors

- ))} - {connectors.map((connector) => { - const isIndexing = indexingConnectorIds.has(connector.id); - const activeTask = logsSummary?.active_tasks?.find( - (task: LogActiveTask) => task.connector_id === connector.id - ); +
+ {filteredConnectors.map((connector) => { + const isIndexing = indexingConnectorIds.has(connector.id); + const activeTask = logsSummary?.active_tasks?.find( + (task: LogActiveTask) => task.connector_id === connector.id + ); + const documentCount = getDocumentCountForConnector( + connector.connector_type, + documentTypeCounts + ); - return ( -
-
- {getConnectorIcon(connector.connector_type, "size-6")} -
-
-

- {connector.name} -

- {isIndexing ? ( -

- - Indexing... - {activeTask?.message && ( - - • {activeTask.message} - + return ( +

+
- ) : ( -

- {connector.last_indexed_at - ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` - : "Never indexed"} -

- )} -
-
+
+

+ {connector.name} +

+ {isIndexing ? ( +

+ + Indexing... + {activeTask?.message && ( + + • {activeTask.message} + + )} +

+ ) : ( +

+ {connector.last_indexed_at + ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` + : "Never indexed"} +

+ )} +

+ {formatDocumentCount(documentCount)} +

+
+ +
+ ); + })} +
+
+ )} + + {/* Standalone Documents Section */} + {standaloneDocuments.length > 0 && ( +
+
+

Documents

+ +
+
+ {standaloneDocuments.map((doc) => ( +
- {isIndexing ? "Syncing..." : "Manage"} - -
- ); - })} -
+
+ {getConnectorIcon(doc.type, "size-3.5")} +
+ {doc.label} + + {formatDocumentCount(doc.count)} + +
+ ))} +
+
+ )}
) : (
@@ -149,4 +229,3 @@ export const ActiveConnectorsTab: FC = ({ ); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 7ea9035c4..bdec4dcb2 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -1,11 +1,10 @@ "use client"; -import { useRouter } from "next/navigation"; -import { type FC } from "react"; +import type { FC } from "react"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; -import { OAUTH_CONNECTORS, CRAWLERS, OTHER_CONNECTORS } from "../constants/connector-constants"; import { ConnectorCard } from "../components/connector-card"; +import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; interface AllConnectorsTabProps { @@ -39,8 +38,6 @@ export const AllConnectorsTab: FC = ({ onCreateYouTubeCrawler, onManage, }) => { - const router = useRouter(); - // Helper to find active task for a connector const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => { if (!logsSummary?.active_tasks) return undefined; @@ -74,98 +71,46 @@ export const AllConnectorsTab: FC = ({ {filteredOAuth.length > 0 && (
-

- Quick Connect -

+

Quick Connect

- {filteredOAuth.map((connector) => { - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; - // Find the actual connector object if connected - const actualConnector = isConnected && allConnectors - ? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType) - : undefined; - - const documentCount = getDocumentCountForConnector(connector.connectorType, documentTypeCounts); - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined; - - return ( - onConnectOAuth(connector)} - onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined} - /> - ); - })} -
-
- )} - - {/* Content Sources */} - {filteredCrawlers.length > 0 && ( -
-
-

- Content Sources -

-
-
- {filteredCrawlers.map((crawler) => { - const isYouTube = crawler.id === "youtube-crawler"; - const isWebcrawler = crawler.id === "webcrawler-connector"; - - // For crawlers that are actual connectors, check connection status - const isConnected = crawler.connectorType - ? connectedTypes.has(crawler.connectorType) - : false; - const isConnecting = connectingId === crawler.id; - + {filteredOAuth.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; // Find the actual connector object if connected - const actualConnector = isConnected && crawler.connectorType && allConnectors - ? allConnectors.find((c: SearchSourceConnector) => c.connector_type === crawler.connectorType) - : undefined; + const actualConnector = + isConnected && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => c.connector_type === connector.connectorType + ) + : undefined; - const documentCount = crawler.connectorType - ? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts) - : undefined; + const documentCount = getDocumentCountForConnector( + connector.connectorType, + documentTypeCounts + ); const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined; - - const handleConnect = isYouTube && onCreateYouTubeCrawler - ? onCreateYouTubeCrawler - : isWebcrawler && onCreateWebcrawler - ? onCreateWebcrawler - : crawler.connectorType && onConnectNonOAuth - ? () => onConnectNonOAuth(crawler.connectorType!) - : crawler.connectorType - ? () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${crawler.id}`) - : () => {}; // Fallback for non-connector crawlers + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; return ( onManage(actualConnector) : undefined} + onConnect={() => onConnectOAuth(connector)} + onManage={ + actualConnector && onManage ? () => onManage(actualConnector) : undefined + } /> ); })} @@ -177,70 +122,127 @@ export const AllConnectorsTab: FC = ({ {filteredOther.length > 0 && (
-

- More Integrations -

+

More Integrations

{filteredOther.map((connector) => { - // Special handling for connectors that can be created in popup - const isWebcrawler = connector.id === "webcrawler-connector"; - const isTavily = connector.id === "tavily-api"; - const isSearxng = connector.id === "searxng"; - const isLinkup = connector.id === "linkup-api"; - const isBaidu = connector.id === "baidu-search-api"; - const isLinear = connector.id === "linear-connector"; - const isElasticsearch = connector.id === "elasticsearch-connector"; - const isSlack = connector.id === "slack-connector"; - const isDiscord = connector.id === "discord-connector"; - const isNotion = connector.id === "notion-connector"; - const isConfluence = connector.id === "confluence-connector"; - const isBookStack = connector.id === "bookstack-connector"; - const isGithub = connector.id === "github-connector"; - const isJira = connector.id === "jira-connector"; - const isClickUp = connector.id === "clickup-connector"; - const isLuma = connector.id === "luma-connector"; - const isCircleback = connector.id === "circleback-connector"; - - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; - - // Find the actual connector object if connected - const actualConnector = isConnected && allConnectors - ? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType) - : undefined; + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; - const documentCount = getDocumentCountForConnector(connector.connectorType, documentTypeCounts); - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined; + // Find the actual connector object if connected + const actualConnector = + isConnected && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => c.connector_type === connector.connectorType + ) + : undefined; - const handleConnect = isWebcrawler && onCreateWebcrawler - ? onCreateWebcrawler - : (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth - ? () => onConnectNonOAuth(connector.connectorType) - : () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`); + const documentCount = getDocumentCountForConnector( + connector.connectorType, + documentTypeCounts + ); + const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; - return ( - onManage(actualConnector) : undefined} - /> - ); - })} + const handleConnect = onConnectNonOAuth + ? () => onConnectNonOAuth(connector.connectorType) + : () => {}; // Fallback - connector popup should handle all connector types + + return ( + onManage(actualConnector) : undefined + } + /> + ); + })} +
+
+ )} + + {/* Content Sources */} + {filteredCrawlers.length > 0 && ( +
+
+

Content Sources

+
+
+ {filteredCrawlers.map((crawler) => { + const isYouTube = crawler.id === "youtube-crawler"; + const isWebcrawler = crawler.id === "webcrawler-connector"; + + // For crawlers that are actual connectors, check connection status + const isConnected = crawler.connectorType + ? connectedTypes.has(crawler.connectorType) + : false; + const isConnecting = connectingId === crawler.id; + + // Find the actual connector object if connected + const actualConnector = + isConnected && crawler.connectorType && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => c.connector_type === crawler.connectorType + ) + : undefined; + + const documentCount = crawler.connectorType + ? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts) + : undefined; + const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; + + const handleConnect = + isYouTube && onCreateYouTubeCrawler + ? onCreateYouTubeCrawler + : isWebcrawler && onCreateWebcrawler + ? onCreateWebcrawler + : crawler.connectorType && onConnectNonOAuth + ? () => { + if (crawler.connectorType) { + onConnectNonOAuth(crawler.connectorType); + } + } + : () => {}; // Fallback for non-connector crawlers + + return ( + onManage(actualConnector) : undefined + } + /> + ); + })}
)}
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/utils/connector-document-mapping.ts b/surfsense_web/components/assistant-ui/connector-popup/utils/connector-document-mapping.ts index 607b6608f..a0b271eb6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/utils/connector-document-mapping.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/utils/connector-document-mapping.ts @@ -2,7 +2,7 @@ /** * Maps SearchSourceConnectorType to DocumentType for fetching document counts - * + * * Note: Some connectors don't have a direct 1:1 mapping to document types: * - Search API connectors (TAVILY_API, SEARXNG_API, etc.) don't index documents * - WEBCRAWLER_CONNECTOR maps to CRAWLED_URL document type @@ -35,9 +35,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record = { * Get the document type for a given connector type * Returns undefined if the connector doesn't index documents (e.g., search APIs) */ -export function getDocumentTypeForConnector( - connectorType: string -): string | undefined { +export function getDocumentTypeForConnector(connectorType: string): string | undefined { return CONNECTOR_TO_DOCUMENT_TYPE[connectorType]; } @@ -62,4 +60,3 @@ export function getDocumentCountForConnector( export function isIndexableConnectorType(connectorType: string): boolean { return connectorType in CONNECTOR_TO_DOCUMENT_TYPE; } - diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx index 7f5ad5d2b..2471c39bb 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx @@ -22,10 +22,7 @@ interface YouTubeCrawlerViewProps { onBack: () => void; } -export const YouTubeCrawlerView: FC = ({ - searchSpaceId, - onBack, -}) => { +export const YouTubeCrawlerView: FC = ({ searchSpaceId, onBack }) => { const t = useTranslations("add_youtube"); const router = useRouter(); const [videoTags, setVideoTags] = useState([]); @@ -133,12 +130,8 @@ export const YouTubeCrawlerView: FC = ({ {getConnectorIcon(EnumConnectorName.YOUTUBE_CONNECTOR, "h-7 w-7")}
-

- {t("title")} -

-

- {t("subtitle")} -

+

{t("title")}

+

{t("subtitle")}

@@ -159,7 +152,8 @@ export const YouTubeCrawlerView: FC = ({ styleClasses={{ inlineTagsContainer: "border border-slate-400/20 rounded-lg bg-muted/50 shadow-sm shadow-black/5 transition-shadow focus-within:border-slate-400/40 focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1", - input: "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent", + input: + "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent", tag: { body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex", closeButton: @@ -172,11 +166,7 @@ export const YouTubeCrawlerView: FC = ({

{t("hint")}

- {error && ( -
- {error} -
- )} + {error &&
{error}
}

{t("tips_title")}

@@ -244,4 +234,3 @@ export const YouTubeCrawlerView: FC = ({
); }; - diff --git a/surfsense_web/components/assistant-ui/document-upload-popup.tsx b/surfsense_web/components/assistant-ui/document-upload-popup.tsx new file mode 100644 index 000000000..f522b57dc --- /dev/null +++ b/surfsense_web/components/assistant-ui/document-upload-popup.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useAtomValue } from "jotai"; +import { + type FC, + createContext, + useContext, + useState, + useCallback, + useRef, + type ReactNode, +} from "react"; +import { useRouter } from "next/navigation"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab"; + +// Context for opening the dialog from anywhere +interface DocumentUploadDialogContextType { + openDialog: () => void; + closeDialog: () => void; +} + +const DocumentUploadDialogContext = createContext(null); + +export const useDocumentUploadDialog = () => { + const context = useContext(DocumentUploadDialogContext); + if (!context) { + throw new Error("useDocumentUploadDialog must be used within DocumentUploadDialogProvider"); + } + return context; +}; + +// Provider component +export const DocumentUploadDialogProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const isClosingRef = useRef(false); + + const openDialog = useCallback(() => { + // Prevent opening if we just closed (debounce) + if (isClosingRef.current) { + return; + } + setIsOpen(true); + }, []); + + const closeDialog = useCallback(() => { + isClosingRef.current = true; + setIsOpen(false); + // Reset the flag after a short delay to allow for file picker to close + setTimeout(() => { + isClosingRef.current = false; + }, 300); + }, []); + + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open) { + // Only close if not already in closing state + if (!isClosingRef.current) { + closeDialog(); + } + } else { + // Only open if not in the middle of closing + if (!isClosingRef.current) { + setIsOpen(true); + } + } + }, + [closeDialog] + ); + + return ( + + {children} + + + ); +}; + +// Internal component that renders the dialog +const DocumentUploadPopupContent: FC<{ + isOpen: boolean; + onOpenChange: (open: boolean) => void; +}> = ({ isOpen, onOpenChange }) => { + const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); + const router = useRouter(); + + if (!searchSpaceId) return null; + + const handleSuccess = () => { + onOpenChange(false); + router.push(`/dashboard/${searchSpaceId}/documents`); + }; + + return ( + + +
+
+
+ +
+
+ {/* Bottom fade shadow */} +
+
+ +
+ ); +}; diff --git a/surfsense_web/components/assistant-ui/edit-composer.tsx b/surfsense_web/components/assistant-ui/edit-composer.tsx index 4e6346909..e2714661e 100644 --- a/surfsense_web/components/assistant-ui/edit-composer.tsx +++ b/surfsense_web/components/assistant-ui/edit-composer.tsx @@ -24,4 +24,3 @@ export const EditComposer: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/thinking-steps.tsx b/surfsense_web/components/assistant-ui/thinking-steps.tsx index f0cf4a7c1..bbd4fca71 100644 --- a/surfsense_web/components/assistant-ui/thinking-steps.tsx +++ b/surfsense_web/components/assistant-ui/thinking-steps.tsx @@ -204,4 +204,3 @@ export const ThinkingStepsScrollHandler: FC = () => { return null; // This component doesn't render anything }; - diff --git a/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx b/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx index 6f641615e..79fee1850 100644 --- a/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx +++ b/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx @@ -16,4 +16,3 @@ export const ThreadScrollToBottom: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/thread-welcome.tsx b/surfsense_web/components/assistant-ui/thread-welcome.tsx index b5e4bbac0..c101a5958 100644 --- a/surfsense_web/components/assistant-ui/thread-welcome.tsx +++ b/surfsense_web/components/assistant-ui/thread-welcome.tsx @@ -69,4 +69,3 @@ export const ThreadWelcome: FC = () => {
); }; - diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 713a9af1c..ff61b8182 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -26,15 +26,7 @@ import { SquareIcon, } from "lucide-react"; import { useParams } from "next/navigation"; -import { - type FC, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { mentionedDocumentIdsAtom, diff --git a/surfsense_web/components/assistant-ui/user-message.tsx b/surfsense_web/components/assistant-ui/user-message.tsx index fbbcf42bf..dcf626461 100644 --- a/surfsense_web/components/assistant-ui/user-message.tsx +++ b/surfsense_web/components/assistant-ui/user-message.tsx @@ -70,4 +70,3 @@ const UserActionBar: FC = () => { ); }; - diff --git a/surfsense_web/components/connectors/google-drive-folder-tree.tsx b/surfsense_web/components/connectors/google-drive-folder-tree.tsx index bad2b5965..8347d9cce 100644 --- a/surfsense_web/components/connectors/google-drive-folder-tree.tsx +++ b/surfsense_web/components/connectors/google-drive-folder-tree.tsx @@ -223,9 +223,17 @@ export function GoogleDriveFolderTree({ const childFiles = children?.filter((c) => !c.isFolder) || []; const indentSize = 0.75; // Smaller indent for mobile - + return ( -
+
e.stopPropagation()} /> - {isFolder && ( - toggleFolderSelection(item.id, item.name)} - className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4" - onClick={(e) => e.stopPropagation()} - /> - )}
{isFolder ? ( @@ -310,7 +310,9 @@ export function GoogleDriveFolderTree({ {childFiles.map((child) => renderItem(child, level + 1))} {children.length === 0 && ( -
Empty folder
+
+ Empty folder +
)}
)} @@ -319,15 +321,15 @@ export function GoogleDriveFolderTree({ }; return ( -
+
-
+
toggleFolderSelection("root", "My Drive")} - className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4" + className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20" />
- -
+ +
{files.map((file, index) => (
@@ -329,7 +306,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {