diff --git a/surfsense_web/components/chat/ChatInputGroup.tsx b/surfsense_web/components/chat/ChatInputGroup.tsx index 09c759e62..dc7e6a4f4 100644 --- a/surfsense_web/components/chat/ChatInputGroup.tsx +++ b/surfsense_web/components/chat/ChatInputGroup.tsx @@ -4,7 +4,6 @@ import { ChatInput } from "@llamaindex/chat-ui"; import { Brain, Check, FolderOpen, Zap } from "lucide-react"; import { useParams } from "next/navigation"; import React, { Suspense, useCallback, useState } from "react"; -import { ConnectorButton as ConnectorButtonComponent } from "@/components/chat/ConnectorComponents"; import { DocumentsDataTable } from "@/components/chat/DocumentsDataTable"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -24,9 +23,9 @@ import { SelectValue, } from "@/components/ui/select"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import { useDocumentTypes } from "@/hooks/use-document-types"; import type { Document } from "@/hooks/use-documents"; import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; -import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; const DocumentSelector = React.memo( ({ @@ -59,22 +58,31 @@ const DocumentSelector = React.memo( return ( -
-
- Select Documents - - Choose documents to include in your research context +
+ + Select Documents + + + Choose specific documents to include in your research context
@@ -106,17 +114,19 @@ const ConnectorSelector = React.memo( const { search_space_id } = useParams(); const [isOpen, setIsOpen] = useState(false); - const { connectorSourceItems, isLoading, isLoaded, fetchConnectors } = - useSearchSourceConnectors(true, Number(search_space_id)); + const { documentTypes, isLoading, isLoaded, fetchDocumentTypes } = useDocumentTypes( + Number(search_space_id), + true + ); const handleOpenChange = useCallback( (open: boolean) => { setIsOpen(open); if (open && !isLoaded) { - fetchConnectors(Number(search_space_id)); + fetchDocumentTypes(Number(search_space_id)); } }, - [fetchConnectors, isLoaded, search_space_id] + [fetchDocumentTypes, isLoaded, search_space_id] ); const handleConnectorToggle = useCallback( @@ -131,64 +141,152 @@ const ConnectorSelector = React.memo( ); const handleSelectAll = useCallback(() => { - onSelectionChange?.(connectorSourceItems.map((c) => c.type)); - }, [connectorSourceItems, onSelectionChange]); + onSelectionChange?.(documentTypes.map((dt) => dt.type)); + }, [documentTypes, onSelectionChange]); const handleClearAll = useCallback(() => { onSelectionChange?.([]); }, [onSelectionChange]); + // Get display name for document type + const getDisplayName = (type: string) => { + return type + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" "); + }; + + // Get selected document types with their counts + const selectedDocTypes = documentTypes.filter((dt) => selectedConnectors.includes(dt.type)); + return ( - setIsOpen(true)} - connectorSources={connectorSourceItems} - /> + - - Select Connectors - - Choose which data sources to include in your research - + +
+
+ Select Document Types + + Choose which document types to include in your search + +
- {/* Connector selection grid */} -
- {isLoading ? ( -
-
-
- ) : ( - connectorSourceItems.map((connector) => { - const isSelected = selectedConnectors.includes(connector.type); + {/* Document type selection grid */} +
+ {isLoading ? ( +
+
+
+ ) : documentTypes.length === 0 ? ( +
+
+ +
+

No documents found

+

+ Add documents to this search space to enable filtering by type +

+
+ ) : ( + documentTypes.map((docType) => { + const isSelected = selectedConnectors.includes(docType.type); - return ( - - ); - }) + return ( + + ); + }) + )} +
+ + {documentTypes.length > 0 && ( + + + + )}
- - -
- - -
-
); @@ -214,26 +312,33 @@ const SearchModeSelector = React.memo( }, [onSearchModeChange]); return ( -
- Scope: -
- - +
); @@ -414,25 +519,29 @@ const CustomChatInputOptions = React.memo( }) => { // Memoize the loading fallback to prevent recreation const loadingFallback = React.useMemo( - () =>
, + () =>
, [] ); return ( -
- - - - - - +
+
+ + + + + + +
+
+
); diff --git a/surfsense_web/hooks/use-document-types.ts b/surfsense_web/hooks/use-document-types.ts new file mode 100644 index 000000000..f00cc05de --- /dev/null +++ b/surfsense_web/hooks/use-document-types.ts @@ -0,0 +1,97 @@ +import { useCallback, useEffect, useState } from "react"; + +export interface DocumentTypeCount { + type: string; + count: number; +} + +/** + * Hook to fetch document type counts from the API + * @param searchSpaceId - The search space ID to filter document types + * @param lazy - If true, types won't be fetched on mount + */ +export const useDocumentTypes = (searchSpaceId?: number, lazy: boolean = false) => { + const [documentTypes, setDocumentTypes] = useState([]); + const [isLoading, setIsLoading] = useState(!lazy); + const [isLoaded, setIsLoaded] = useState(false); + const [error, setError] = useState(null); + + const fetchDocumentTypes = useCallback( + async (spaceId?: number) => { + if (isLoaded && lazy) return; + + try { + setIsLoading(true); + setError(null); + const token = localStorage.getItem("surfsense_bearer_token"); + + if (!token) { + throw new Error("No authentication token found"); + } + + // Build URL with optional search_space_id query parameter + const url = new URL( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/type-counts/` + ); + if (spaceId !== undefined) { + url.searchParams.append("search_space_id", spaceId.toString()); + } + + const response = await fetch(url.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch document types: ${response.statusText}`); + } + + const data = await response.json(); + + // Convert the object to an array of DocumentTypeCount + const typeCounts: DocumentTypeCount[] = Object.entries(data).map(([type, count]) => ({ + type, + count: count as number, + })); + + setDocumentTypes(typeCounts); + setIsLoaded(true); + + return typeCounts; + } catch (err) { + setError(err instanceof Error ? err : new Error("An unknown error occurred")); + console.error("Error fetching document types:", err); + } finally { + setIsLoading(false); + } + }, + [isLoaded, lazy] + ); + + useEffect(() => { + if (!lazy) { + fetchDocumentTypes(searchSpaceId); + } + }, [lazy, fetchDocumentTypes, searchSpaceId]); + + // Function to refresh the document types + const refreshDocumentTypes = useCallback( + async (spaceId?: number) => { + setIsLoaded(false); + await fetchDocumentTypes(spaceId !== undefined ? spaceId : searchSpaceId); + }, + [fetchDocumentTypes, searchSpaceId] + ); + + return { + documentTypes, + isLoading, + isLoaded, + error, + fetchDocumentTypes, + refreshDocumentTypes, + }; +};