diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx index 651307488..9fea40394 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useRef, useEffect, useState } from 'react'; +import React, { useRef, useEffect, useState, useMemo, useCallback } from 'react'; import { useChat } from '@ai-sdk/react'; import { useParams } from 'next/navigation'; import { @@ -20,7 +20,9 @@ import { Globe, Webhook, FolderOpen, - Upload + Upload, + ChevronDown, + Filter } from 'lucide-react'; import { IconBrandDiscord, IconBrandGithub, IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconLayoutKanban } from "@tabler/icons-react"; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -36,6 +38,16 @@ import { DialogTrigger, DialogFooter } from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; import { ConnectorButton as ConnectorButtonComponent, getConnectorIcon, @@ -95,6 +107,97 @@ const documentTypeIcons = { DISCORD_CONNECTOR: IconBrandDiscord, } as const; +/** + * Skeleton loader for document items + */ +const DocumentSkeleton = () => ( +
+ +
+ + + +
+ +
+); + +/** + * Enhanced document type filter dropdown + */ +const DocumentTypeFilter = ({ + value, + onChange, + counts +}: { + value: DocumentType | "ALL"; + onChange: (value: DocumentType | "ALL") => void; + counts: Record; +}) => { + const getTypeLabel = (type: DocumentType | "ALL") => { + if (type === "ALL") return "All Types"; + return type.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase()); + }; + + const getTypeIcon = (type: DocumentType | "ALL") => { + switch (type) { + case "ALL": + return ; + case "EXTENSION": + return ; + case "CRAWLED_URL": + return ; + case "FILE": + return ; + case "SLACK_CONNECTOR": + return ; + case "NOTION_CONNECTOR": + return ; + case "YOUTUBE_VIDEO": + return ; + case "GITHUB_CONNECTOR": + return ; + case "LINEAR_CONNECTOR": + return ; + case "DISCORD_CONNECTOR": + return ; + default: + return ; + } + }; + + return ( + + + + + + Document Types + + {Object.entries(counts).map(([type, count]) => ( + onChange(type as DocumentType | "ALL")} + className="flex items-center justify-between" + > +
+ {getTypeIcon(type as DocumentType | "ALL")} + {getTypeLabel(type as DocumentType | "ALL")} +
+ + {count} + +
+ ))} +
+
+ ); +}; + /** * Button that displays selected connectors and opens connector selection dialog */ @@ -327,8 +430,70 @@ const ChatPage = () => { // Document selection state const [selectedDocuments, setSelectedDocuments] = useState([]); const [documentFilter, setDocumentFilter] = useState(""); + const [debouncedDocumentFilter, setDebouncedDocumentFilter] = useState(""); + const [documentTypeFilter, setDocumentTypeFilter] = useState("ALL"); + const [documentsPage, setDocumentsPage] = useState(1); + const [documentsPerPage] = useState(10); const { documents, loading: isLoadingDocuments, error: documentsError } = useDocuments(Number(search_space_id)); + // Custom hook for debounced search + const useDebounce = (value: string, delay: number) => { + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedDocumentFilter(value); + setDocumentsPage(1); // Reset page when search changes + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedDocumentFilter; + }; + + // Use debounced search + useDebounce(documentFilter, 300); + + // Memoized filtered and paginated documents + const filteredDocuments = useMemo(() => { + if (!documents) return []; + + return documents.filter(doc => { + const matchesSearch = doc.title.toLowerCase().includes(debouncedDocumentFilter.toLowerCase()) || + doc.content.toLowerCase().includes(debouncedDocumentFilter.toLowerCase()); + const matchesType = documentTypeFilter === "ALL" || doc.document_type === documentTypeFilter; + return matchesSearch && matchesType; + }); + }, [documents, debouncedDocumentFilter, documentTypeFilter]); + + const paginatedDocuments = useMemo(() => { + const startIndex = (documentsPage - 1) * documentsPerPage; + return filteredDocuments.slice(startIndex, startIndex + documentsPerPage); + }, [filteredDocuments, documentsPage, documentsPerPage]); + + const totalPages = Math.ceil(filteredDocuments.length / documentsPerPage); + + // Document type counts for filter dropdown + const documentTypeCounts = useMemo(() => { + if (!documents) return {}; + + const counts: Record = { ALL: documents.length }; + documents.forEach(doc => { + counts[doc.document_type] = (counts[doc.document_type] || 0) + 1; + }); + return counts; + }, [documents]); + + // Callback to handle document selection + const handleDocumentToggle = useCallback((documentId: number) => { + setSelectedDocuments(prev => + prev.includes(documentId) + ? prev.filter(id => id !== documentId) + : [...prev, documentId] + ); + }, []); + // Function to scroll terminal to bottom const scrollTerminalToBottom = () => { if (terminalMessagesRef.current) { @@ -874,7 +1039,7 @@ const ChatPage = () => { // Use these message-specific sources for the Tabs component return ( 0 ? messageConnectorSources[0].type : "CRAWLED_URL"} + defaultValue={messageConnectorSources.length > 0 ? messageConnectorSources[0].type : undefined} className="w-full" >
@@ -1068,7 +1233,7 @@ const ChatPage = () => {
- {/* Document Selection Dialog */} + {/* Enhanced Document Selection Dialog */} { documentsCount={documents?.length || 0} /> - - + + - Select Documents +
+ + Select Documents + + {selectedDocuments.length} selected + +
- Choose documents to include in your research context + Choose documents to include in your research context. Use filters and search to find specific documents.
- {/* Document Search */} -
- - setDocumentFilter(e.target.value)} - /> - {documentFilter && ( - - )} -
- - {/* Document List */} -
- {isLoadingDocuments ? ( -
- + {/* Enhanced Search and Filter Controls */} +
+
+ {/* Search Input */} +
+ + setDocumentFilter(e.target.value)} + /> + {documentFilter && ( + + )}
- ) : documentsError ? ( -
-

Error loading documents

-
- ) : ( - (() => { - const filteredDocuments = documents?.filter(doc => - doc.title.toLowerCase().includes(documentFilter.toLowerCase()) - ) || []; - if (filteredDocuments.length === 0) { - return ( -
- -

{documentFilter ? `No documents found matching "${documentFilter}"` : 'No documents available'}

-
- ); - } - - return filteredDocuments.map((document) => { - const Icon = documentTypeIcons[document.document_type]; - const isSelected = selectedDocuments.includes(document.id); - - return ( -
{ - setSelectedDocuments(prev => - isSelected - ? prev.filter(id => id !== document.id) - : [...prev, document.id] - ); - }} - > -
- -
-
-

{document.title}

-

- {document.document_type.replace(/_/g, ' ').toLowerCase()} - {' • '} - {new Date(document.created_at).toLocaleDateString()} -

-

- {document.content.substring(0, 150)}... -

-
- {isSelected && ( -
- -
- )} -
- ); - }); - })() - )} -
- - -
- {selectedDocuments.length} document{selectedDocuments.length !== 1 ? 's' : ''} selected + {/* Document Type Filter */} +
-
+ + {/* Results Summary */} +
+ + {isLoadingDocuments ? ( + "Loading documents..." + ) : ( + `Showing ${paginatedDocuments.length} of ${filteredDocuments.length} documents` + )} + + {filteredDocuments.length > 0 && ( + + Page {documentsPage} of {totalPages} + + )} +
+
+ + {/* Document List with Proper Scrolling */} +
+
+ {isLoadingDocuments ? ( + // Enhanced skeleton loading + Array.from({ length: 6 }, (_, i) => ( + + )) + ) : documentsError ? ( +
+
+ +
+

Error loading documents

+

Please try refreshing the page

+
+ ) : filteredDocuments.length === 0 ? ( +
+
+ +
+

No documents found

+

+ {documentFilter || documentTypeFilter !== "ALL" + ? "Try adjusting your search or filters" + : "Upload documents to get started"} +

+ {(!documentFilter && documentTypeFilter === "ALL") && ( + + )} +
+ ) : ( + // Enhanced document list + paginatedDocuments.map((document) => { + const isSelected = selectedDocuments.includes(document.id); + const typeLabel = document.document_type.replace(/_/g, ' ').toLowerCase(); + + return ( +
handleDocumentToggle(document.id)} + > +
+ {(() => { + const iconClassName = `${isSelected ? 'text-primary' : 'text-muted-foreground'} transition-colors`; + const iconSize = { className: iconClassName }; + + if (document.document_type === "EXTENSION") return ; + if (document.document_type === "CRAWLED_URL") return ; + if (document.document_type === "FILE") return ; + if (document.document_type === "SLACK_CONNECTOR") return ; + if (document.document_type === "NOTION_CONNECTOR") return ; + if (document.document_type === "YOUTUBE_VIDEO") return ; + if (document.document_type === "GITHUB_CONNECTOR") return ; + if (document.document_type === "LINEAR_CONNECTOR") return ; + if (document.document_type === "DISCORD_CONNECTOR") return ; + return ; + })()} +
+
+
+

+ {document.title} +

+ {isSelected && ( +
+
+ +
+
+ )} +
+
+ + {typeLabel} + + + {new Date(document.created_at).toLocaleDateString()} + +
+

+ {document.content.substring(0, 200)}... +

+
+
+ ); + }) + )} +
+
+ + {/* Enhanced Pagination Controls */} + {totalPages > 1 && ( +
+
+ +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + const page = documentsPage <= 3 ? i + 1 : documentsPage - 2 + i; + if (page > totalPages) return null; + return ( + + ); + })} + {totalPages > 5 && documentsPage < totalPages - 2 && ( + <> + ... + + + )} +
+ +
+
+ )} + + {/* Enhanced Footer */} + +
+ + {selectedDocuments.length} of {filteredDocuments.length} document{selectedDocuments.length !== 1 ? 's' : ''} selected + +
+
+
diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx index cde59810f..5e1808278 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx @@ -31,7 +31,7 @@ const ResearcherPage = () => { body: JSON.stringify({ type: "QNA", title: "Untitled Chat", // Empty title initially - initial_connectors: ["CRAWLED_URL"], // Default connector + initial_connectors: [], // No default connectors messages: [], search_space_id: Number(search_space_id) })