From b35656985459a5d395b80e013ca7aff5c52244fe Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 5 Jun 2025 23:10:23 -0700 Subject: [PATCH 1/4] fix(ui): Improved Chat Document Selector Dialog. --- .../researcher/[chat_id]/page.tsx | 530 ++++++++++++++---- .../[search_space_id]/researcher/page.tsx | 2 +- 2 files changed, 423 insertions(+), 109 deletions(-) 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 6513074..9fea403 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 cde5981..5e18082 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) }) From 57c9bcbccba692875bc3a5bcb58f2fef8d15e500 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 5 Jun 2025 23:20:48 -0700 Subject: [PATCH 2/4] refactor: streamline debounced search implementation and enhance document type filter behavior --- .../researcher/[chat_id]/page.tsx | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) 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 9fea403..6be175e 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 @@ -436,24 +436,17 @@ const ChatPage = () => { 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); + // Debounced search effect (proper implementation) + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedDocumentFilter(documentFilter); + setDocumentsPage(1); // Reset page when search changes + }, 300); - return () => { - clearTimeout(handler); - }; - }, [value, delay]); - - return debouncedDocumentFilter; - }; - - // Use debounced search - useDebounce(documentFilter, 300); + return () => { + clearTimeout(handler); + }; + }, [documentFilter]); // Memoized filtered and paginated documents const filteredDocuments = useMemo(() => { @@ -1291,12 +1284,15 @@ const ChatPage = () => { )}
- {/* Document Type Filter */} - + {/* Document Type Filter */} + { + setDocumentTypeFilter(newType); + setDocumentsPage(1); // Reset to page 1 when filter changes + }} + counts={documentTypeCounts} + />
{/* Results Summary */} From 21c0e249a834a503650a18d70683547e5efc4421 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 5 Jun 2025 23:25:21 -0700 Subject: [PATCH 3/4] refactor: remove unused Document interface and associated icon mapping --- .../researcher/[chat_id]/page.tsx | 22 ------------------- 1 file changed, 22 deletions(-) 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 6be175e..7ffb31d 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 @@ -84,28 +84,6 @@ interface ConnectorSource { type DocumentType = "EXTENSION" | "CRAWLED_URL" | "SLACK_CONNECTOR" | "NOTION_CONNECTOR" | "FILE" | "YOUTUBE_VIDEO" | "GITHUB_CONNECTOR" | "LINEAR_CONNECTOR" | "DISCORD_CONNECTOR"; -interface Document { - id: number; - title: string; - document_type: DocumentType; - document_metadata: any; - content: string; - created_at: string; - search_space_id: number; -} - -// Document type icons mapping -const documentTypeIcons = { - EXTENSION: Webhook, - CRAWLED_URL: Globe, - SLACK_CONNECTOR: IconBrandSlack, - NOTION_CONNECTOR: IconBrandNotion, - FILE: File, - YOUTUBE_VIDEO: IconBrandYoutube, - GITHUB_CONNECTOR: IconBrandGithub, - LINEAR_CONNECTOR: IconLayoutKanban, - DISCORD_CONNECTOR: IconBrandDiscord, -} as const; /** * Skeleton loader for document items From 284eedde20dfa45f0a8575176759180923ba03de Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 5 Jun 2025 23:30:31 -0700 Subject: [PATCH 4/4] refactor: simplify document type icon rendering by consolidating logic into a single function --- .../researcher/[chat_id]/page.tsx | 48 ++----------------- 1 file changed, 5 insertions(+), 43 deletions(-) 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 7ffb31d..a9ebd9c 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 @@ -16,15 +16,11 @@ import { SendHorizontal, FileText, Grid3x3, - File, - Globe, - Webhook, FolderOpen, 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'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -118,30 +114,8 @@ const DocumentTypeFilter = ({ }; 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 ; - } + if (type === "ALL") return ; + return getConnectorIcon(type); }; return ( @@ -1345,21 +1319,9 @@ const ChatPage = () => { onClick={() => 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 ; - })()} +
+ {getConnectorIcon(document.document_type)} +