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 8a0bde74f..4d4aa74e0 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,77 +1,77 @@ "use client"; import React, { - useRef, - useEffect, - useState, - useMemo, - useCallback, + useRef, + useEffect, + useState, + useMemo, + useCallback, } from "react"; import { useChat } from "@ai-sdk/react"; import { useParams } from "next/navigation"; import { - Loader2, - X, - Search, - ExternalLink, - ChevronLeft, - ChevronRight, - Check, - ArrowDown, - CircleUser, - Database, - SendHorizontal, - FileText, - Grid3x3, - FolderOpen, - Upload, - ChevronDown, - Filter, - Brain, - Zap, + Loader2, + X, + Search, + ExternalLink, + ChevronLeft, + ChevronRight, + Check, + ArrowDown, + CircleUser, + Database, + SendHorizontal, + FileText, + Grid3x3, + FolderOpen, + Upload, + ChevronDown, + Filter, + Brain, + Zap, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, - DialogFooter, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter, } from "@/components/ui/dialog"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { - ConnectorButton as ConnectorButtonComponent, - getConnectorIcon, - getFilteredSources as getFilteredSourcesUtil, - getPaginatedDialogSources as getPaginatedDialogSourcesUtil, - useScrollToBottom, - updateScrollIndicators as updateScrollIndicatorsUtil, - useScrollIndicators, - scrollTabsLeft as scrollTabsLeftUtil, - scrollTabsRight as scrollTabsRightUtil, - Source, - ResearchMode, - ResearchModeControl, + ConnectorButton as ConnectorButtonComponent, + getConnectorIcon, + getFilteredSources as getFilteredSourcesUtil, + getPaginatedDialogSources as getPaginatedDialogSourcesUtil, + useScrollToBottom, + updateScrollIndicators as updateScrollIndicatorsUtil, + useScrollIndicators, + scrollTabsLeft as scrollTabsLeftUtil, + scrollTabsRight as scrollTabsRightUtil, + Source, + ResearchMode, + ResearchModeControl, } from "@/components/chat"; import { MarkdownViewer } from "@/components/markdown-viewer"; import { Logo } from "@/components/Logo"; @@ -80,446 +80,467 @@ import { useDocuments } from "@/hooks/use-documents"; import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; interface SourceItem { - id: number; - title: string; - description: string; - url: string; - connectorType?: string; + id: number; + title: string; + description: string; + url: string; + connectorType?: string; } interface ConnectorSource { - id: number; - name: string; - type: string; - sources: SourceItem[]; + id: number; + name: string; + type: string; + sources: SourceItem[]; } type DocumentType = - | "EXTENSION" - | "CRAWLED_URL" - | "SLACK_CONNECTOR" - | "NOTION_CONNECTOR" - | "FILE" - | "YOUTUBE_VIDEO" - | "GITHUB_CONNECTOR" - | "LINEAR_CONNECTOR" - | "DISCORD_CONNECTOR"; + | "EXTENSION" + | "CRAWLED_URL" + | "SLACK_CONNECTOR" + | "NOTION_CONNECTOR" + | "FILE" + | "YOUTUBE_VIDEO" + | "GITHUB_CONNECTOR" + | "LINEAR_CONNECTOR" + | "DISCORD_CONNECTOR"; /** * Skeleton loader for document items */ const DocumentSkeleton = () => ( -
- -
- - - -
- -
+
+ +
+ + + +
+ +
); /** * Enhanced document type filter dropdown */ const DocumentTypeFilter = ({ - value, - onChange, - counts, + value, + onChange, + counts, }: { - value: DocumentType | "ALL"; - onChange: (value: DocumentType | "ALL") => void; - counts: Record; + 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 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") => { - if (type === "ALL") return ; - return getConnectorIcon(type); - }; + const getTypeIcon = (type: DocumentType | "ALL") => { + if (type === "ALL") return ; + return getConnectorIcon(type); + }; - 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} - -
- ))} -
-
- ); + 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 */ const ConnectorButton = ({ - selectedConnectors, - onClick, + selectedConnectors, + onClick, }: { - selectedConnectors: string[]; - onClick: () => void; + selectedConnectors: string[]; + onClick: () => void; }) => { - const { connectorSourceItems } = useSearchSourceConnectors(); + const { connectorSourceItems } = useSearchSourceConnectors(); - return ( - - ); + return ( + + ); }; /** * Button that displays selected documents count and opens document selection dialog */ const DocumentSelectorButton = ({ - selectedDocuments, - onClick, - documentsCount, + selectedDocuments, + onClick, + documentsCount, }: { - selectedDocuments: number[]; - onClick: () => void; - documentsCount: number; + selectedDocuments: number[]; + onClick: () => void; + documentsCount: number; }) => { - return ( -
- - {selectedDocuments.length > 0 && ( - - {selectedDocuments.length > 99 ? "99+" : selectedDocuments.length} - - )} - {selectedDocuments.length === 0 && ( - - 0 - - )} -
- ); + return ( +
+ + {selectedDocuments.length > 0 && ( + + {selectedDocuments.length > 99 + ? "99+" + : selectedDocuments.length} + + )} + {selectedDocuments.length === 0 && ( + + 0 + + )} +
+ ); }; // Create a wrapper component for the sources dialog content const SourcesDialogContent = ({ - connector, - sourceFilter, - expandedSources, - sourcesPage, - setSourcesPage, - setSourceFilter, - setExpandedSources, - isLoadingMore, + connector, + sourceFilter, + expandedSources, + sourcesPage, + setSourcesPage, + setSourceFilter, + setExpandedSources, + isLoadingMore, }: { - connector: any; - sourceFilter: string; - expandedSources: boolean; - sourcesPage: number; - setSourcesPage: React.Dispatch>; - setSourceFilter: React.Dispatch>; - setExpandedSources: React.Dispatch>; - isLoadingMore: boolean; + connector: any; + sourceFilter: string; + expandedSources: boolean; + sourcesPage: number; + setSourcesPage: React.Dispatch>; + setSourceFilter: React.Dispatch>; + setExpandedSources: React.Dispatch>; + isLoadingMore: boolean; }) => { - // Safely access sources with fallbacks - const sources = connector?.sources || []; + // Safely access sources with fallbacks + const sources = connector?.sources || []; - // Safe versions of utility functions - const getFilteredSourcesSafe = () => { - if (!sources.length) return []; - return getFilteredSourcesUtil(connector, sourceFilter); - }; + // Safe versions of utility functions + const getFilteredSourcesSafe = () => { + if (!sources.length) return []; + return getFilteredSourcesUtil(connector, sourceFilter); + }; - const getPaginatedSourcesSafe = () => { - if (!sources.length) return []; - return getPaginatedDialogSourcesUtil( - connector, - sourceFilter, - expandedSources, - sourcesPage, - 5, // SOURCES_PER_PAGE - ); - }; + const getPaginatedSourcesSafe = () => { + if (!sources.length) return []; + return getPaginatedDialogSourcesUtil( + connector, + sourceFilter, + expandedSources, + sourcesPage, + 5 // SOURCES_PER_PAGE + ); + }; - const filteredSources = getFilteredSourcesSafe() || []; - const paginatedSources = getPaginatedSourcesSafe() || []; + const filteredSources = getFilteredSourcesSafe() || []; + const paginatedSources = getPaginatedSourcesSafe() || []; - // Description text - const descriptionText = sourceFilter - ? `Found ${filteredSources.length} sources matching "${sourceFilter}"` - : `Viewing ${paginatedSources.length} of ${sources.length} sources`; + // Description text + const descriptionText = sourceFilter + ? `Found ${filteredSources.length} sources matching "${sourceFilter}"` + : `Viewing ${paginatedSources.length} of ${sources.length} sources`; - if (paginatedSources.length === 0) { - return ( -
- -

No sources found matching "{sourceFilter}"

- -
- ); - } + if (paginatedSources.length === 0) { + return ( +
+ +

No sources found matching "{sourceFilter}"

+ +
+ ); + } - return ( - <> - - - {getConnectorIcon(connector.type)} - {connector.name} Sources - - - {descriptionText} - - + return ( + <> + + + {getConnectorIcon(connector.type)} + {connector.name} Sources + + + {descriptionText} + + -
- - { - setSourceFilter(e.target.value); - setSourcesPage(1); - setExpandedSources(false); - }} - /> - {sourceFilter && ( - - )} -
+
+ + { + setSourceFilter(e.target.value); + setSourcesPage(1); + setExpandedSources(false); + }} + /> + {sourceFilter && ( + + )} +
-
- {paginatedSources.map((source: any, index: number) => ( - -
-
- {getConnectorIcon(connector.type)} -
-
-

{source.title}

-

- {source.description} -

-
- -
-
- ))} +
+ {paginatedSources.map((source: any, index: number) => ( + +
+
+ {getConnectorIcon(connector.type)} +
+
+

+ {source.title} +

+

+ {source.description} +

+
+ +
+
+ ))} - {!expandedSources && - paginatedSources.length < filteredSources.length && ( - - )} + {!expandedSources && + paginatedSources.length < filteredSources.length && ( + + )} - {expandedSources && filteredSources.length > 10 && ( -
- Showing all {filteredSources.length} sources -
- )} -
- - ); + {expandedSources && filteredSources.length > 10 && ( +
+ Showing all {filteredSources.length} sources +
+ )} +
+ + ); }; const ChatPage = () => { - const [token, setToken] = React.useState(null); - const [dialogOpenId, setDialogOpenId] = useState(null); - const [sourcesPage, setSourcesPage] = useState(1); - const [expandedSources, setExpandedSources] = useState(false); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(true); - const [sourceFilter, setSourceFilter] = useState(""); - const tabsListRef = useRef(null); - const [terminalExpanded, setTerminalExpanded] = useState(false); - const [selectedConnectors, setSelectedConnectors] = useState([]); - const [searchMode, setSearchMode] = useState<"DOCUMENTS" | "CHUNKS">( - "DOCUMENTS", - ); - const [researchMode, setResearchMode] = useState("QNA"); - const [currentTime, setCurrentTime] = useState(""); - const [currentDate, setCurrentDate] = useState(""); - const terminalMessagesRef = useRef(null); - const { connectorSourceItems, isLoading: isLoadingConnectors } = - useSearchSourceConnectors(); - const { llmConfigs } = useLLMConfigs(); - const { preferences, updatePreferences } = useLLMPreferences(); + const [token, setToken] = React.useState(null); + const [dialogOpenId, setDialogOpenId] = useState(null); + const [sourcesPage, setSourcesPage] = useState(1); + const [expandedSources, setExpandedSources] = useState(false); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(true); + const [sourceFilter, setSourceFilter] = useState(""); + const tabsListRef = useRef(null); + const [terminalExpanded, setTerminalExpanded] = useState(false); + const [selectedConnectors, setSelectedConnectors] = useState([]); + const [searchMode, setSearchMode] = useState<"DOCUMENTS" | "CHUNKS">( + "DOCUMENTS" + ); + const [researchMode, setResearchMode] = useState("QNA"); + const [currentTime, setCurrentTime] = useState(""); + const [currentDate, setCurrentDate] = useState(""); + const terminalMessagesRef = useRef(null); + const { + connectorSourceItems, + isLoading: isLoadingConnectors, + isLoaded: isConnectorsLoaded, + fetchConnectors, + } = useSearchSourceConnectors(); + const { llmConfigs } = useLLMConfigs(); + const { preferences, updatePreferences } = useLLMPreferences(); - const INITIAL_SOURCES_DISPLAY = 3; + const INITIAL_SOURCES_DISPLAY = 3; - const { search_space_id, chat_id } = useParams(); + const { search_space_id, chat_id } = useParams(); - // Document selection state - const [selectedDocuments, setSelectedDocuments] = useState([]); - const [documentFilter, setDocumentFilter] = useState(""); - const [debouncedDocumentFilter, setDebouncedDocumentFilter] = useState(""); - const [documentTypeFilter, setDocumentTypeFilter] = useState< - DocumentType | "ALL" - >("ALL"); - const [documentsPage, setDocumentsPage] = useState(1); - const [documentsPerPage] = useState(10); - const { - documents, - loading: isLoadingDocuments, - error: documentsError, - } = useDocuments(Number(search_space_id)); + // Document selection state + const [selectedDocuments, setSelectedDocuments] = useState([]); + const [documentFilter, setDocumentFilter] = useState(""); + const [debouncedDocumentFilter, setDebouncedDocumentFilter] = useState(""); + const [documentTypeFilter, setDocumentTypeFilter] = useState< + DocumentType | "ALL" + >("ALL"); + const [documentsPage, setDocumentsPage] = useState(1); + const [documentsPerPage] = useState(10); + const { + documents, + loading: isLoadingDocuments, + error: documentsError, + } = useDocuments(Number(search_space_id)); - // Debounced search effect (proper implementation) - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedDocumentFilter(documentFilter); - setDocumentsPage(1); // Reset page when search changes - }, 300); + // Debounced search effect (proper implementation) + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedDocumentFilter(documentFilter); + setDocumentsPage(1); // Reset page when search changes + }, 300); - return () => { - clearTimeout(handler); - }; - }, [documentFilter]); + return () => { + clearTimeout(handler); + }; + }, [documentFilter]); - // Memoized filtered and paginated documents - const filteredDocuments = useMemo(() => { - if (!documents) return []; + // 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]); + 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 paginatedDocuments = useMemo(() => { + const startIndex = (documentsPage - 1) * documentsPerPage; + return filteredDocuments.slice( + startIndex, + startIndex + documentsPerPage + ); + }, [filteredDocuments, documentsPage, documentsPerPage]); - const totalPages = Math.ceil(filteredDocuments.length / documentsPerPage); + const totalPages = Math.ceil(filteredDocuments.length / documentsPerPage); - // Document type counts for filter dropdown - const documentTypeCounts = useMemo(() => { - if (!documents) return {}; + // 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]); + 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], - ); - }, []); + // 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) { - terminalMessagesRef.current.scrollTop = - terminalMessagesRef.current.scrollHeight; - } - }; + // Function to scroll terminal to bottom + const scrollTerminalToBottom = () => { + if (terminalMessagesRef.current) { + terminalMessagesRef.current.scrollTop = + terminalMessagesRef.current.scrollHeight; + } + }; - // Get token from localStorage on client side only - React.useEffect(() => { - setToken(localStorage.getItem("surfsense_bearer_token")); - }, []); + // Get token from localStorage on client side only + React.useEffect(() => { + setToken(localStorage.getItem("surfsense_bearer_token")); + }, []); - // Set the current time only on the client side after initial render - useEffect(() => { - setCurrentDate(new Date().toISOString().split("T")[0]); - setCurrentTime(new Date().toTimeString().split(" ")[0]); - }, []); + // Set the current time only on the client side after initial render + useEffect(() => { + setCurrentDate(new Date().toISOString().split("T")[0]); + setCurrentTime(new Date().toTimeString().split(" ")[0]); + }, []); - // Add this CSS to remove input shadow and improve the UI - useEffect(() => { - if (typeof document !== "undefined") { - const style = document.createElement("style"); - style.innerHTML = ` + // Add this CSS to remove input shadow and improve the UI + useEffect(() => { + if (typeof document !== "undefined") { + const style = document.createElement("style"); + style.innerHTML = ` .no-shadow-input { box-shadow: none !important; } @@ -617,1454 +638,1909 @@ const ChatPage = () => { background: hsl(var(--muted-foreground) / 0.5); } `; - document.head.appendChild(style); + document.head.appendChild(style); - return () => { - document.head.removeChild(style); - }; - } - }, []); + return () => { + document.head.removeChild(style); + }; + } + }, []); - const { - messages, - input, - handleInputChange, - handleSubmit: handleChatSubmit, - status, - setMessages, - } = useChat({ - api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`, - streamProtocol: "data", - headers: { - ...(token && { Authorization: `Bearer ${token}` }), - }, - body: { - data: { - search_space_id: search_space_id, - selected_connectors: selectedConnectors, - research_mode: researchMode, - search_mode: searchMode, - document_ids_to_add_in_context: selectedDocuments, - }, - }, - onError: (error) => { - console.error("Chat error:", error); - // You can add additional error handling here if needed - }, - }); + const { + messages, + input, + handleInputChange, + handleSubmit: handleChatSubmit, + status, + setMessages, + } = useChat({ + api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`, + streamProtocol: "data", + headers: { + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: { + data: { + search_space_id: search_space_id, + selected_connectors: selectedConnectors, + research_mode: researchMode, + search_mode: searchMode, + document_ids_to_add_in_context: selectedDocuments, + }, + }, + onError: (error) => { + console.error("Chat error:", error); + // You can add additional error handling here if needed + }, + }); - // Fetch chat details when component mounts - useEffect(() => { - const fetchChatDetails = async () => { - try { - if (!token) return; // Wait for token to be set + // Fetch chat details when component mounts + useEffect(() => { + const fetchChatDetails = async () => { + try { + if (!token) return; // Wait for token to be set - // console.log('Fetching chat details for chat ID:', chat_id); + // console.log('Fetching chat details for chat ID:', chat_id); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chat_id)}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }, - ); + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/chats/${Number(chat_id)}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); - if (!response.ok) { - throw new Error( - `Failed to fetch chat details: ${response.statusText}`, - ); - } + if (!response.ok) { + throw new Error( + `Failed to fetch chat details: ${response.statusText}` + ); + } - const chatData = await response.json(); - // console.log('Chat details fetched:', chatData); + const chatData = await response.json(); + // console.log('Chat details fetched:', chatData); - // Set research mode from chat data - if (chatData.type) { - setResearchMode(chatData.type as ResearchMode); - } + // Set research mode from chat data + if (chatData.type) { + setResearchMode(chatData.type as ResearchMode); + } - // Set connectors from chat data - if ( - chatData.initial_connectors && - Array.isArray(chatData.initial_connectors) - ) { - setSelectedConnectors(chatData.initial_connectors); - } + // Set connectors from chat data + if ( + chatData.initial_connectors && + Array.isArray(chatData.initial_connectors) + ) { + setSelectedConnectors(chatData.initial_connectors); + } - // Set messages from chat data - if (chatData.messages && Array.isArray(chatData.messages)) { - setMessages(chatData.messages); - } - } catch (err) { - console.error("Error fetching chat details:", err); - } - }; - - if (token) { - fetchChatDetails(); - } - }, [token, chat_id, setMessages]); - - // Update chat when a conversation exchange is complete - useEffect(() => { - const updateChat = async () => { - try { - // Only update when: - // 1. Status is ready (not loading) - // 2. We have messages - // 3. Last message is from assistant (completed response) - if ( - status === "ready" && - messages.length > 0 && - messages[messages.length - 1]?.role === "assistant" - ) { - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) return; - - // Find the first user message to use as title - const userMessages = messages.filter((msg) => msg.role === "user"); - if (userMessages.length === 0) return; - - // Use the first user message as the title - const title = userMessages[0].content; - - // console.log('Updating chat with title:', title); - - // Update the chat - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chat_id)}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - type: researchMode, - title: title, - initial_connectors: selectedConnectors, - messages: messages, - search_space_id: Number(search_space_id), - }), - }, - ); - - if (!response.ok) { - throw new Error(`Failed to update chat: ${response.statusText}`); - } - - // console.log('Chat updated successfully'); - } - } catch (err) { - console.error("Error updating chat:", err); - } - }; - - updateChat(); - }, [ - messages, - status, - chat_id, - researchMode, - selectedConnectors, - search_space_id, - ]); - - // Check and scroll terminal when terminal info is available - useEffect(() => { - // Modified to trigger during streaming as well (removed status check) - if (messages.length === 0) return; - - // Find the latest assistant message - const assistantMessages = messages.filter( - (msg) => msg.role === "assistant", - ); - if (assistantMessages.length === 0) return; - - const latestAssistantMessage = - assistantMessages[assistantMessages.length - 1]; - if (!latestAssistantMessage?.annotations) return; - - // Check for terminal info annotations - const annotations = latestAssistantMessage.annotations as any[]; - const terminalInfoAnnotations = annotations.filter( - (a) => a.type === "TERMINAL_INFO", - ); - - if (terminalInfoAnnotations.length > 0) { - // Always scroll to bottom when terminal info is updated, even during streaming - scrollTerminalToBottom(); - } - }, [messages]); // Removed status from dependencies to ensure it triggers during streaming - - // Pure function to get connector sources for a specific message - const getMessageConnectorSources = (message: any): any[] => { - if (!message || message.role !== "assistant" || !message.annotations) - return []; - - // Find all SOURCES annotations - const annotations = message.annotations as any[]; - const sourcesAnnotations = annotations.filter((a) => a.type === "SOURCES"); - - // Get the latest SOURCES annotation - if (sourcesAnnotations.length === 0) return []; - const latestSourcesAnnotation = - sourcesAnnotations[sourcesAnnotations.length - 1]; - - if (!latestSourcesAnnotation.content) return []; - - return latestSourcesAnnotation.content; - }; - - // Custom handleSubmit function to include selected connectors and answer type - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!input.trim() || status !== "ready") return; - - // Validation: require at least one connector OR at least one document - // Note: Fast LLM selection updates user preferences automatically - // if (selectedConnectors.length === 0 && selectedDocuments.length === 0) { - // alert("Please select at least one connector or document"); - // return; - // } - - // Call the original handleSubmit from useChat - handleChatSubmit(e); - }; - - // Reference to the messages container for auto-scrolling - const messagesEndRef = useRef(null); - - // Function to scroll to bottom - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - // Scroll to bottom when messages change - useEffect(() => { - scrollToBottom(); - }, [messages]); - - // Reset sources page when new messages arrive - useEffect(() => { - // Reset pagination when we get new messages - setSourcesPage(1); - setExpandedSources(false); - }, [messages]); - - // Scroll terminal to bottom when expanded - useEffect(() => { - if (terminalExpanded) { - setTimeout(scrollTerminalToBottom, 300); // Wait for transition to complete - } - }, [terminalExpanded]); - - // Function to check scroll position and update indicators - const updateScrollIndicators = () => { - updateScrollIndicatorsUtil( - tabsListRef as React.RefObject, - setCanScrollLeft, - setCanScrollRight, - ); - }; - - // Initialize scroll indicators - const updateIndicators = useScrollIndicators( - tabsListRef as React.RefObject, - setCanScrollLeft, - setCanScrollRight, - ); - - // Function to scroll tabs list left - const scrollTabsLeft = () => { - scrollTabsLeftUtil( - tabsListRef as React.RefObject, - updateIndicators, - ); - }; - - // Function to scroll tabs list right - const scrollTabsRight = () => { - scrollTabsRightUtil( - tabsListRef as React.RefObject, - updateIndicators, - ); - }; - - // Use the scroll to bottom hook - useScrollToBottom(messagesEndRef as React.RefObject, [ - messages, - ]); - - // Function to get a citation source by ID - const getCitationSource = React.useCallback( - (citationId: number, messageIndex?: number): Source | null => { - if (!messages || messages.length === 0) return null; - - // If no specific message index is provided, use the latest assistant message - if (messageIndex === undefined) { - // Find the latest assistant message - const assistantMessages = messages.filter( - (msg) => msg.role === "assistant", - ); - if (assistantMessages.length === 0) return null; - - const latestAssistantMessage = - assistantMessages[assistantMessages.length - 1]; - - // Use our helper function to get sources - const sources = getMessageConnectorSources(latestAssistantMessage); - if (sources.length === 0) return null; - - // Flatten all sources from all connectors - const allSources: Source[] = []; - sources.forEach((connector: ConnectorSource) => { - if (connector.sources && Array.isArray(connector.sources)) { - connector.sources.forEach((source: SourceItem) => { - allSources.push({ - id: source.id, - title: source.title, - description: source.description, - url: source.url, - connectorType: connector.type, - }); - }); - } - }); - - // Find the source with the matching ID - const foundSource = allSources.find( - (source) => source.id === citationId, - ); - - return foundSource || null; - } else { - // Use the specific message by index - const message = messages[messageIndex]; - - // Use our helper function to get sources - const sources = getMessageConnectorSources(message); - if (sources.length === 0) return null; - - // Flatten all sources from all connectors - const allSources: Source[] = []; - sources.forEach((connector: ConnectorSource) => { - if (connector.sources && Array.isArray(connector.sources)) { - connector.sources.forEach((source: SourceItem) => { - allSources.push({ - id: source.id, - title: source.title, - description: source.description, - url: source.url, - connectorType: connector.type, - }); - }); - } - }); - - // Find the source with the matching ID - const foundSource = allSources.find( - (source) => source.id === citationId, - ); - - return foundSource || null; - } - }, - [messages], - ); - - // Pure function for rendering terminal content - no hooks allowed here - const renderTerminalContent = (message: any) => { - if (!message.annotations) return null; - - // Get all TERMINAL_INFO annotations content - const terminalInfoAnnotations = (message.annotations as any[]).map(item => { - if(item.type === "TERMINAL_INFO") { - return item.content.map((a: any) => a.text) - + // Set messages from chat data + if (chatData.messages && Array.isArray(chatData.messages)) { + setMessages(chatData.messages); + } + } catch (err) { + console.error("Error fetching chat details:", err); } - }).flat().filter(Boolean) + }; - // Render the content of the latest TERMINAL_INFO annotation - return terminalInfoAnnotations.map((item: any, idx: number) => ( -
- - [{String(idx).padStart(2, "0")}: - {String(Math.floor(idx * 2)).padStart(2, "0")}] - - {">"} - { + const updateChat = async () => { + try { + // Only update when: + // 1. Status is ready (not loading) + // 2. We have messages + // 3. Last message is from assistant (completed response) + if ( + status === "ready" && + messages.length > 0 && + messages[messages.length - 1]?.role === "assistant" + ) { + const token = localStorage.getItem( + "surfsense_bearer_token" + ); + if (!token) return; + + // Find the first user message to use as title + const userMessages = messages.filter( + (msg) => msg.role === "user" + ); + if (userMessages.length === 0) return; + + // Use the first user message as the title + const title = userMessages[0].content; + + // console.log('Updating chat with title:', title); + + // Update the chat + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/chats/${Number(chat_id)}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + type: researchMode, + title: title, + initial_connectors: selectedConnectors, + messages: messages, + search_space_id: Number(search_space_id), + }), + } + ); + + if (!response.ok) { + throw new Error( + `Failed to update chat: ${response.statusText}` + ); + } + + // console.log('Chat updated successfully'); + } + } catch (err) { + console.error("Error updating chat:", err); + } + }; + + updateChat(); + }, [ + messages, + status, + chat_id, + researchMode, + selectedConnectors, + search_space_id, + ]); + + // Check and scroll terminal when terminal info is available + useEffect(() => { + // Modified to trigger during streaming as well (removed status check) + if (messages.length === 0) return; + + // Find the latest assistant message + const assistantMessages = messages.filter( + (msg) => msg.role === "assistant" + ); + if (assistantMessages.length === 0) return; + + const latestAssistantMessage = + assistantMessages[assistantMessages.length - 1]; + if (!latestAssistantMessage?.annotations) return; + + // Check for terminal info annotations + const annotations = latestAssistantMessage.annotations as any[]; + const terminalInfoAnnotations = annotations.filter( + (a) => a.type === "TERMINAL_INFO" + ); + + if (terminalInfoAnnotations.length > 0) { + // Always scroll to bottom when terminal info is updated, even during streaming + scrollTerminalToBottom(); + } + }, [messages]); // Removed status from dependencies to ensure it triggers during streaming + + // Pure function to get connector sources for a specific message + const getMessageConnectorSources = (message: any): any[] => { + if (!message || message.role !== "assistant" || !message.annotations) + return []; + + // Find all SOURCES annotations + const annotations = message.annotations as any[]; + const sourcesAnnotations = annotations.filter( + (a) => a.type === "SOURCES" + ); + + // Get the latest SOURCES annotation + if (sourcesAnnotations.length === 0) return []; + const latestSourcesAnnotation = + sourcesAnnotations[sourcesAnnotations.length - 1]; + + if (!latestSourcesAnnotation.content) return []; + + return latestSourcesAnnotation.content; + }; + + // Custom handleSubmit function to include selected connectors and answer type + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!input.trim() || status !== "ready") return; + + // Validation: require at least one connector OR at least one document + // Note: Fast LLM selection updates user preferences automatically + // if (selectedConnectors.length === 0 && selectedDocuments.length === 0) { + // alert("Please select at least one connector or document"); + // return; + // } + + // Call the original handleSubmit from useChat + handleChatSubmit(e); + }; + + // Reference to the messages container for auto-scrolling + const messagesEndRef = useRef(null); + + // Function to scroll to bottom + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + // Scroll to bottom when messages change + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // Reset sources page when new messages arrive + useEffect(() => { + // Reset pagination when we get new messages + setSourcesPage(1); + setExpandedSources(false); + }, [messages]); + + // Scroll terminal to bottom when expanded + useEffect(() => { + if (terminalExpanded) { + setTimeout(scrollTerminalToBottom, 300); // Wait for transition to complete + } + }, [terminalExpanded]); + + // Function to check scroll position and update indicators + const updateScrollIndicators = () => { + updateScrollIndicatorsUtil( + tabsListRef as React.RefObject, + setCanScrollLeft, + setCanScrollRight + ); + }; + + // Initialize scroll indicators + const updateIndicators = useScrollIndicators( + tabsListRef as React.RefObject, + setCanScrollLeft, + setCanScrollRight + ); + + // Function to scroll tabs list left + const scrollTabsLeft = () => { + scrollTabsLeftUtil( + tabsListRef as React.RefObject, + updateIndicators + ); + }; + + // Function to scroll tabs list right + const scrollTabsRight = () => { + scrollTabsRightUtil( + tabsListRef as React.RefObject, + updateIndicators + ); + }; + + // Use the scroll to bottom hook + useScrollToBottom(messagesEndRef as React.RefObject, [ + messages, + ]); + + // Function to get a citation source by ID + const getCitationSource = React.useCallback( + (citationId: number, messageIndex?: number): Source | null => { + if (!messages || messages.length === 0) return null; + + // If no specific message index is provided, use the latest assistant message + if (messageIndex === undefined) { + // Find the latest assistant message + const assistantMessages = messages.filter( + (msg) => msg.role === "assistant" + ); + if (assistantMessages.length === 0) return null; + + const latestAssistantMessage = + assistantMessages[assistantMessages.length - 1]; + + // Use our helper function to get sources + const sources = getMessageConnectorSources( + latestAssistantMessage + ); + if (sources.length === 0) return null; + + // Flatten all sources from all connectors + const allSources: Source[] = []; + sources.forEach((connector: ConnectorSource) => { + if (connector.sources && Array.isArray(connector.sources)) { + connector.sources.forEach((source: SourceItem) => { + allSources.push({ + id: source.id, + title: source.title, + description: source.description, + url: source.url, + connectorType: connector.type, + }); + }); + } + }); + + // Find the source with the matching ID + const foundSource = allSources.find( + (source) => source.id === citationId + ); + + return foundSource || null; + } else { + // Use the specific message by index + const message = messages[messageIndex]; + + // Use our helper function to get sources + const sources = getMessageConnectorSources(message); + if (sources.length === 0) return null; + + // Flatten all sources from all connectors + const allSources: Source[] = []; + sources.forEach((connector: ConnectorSource) => { + if (connector.sources && Array.isArray(connector.sources)) { + connector.sources.forEach((source: SourceItem) => { + allSources.push({ + id: source.id, + title: source.title, + description: source.description, + url: source.url, + connectorType: connector.type, + }); + }); + } + }); + + // Find the source with the matching ID + const foundSource = allSources.find( + (source) => source.id === citationId + ); + + return foundSource || null; + } + }, + [messages] + ); + + // Pure function for rendering terminal content - no hooks allowed here + const renderTerminalContent = (message: any) => { + if (!message.annotations) return null; + + // Get all TERMINAL_INFO annotations content + const terminalInfoAnnotations = (message.annotations as any[]) + .map((item) => { + if (item.type === "TERMINAL_INFO") { + return item.content.map((a: any) => a.text); + } + }) + .flat() + .filter(Boolean); + + // Render the content of the latest TERMINAL_INFO annotation + return terminalInfoAnnotations.map((item: any, idx: number) => ( +
+ + [{String(idx).padStart(2, "0")}: + {String(Math.floor(idx * 2)).padStart(2, "0")}] + + {">"} + - {item} - -
- )); - }; + > + {item} +
+
+ )); + }; - return ( - <> -
- {messages.length === 0 && ( -

- -
- Surf{""} -
-
- Sense -
-
-
-

- )} - {messages?.map((message, index) => { - if (message.role === "user") { - return ( -
- -
- - - getCitationSource(id, index)} - className="text-sm" - /> - - -
-
- ); - } - - if (message.role === "assistant") { - return ( -
- - - - Answer - - - - {/* Status Messages Section */} - -
-
-
-
setTerminalExpanded(false)} - >
-
-
setTerminalExpanded(true)} - >
-
- - surfsense-research-terminal - -
-
- -
-
- Last login: {currentDate} {currentTime} -
-
- - researcher@surfsense - - : - ~/research - $ - surfsense-researcher -
- - {renderTerminalContent(message)} - -
- - [00:13] - - - researcher@surfsense - - : - ~/research - $ -
-
- - {/* Terminal scroll button */} -
- -
-
-
- - {/* Sources Section with Connector Tabs */} -
-
- - Sources -
- - {(() => { - // Get sources for this specific message - const messageConnectorSources = - getMessageConnectorSources(message); - - if (messageConnectorSources.length === 0) { - return ( -
- -
- ); - } - - // Use these message-specific sources for the Tabs component - return ( - 0 - ? messageConnectorSources[0].type - : undefined - } - className="w-full" - > -
-
- - -
-
- - {messageConnectorSources.map( - (connector) => ( - - {getConnectorIcon(connector.type)} - - {connector.name.split(" ")[0]} - - - {connector.sources?.length || 0} - - - ), - )} - -
-
- - -
-
- - {messageConnectorSources.map((connector) => ( - -
- {connector.sources - ?.slice(0, INITIAL_SOURCES_DISPLAY) - ?.map((source: any, index: number) => ( - -
-
- {getConnectorIcon(connector.type)} -
-
-

- {source.title} -

-

- {source.description} -

-
- -
-
- ))} - - {connector.sources?.length > - INITIAL_SOURCES_DISPLAY && ( - - setDialogOpenId( - open ? connector.id : null, - ) - } - > - - - - - - - - )} -
-
- ))} -
- ); - })()} -
- - {/* Answer Section */} -
- { -
- {message.annotations && - (() => { - // Get all ANSWER annotations - const answerAnnotations = ( - message.annotations as any[] - ).filter((a) => a.type === "ANSWER"); - - // Get the latest ANSWER annotation - const latestAnswer = - answerAnnotations.length > 0 - ? answerAnnotations[ - answerAnnotations.length - 1 - ] - : null; - - // If we have a latest ANSWER annotation with content, render it - if ( - latestAnswer?.content && - latestAnswer.content.length > 0 - ) { - return ( - - getCitationSource(id, index) - } - type="ai" - /> - ); - } - - // Fallback to the message content if no ANSWER annotation is available - return getCitationSource(id, index)} - type="ai" - />; - })()} -
- } -
- - {/* Further Questions Section */} - {message.annotations && (() => { - // Get all FURTHER_QUESTIONS annotations - const furtherQuestionsAnnotations = (message.annotations as any[]) - .filter(a => a.type === 'FURTHER_QUESTIONS'); - - // Get the latest FURTHER_QUESTIONS annotation - const latestFurtherQuestions = furtherQuestionsAnnotations.length > 0 - ? furtherQuestionsAnnotations[furtherQuestionsAnnotations.length - 1] - : null; - - // Only render if we have questions - if (!latestFurtherQuestions?.content || latestFurtherQuestions.content.length === 0) { - return null; - } - - const furtherQuestions = latestFurtherQuestions.content; - - return ( -
- {/* Main container with improved styling */} -
- {/* Header with better visual separation */} -
-
-

- - - - Follow-up Questions -

- - {furtherQuestions.length} suggestion{furtherQuestions.length !== 1 ? 's' : ''} - -
-
- - {/* Questions container with enhanced scrolling */} -
-
- {/* Left fade gradient */} -
- - {/* Right fade gradient */} -
- - {/* Scrollable container */} -
-
- {furtherQuestions.map((question: any, qIndex: number) => ( - - ))} -
+ return ( + <> +
+ {messages.length === 0 && ( +

+ +
+ Surf{""} +
+
+ Sense
-
-

- ); - })()} - {/* Scroll to bottom button */} -
- + + )} + {messages?.map((message, index) => { + if (message.role === "user") { + return ( +
+ +
+ + + + getCitationSource(id, index) + } + className="text-sm" + /> + + +
+
+ ); + } + + if (message.role === "assistant") { + return ( +
+ + + + Answer + + + + {/* Status Messages Section */} + +
+
+
+
+ setTerminalExpanded( + false + ) + } + >
+
+
+ setTerminalExpanded( + true + ) + } + >
+
+ + surfsense-research-terminal + +
+
+ +
+
+ Last login: {currentDate}{" "} + {currentTime} +
+
+ + researcher@surfsense + + + : + + + ~/research + + + $ + + + surfsense-researcher + +
+ + {renderTerminalContent(message)} + +
+ + [00:13] + + + researcher@surfsense + + + : + + + ~/research + + + $ + +
+
+ + {/* Terminal scroll button */} +
+ +
+
+
+ + {/* Sources Section with Connector Tabs */} +
+
+ + + Sources + +
+ + {(() => { + // Get sources for this specific message + const messageConnectorSources = + getMessageConnectorSources( + message + ); + + if ( + messageConnectorSources.length === + 0 + ) { + return ( +
+ +
+ ); + } + + // Use these message-specific sources for the Tabs component + return ( + + 0 + ? messageConnectorSources[0] + .type + : undefined + } + className="w-full" + > +
+
+ + +
+
+ + {messageConnectorSources.map( + ( + connector + ) => ( + + {getConnectorIcon( + connector.type + )} + + { + connector.name.split( + " " + )[0] + } + + + {connector + .sources + ?.length || + 0} + + + ) + )} + +
+
+ + +
+
+ + {messageConnectorSources.map( + (connector) => ( + +
+ {connector.sources + ?.slice( + 0, + INITIAL_SOURCES_DISPLAY + ) + ?.map( + ( + source: any, + index: number + ) => ( + +
+
+ {getConnectorIcon( + connector.type + )} +
+
+

+ { + source.title + } +

+

+ { + source.description + } +

+
+ +
+
+ ) + )} + + {connector + .sources + ?.length > + INITIAL_SOURCES_DISPLAY && ( + + setDialogOpenId( + open + ? connector.id + : null + ) + } + > + + + + + + + + )} +
+
+ ) + )} +
+ ); + })()} +
+ + {/* Answer Section */} +
+ { +
+ {message.annotations && + (() => { + // Get all ANSWER annotations + const answerAnnotations = + ( + message.annotations as any[] + ).filter( + (a) => + a.type === + "ANSWER" + ); + + // Get the latest ANSWER annotation + const latestAnswer = + answerAnnotations.length > + 0 + ? answerAnnotations[ + answerAnnotations.length - + 1 + ] + : null; + + // If we have a latest ANSWER annotation with content, render it + if ( + latestAnswer?.content && + latestAnswer + .content + .length > 0 + ) { + return ( + + getCitationSource( + id, + index + ) + } + type="ai" + /> + ); + } + + // Fallback to the message content if no ANSWER annotation is available + return ( + + getCitationSource( + id, + index + ) + } + type="ai" + /> + ); + })()} +
+ } +
+ + {/* Further Questions Section */} + {message.annotations && + (() => { + // Get all FURTHER_QUESTIONS annotations + const furtherQuestionsAnnotations = + ( + message.annotations as any[] + ).filter( + (a) => + a.type === + "FURTHER_QUESTIONS" + ); + + // Get the latest FURTHER_QUESTIONS annotation + const latestFurtherQuestions = + furtherQuestionsAnnotations.length > + 0 + ? furtherQuestionsAnnotations[ + furtherQuestionsAnnotations.length - + 1 + ] + : null; + + // Only render if we have questions + if ( + !latestFurtherQuestions?.content || + latestFurtherQuestions + .content.length === 0 + ) { + return null; + } + + const furtherQuestions = + latestFurtherQuestions.content; + + return ( +
+ {/* Main container with improved styling */} +
+ {/* Header with better visual separation */} +
+
+

+ + + + Follow-up + Questions +

+ + { + furtherQuestions.length + }{" "} + suggestion + {furtherQuestions.length !== + 1 + ? "s" + : ""} + +
+
+ + {/* Questions container with enhanced scrolling */} +
+
+ {/* Left fade gradient */} +
+ + {/* Right fade gradient */} +
+ + {/* Scrollable container */} +
+
+ {furtherQuestions.map( + ( + question: any, + qIndex: number + ) => ( + + ) + )} +
+
+
+
+
+
+ ); + })()} + {/* Scroll to bottom button */} +
+ +
+ + +
+ ); + } + + return null; + })} + + {/* New Chat Input Form */} +
+
+ + {/* Send button */} + +
+
+
+ {/* Enhanced Document Selection Dialog */} + + + {}} + documentsCount={documents?.length || 0} + /> + + + + +
+ + Select Documents + + {selectedDocuments.length}{" "} + selected + +
+ +
+ + Choose documents to include in your + research context. Use filters and + search to find specific documents. + +
+ + {/* Enhanced Search and Filter Controls */} +
+
+ {/* Search Input */} +
+ + + setDocumentFilter( + e.target.value + ) + } + /> + {documentFilter && ( + + )} +
+ + {/* Document Type Filter */} + { + setDocumentTypeFilter( + newType + ); + setDocumentsPage(1); // Reset to page 1 when filter changes + }} + counts={documentTypeCounts} + /> +
+ + {/* 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 + ) + } + > +
+
+ {getConnectorIcon( + document.document_type + )} +
+
+
+
+

+ { + 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 + +
+
+ + + +
+
+
+
+ + {/* Connector Selection Dialog */} + { + if (open && !isConnectorsLoaded) { + fetchConnectors(); + } + }} + > + + {}} + /> + + + + + Select Connectors + + + Choose which data sources to include + in your research + + + + {/* Connector selection grid */} +
+ {isLoadingConnectors ? ( +
+ +
+ ) : ( + connectorSourceItems.map( + (connector) => { + const isSelected = + selectedConnectors.includes( + connector.type + ); + + return ( +
{ + setSelectedConnectors( + isSelected + ? selectedConnectors.filter( + ( + type + ) => + type !== + connector.type + ) + : [ + ...selectedConnectors, + connector.type, + ] + ); + }} + role="checkbox" + aria-checked={ + isSelected + } + tabIndex={0} + > +
+ {getConnectorIcon( + connector.type + )} +
+ + {connector.name} + + {isSelected && ( + + )} +
+ ); + } + ) + )} +
+ + +
+ + +
+
+
+
+ + {/* Search Mode Control */} +
+ + +
+ + {/* Research Mode Control */} +
+ +
+ + {/* Fast LLM Selector */} +
+ +
+
- - -
- ); - } +
- return null; - })} - - {/* New Chat Input Form */} -
-
- - {/* Send button */} - -
-
-
- {/* Enhanced Document Selection Dialog */} - - - {}} - documentsCount={documents?.length || 0} - /> - - - - -
- - Select Documents - - {selectedDocuments.length} selected - -
- -
- - Choose documents to include in your research context. Use - filters and search to find specific documents. - -
- - {/* Enhanced Search and Filter Controls */} -
-
- {/* Search Input */} -
- - setDocumentFilter(e.target.value)} - /> - {documentFilter && ( - - )} -
- - {/* Document Type Filter */} - { - setDocumentTypeFilter(newType); - setDocumentsPage(1); // Reset to page 1 when filter changes - }} - counts={documentTypeCounts} - /> -
- - {/* 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)} - > -
-
- {getConnectorIcon(document.document_type)} -
-
-
-
-

- {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 - -
-
- - - -
-
-
-
- - {/* Connector Selection Dialog */} - - - {}} - /> - - - - Select Connectors - - Choose which data sources to include in your research - - - - {/* Connector selection grid */} -
- {isLoadingConnectors ? ( -
- -
- ) : ( - connectorSourceItems.map((connector) => { - const isSelected = selectedConnectors.includes( - connector.type, - ); - - return ( -
{ - setSelectedConnectors( - isSelected - ? selectedConnectors.filter( - (type) => type !== connector.type, - ) - : [...selectedConnectors, connector.type], - ); - }} - role="checkbox" - aria-checked={isSelected} - tabIndex={0} - > -
- {getConnectorIcon(connector.type)} -
- - {connector.name} - - {isSelected && ( - - )} -
- ); - }) - )} -
- - -
- - -
-
-
-
- - {/* Search Mode Control */} -
- - -
- - {/* Research Mode Control */} -
- -
- - {/* Fast LLM Selector */} -
- -
-
-
-
- - {/* Reference for auto-scrolling */} -
-
- - ); + {/* Reference for auto-scrolling */} +
+
+ + ); }; export default ChatPage; diff --git a/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx index cff02069c..8e831adcd 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx @@ -42,9 +42,15 @@ export default function ResearchChatPageV2() { return selectedDocuments.map((doc) => doc.id); }, [selectedDocuments]); + // Memoize connector types to prevent infinite re-renders + const connectorTypes = useMemo(() => { + return selectedConnectors; + }, [selectedConnectors]); + // Unified localStorage management for chat state interface ChatState { selectedDocuments: Document[]; + selectedConnectors: string[]; searchMode: "DOCUMENTS" | "CHUNKS"; researchMode: ResearchMode; } @@ -89,7 +95,7 @@ export default function ResearchChatPageV2() { body: { data: { search_space_id: search_space_id, - selected_connectors: selectedConnectors, + selected_connectors: connectorTypes, research_mode: researchMode, search_mode: searchMode, document_ids_to_add_in_context: documentIds, @@ -104,11 +110,16 @@ export default function ResearchChatPageV2() { message: Message | CreateMessage, chatRequestOptions?: { data?: any } ) => { - const newChatId = await createChat(message.content, researchMode); + const newChatId = await createChat( + message.content, + researchMode, + selectedConnectors + ); if (newChatId) { // Store chat state before navigation storeChatState(search_space_id as string, newChatId, { selectedDocuments, + selectedConnectors, searchMode, researchMode, }); @@ -133,6 +144,7 @@ export default function ResearchChatPageV2() { ); if (restoredState) { setSelectedDocuments(restoredState.selectedDocuments); + setSelectedConnectors(restoredState.selectedConnectors); setSearchMode(restoredState.searchMode); setResearchMode(restoredState.researchMode); } @@ -141,6 +153,7 @@ export default function ResearchChatPageV2() { chatIdParam, search_space_id, setSelectedDocuments, + setSelectedConnectors, setSearchMode, setResearchMode, ]); @@ -192,7 +205,12 @@ export default function ResearchChatPageV2() { handler.messages.length > 0 && handler.messages[handler.messages.length - 1]?.role === "assistant" ) { - updateChat(chatIdParam, handler.messages, researchMode); + updateChat( + chatIdParam, + handler.messages, + researchMode, + selectedConnectors + ); } }, [handler.messages, handler.status, chatIdParam, isNewChat]); @@ -212,6 +230,8 @@ export default function ResearchChatPageV2() { }} onDocumentSelectionChange={setSelectedDocuments} selectedDocuments={selectedDocuments} + onConnectorSelectionChange={setSelectedConnectors} + selectedConnectors={selectedConnectors} searchMode={searchMode} onSearchModeChange={setSearchMode} researchMode={researchMode} diff --git a/surfsense_web/components/chat_v2/ChatInputGroup.tsx b/surfsense_web/components/chat_v2/ChatInputGroup.tsx index ebef6fd91..6229d1fae 100644 --- a/surfsense_web/components/chat_v2/ChatInputGroup.tsx +++ b/surfsense_web/components/chat_v2/ChatInputGroup.tsx @@ -1,7 +1,7 @@ "use client"; import { ChatInput } from "@llamaindex/chat-ui"; -import { FolderOpen } from "lucide-react"; +import { FolderOpen, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -9,6 +9,7 @@ import { DialogDescription, DialogTitle, DialogTrigger, + DialogFooter, } from "@/components/ui/dialog"; import { Select, @@ -21,6 +22,11 @@ import { Suspense, useState, useCallback } from "react"; import { useParams } from "next/navigation"; import { useDocuments, Document } from "@/hooks/use-documents"; import { DocumentsDataTable } from "@/components/chat_v2/DocumentsDataTable"; +import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; +import { + getConnectorIcon, + ConnectorButton as ConnectorButtonComponent, +} from "@/components/chat/ConnectorComponents"; import { ResearchMode } from "@/components/chat"; import React from "react"; @@ -113,6 +119,126 @@ const DocumentSelector = React.memo( } ); +const ConnectorSelector = React.memo( + ({ + onSelectionChange, + selectedConnectors = [], + }: { + onSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; + }) => { + const [isOpen, setIsOpen] = useState(false); + + const { connectorSourceItems, isLoading, isLoaded, fetchConnectors } = + useSearchSourceConnectors(); + + const handleOpenChange = useCallback( + (open: boolean) => { + setIsOpen(open); + if (open && !isLoaded) { + fetchConnectors(); + } + }, + [fetchConnectors, isLoaded] + ); + + const handleConnectorToggle = useCallback( + (connectorType: string) => { + const isSelected = selectedConnectors.includes(connectorType); + const newSelection = isSelected + ? selectedConnectors.filter( + (type) => type !== connectorType + ) + : [...selectedConnectors, connectorType]; + onSelectionChange?.(newSelection); + }, + [selectedConnectors, onSelectionChange] + ); + + const handleSelectAll = useCallback(() => { + onSelectionChange?.(connectorSourceItems.map((c) => c.type)); + }, [connectorSourceItems, onSelectionChange]); + + const handleClearAll = useCallback(() => { + onSelectionChange?.([]); + }, [onSelectionChange]); + + return ( + + + setIsOpen(true)} + connectorSources={connectorSourceItems} + /> + + + + Select Connectors + + Choose which data sources to include in your research + + + {/* Connector selection grid */} +
+ {isLoading ? ( +
+
+
+ ) : ( + connectorSourceItems.map((connector) => { + const isSelected = selectedConnectors.includes( + connector.type + ); + + return ( +
+ handleConnectorToggle( + connector.type + ) + } + role="checkbox" + aria-checked={isSelected} + tabIndex={0} + > +
+ {getConnectorIcon(connector.type)} +
+ + {connector.name} + + {isSelected && ( + + )} +
+ ); + }) + )} +
+ + +
+ + +
+
+ +
+ ); + } +); + const SearchModeSelector = ({ searchMode, onSearchModeChange, @@ -155,20 +281,6 @@ const ResearchModeSelector = ({ researchMode?: ResearchMode; onResearchModeChange?: (mode: ResearchMode) => void; }) => { - const researchModeLabels: Record = { - QNA: "Q&A", - REPORT_GENERAL: "General Report", - REPORT_DEEP: "Deep Report", - REPORT_DEEPER: "Deeper Report", - }; - - const researchModeShortLabels: Record = { - QNA: "Q&A", - REPORT_GENERAL: "General", - REPORT_DEEP: "Deep", - REPORT_DEEPER: "Deeper", - }; - return (
@@ -206,6 +318,8 @@ const ResearchModeSelector = ({ const CustomChatInputOptions = ({ onDocumentSelectionChange, selectedDocuments, + onConnectorSelectionChange, + selectedConnectors, searchMode, onSearchModeChange, researchMode, @@ -213,6 +327,8 @@ const CustomChatInputOptions = ({ }: { onDocumentSelectionChange?: (documents: Document[]) => void; selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; searchMode?: "DOCUMENTS" | "CHUNKS"; onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; researchMode?: ResearchMode; @@ -226,6 +342,12 @@ const CustomChatInputOptions = ({ selectedDocuments={selectedDocuments} /> + Loading...
}> + + void; selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; searchMode?: "DOCUMENTS" | "CHUNKS"; onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; researchMode?: ResearchMode; @@ -262,6 +388,8 @@ export const CustomChatInput = ({ void; selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; searchMode?: "DOCUMENTS" | "CHUNKS"; onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; researchMode?: ResearchMode; @@ -25,6 +27,8 @@ export default function ChatInterface({ handler, onDocumentSelectionChange, selectedDocuments = [], + onConnectorSelectionChange, + selectedConnectors = [], searchMode, onSearchModeChange, researchMode, @@ -44,6 +48,8 @@ export default function ChatInterface({ => { if (!token) { console.error("Authentication token not found"); @@ -107,7 +108,7 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { body: JSON.stringify({ type: researchMode, title: "Untitled Chat", - initial_connectors: [], + initial_connectors: selectedConnectors, messages: [ { role: "user", @@ -139,7 +140,8 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { async ( chatId: string, messages: Message[], - researchMode: ResearchMode + researchMode: ResearchMode, + selectedConnectors: string[] ) => { if (!token) return; @@ -164,7 +166,7 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { body: JSON.stringify({ type: researchMode, title: title, - initial_connectors: [], + initial_connectors: selectedConnectors, messages: messages, search_space_id: Number(search_space_id), }), diff --git a/surfsense_web/hooks/useSearchSourceConnectors.ts b/surfsense_web/hooks/useSearchSourceConnectors.ts index 968e883ec..ddc16f963 100644 --- a/surfsense_web/hooks/useSearchSourceConnectors.ts +++ b/surfsense_web/hooks/useSearchSourceConnectors.ts @@ -1,328 +1,367 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback } from "react"; export interface SearchSourceConnector { - id: number; - name: string; - connector_type: string; - is_indexable: boolean; - last_indexed_at: string | null; - config: Record; - user_id?: string; - created_at?: string; + id: number; + name: string; + connector_type: string; + is_indexable: boolean; + last_indexed_at: string | null; + config: Record; + user_id?: string; + created_at?: string; } export interface ConnectorSourceItem { - id: number; - name: string; - type: string; - sources: any[]; + id: number; + name: string; + type: string; + sources: any[]; } /** * Hook to fetch search source connectors from the API */ export const useSearchSourceConnectors = () => { - const [connectors, setConnectors] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [connectorSourceItems, setConnectorSourceItems] = useState([ - { - id: 1, - name: "Crawled URL", - type: "CRAWLED_URL", - sources: [], - }, - { - id: 2, - name: "File", - type: "FILE", - sources: [], - }, - { - id: 3, - name: "Extension", - type: "EXTENSION", - sources: [], - }, - { - id: 4, - name: "Youtube Video", - type: "YOUTUBE_VIDEO", - sources: [], - } - ]); + const [connectors, setConnectors] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const [error, setError] = useState(null); + const [connectorSourceItems, setConnectorSourceItems] = useState< + ConnectorSourceItem[] + >([ + { + id: 1, + name: "Crawled URL", + type: "CRAWLED_URL", + sources: [], + }, + { + id: 2, + name: "File", + type: "FILE", + sources: [], + }, + { + id: 3, + name: "Extension", + type: "EXTENSION", + sources: [], + }, + { + id: 4, + name: "Youtube Video", + type: "YOUTUBE_VIDEO", + sources: [], + }, + ]); - useEffect(() => { - const fetchConnectors = async () => { - try { - setIsLoading(true); - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - throw new Error('No authentication token found'); - } + const fetchConnectors = useCallback(async () => { + if (isLoaded) return; // Don't fetch if already loaded - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` + try { + setIsLoading(true); + setError(null); + const token = localStorage.getItem("surfsense_bearer_token"); + + if (!token) { + throw new Error("No authentication token found"); } - } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch connectors: ${response.statusText}` + ); + } + + const data = await response.json(); + setConnectors(data); + setIsLoaded(true); + + // Update connector source items when connectors change + updateConnectorSourceItems(data); + } catch (err) { + setError( + err instanceof Error + ? err + : new Error("An unknown error occurred") + ); + console.error("Error fetching search source connectors:", err); + } finally { + setIsLoading(false); + } + }, [isLoaded]); + + // Update connector source items when connectors change + const updateConnectorSourceItems = ( + currentConnectors: SearchSourceConnector[] + ) => { + // Start with the default hardcoded connectors + const defaultConnectors: ConnectorSourceItem[] = [ + { + id: 1, + name: "Crawled URL", + type: "CRAWLED_URL", + sources: [], + }, + { + id: 2, + name: "File", + type: "FILE", + sources: [], + }, + { + id: 3, + name: "Extension", + type: "EXTENSION", + sources: [], + }, + { + id: 4, + name: "Youtube Video", + type: "YOUTUBE_VIDEO", + sources: [], + }, + ]; + + // Add the API connectors + const apiConnectors: ConnectorSourceItem[] = currentConnectors.map( + (connector, index) => ({ + id: 1000 + index, // Use a high ID to avoid conflicts with hardcoded IDs + name: connector.name, + type: connector.connector_type, + sources: [], + }) ); - if (!response.ok) { - throw new Error(`Failed to fetch connectors: ${response.statusText}`); - } - - const data = await response.json(); - setConnectors(data); - - // Update connector source items when connectors change - updateConnectorSourceItems(data); - } catch (err) { - setError(err instanceof Error ? err : new Error('An unknown error occurred')); - console.error('Error fetching search source connectors:', err); - } finally { - setIsLoading(false); - } + setConnectorSourceItems([...defaultConnectors, ...apiConnectors]); }; - fetchConnectors(); - }, []); - - // Update connector source items when connectors change - const updateConnectorSourceItems = (currentConnectors: SearchSourceConnector[]) => { - // Start with the default hardcoded connectors - const defaultConnectors: ConnectorSourceItem[] = [ - { - id: 1, - name: "Crawled URL", - type: "CRAWLED_URL", - sources: [], - }, - { - id: 2, - name: "File", - type: "FILE", - sources: [], - }, - { - id: 3, - name: "Extension", - type: "EXTENSION", - sources: [], - }, - { - id: 4, - name: "Youtube Video", - type: "YOUTUBE_VIDEO", - sources: [], - } - ]; - - // Add the API connectors - const apiConnectors: ConnectorSourceItem[] = currentConnectors.map((connector, index) => ({ - id: 1000 + index, // Use a high ID to avoid conflicts with hardcoded IDs - name: connector.name, - type: connector.connector_type, - sources: [], - })); - - setConnectorSourceItems([...defaultConnectors, ...apiConnectors]); - }; + /** + * Create a new search source connector + */ + const createConnector = async ( + connectorData: Omit< + SearchSourceConnector, + "id" | "user_id" | "created_at" + > + ) => { + try { + const token = localStorage.getItem("surfsense_bearer_token"); - /** - * Create a new search source connector - */ - const createConnector = async (connectorData: Omit) => { - try { - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - throw new Error('No authentication token found'); - } + if (!token) { + throw new Error("No authentication token found"); + } - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify(connectorData) + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(connectorData), + } + ); + + if (!response.ok) { + throw new Error( + `Failed to create connector: ${response.statusText}` + ); + } + + const newConnector = await response.json(); + const updatedConnectors = [...connectors, newConnector]; + setConnectors(updatedConnectors); + updateConnectorSourceItems(updatedConnectors); + return newConnector; + } catch (err) { + console.error("Error creating search source connector:", err); + throw err; } - ); + }; - if (!response.ok) { - throw new Error(`Failed to create connector: ${response.statusText}`); - } + /** + * Update an existing search source connector + */ + const updateConnector = async ( + connectorId: number, + connectorData: Partial< + Omit + > + ) => { + try { + const token = localStorage.getItem("surfsense_bearer_token"); - const newConnector = await response.json(); - const updatedConnectors = [...connectors, newConnector]; - setConnectors(updatedConnectors); - updateConnectorSourceItems(updatedConnectors); - return newConnector; - } catch (err) { - console.error('Error creating search source connector:', err); - throw err; - } - }; + if (!token) { + throw new Error("No authentication token found"); + } - /** - * Update an existing search source connector - */ - const updateConnector = async ( - connectorId: number, - connectorData: Partial> - ) => { - try { - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - throw new Error('No authentication token found'); - } + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(connectorData), + } + ); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify(connectorData) + if (!response.ok) { + throw new Error( + `Failed to update connector: ${response.statusText}` + ); + } + + const updatedConnector = await response.json(); + const updatedConnectors = connectors.map((connector) => + connector.id === connectorId ? updatedConnector : connector + ); + setConnectors(updatedConnectors); + updateConnectorSourceItems(updatedConnectors); + return updatedConnector; + } catch (err) { + console.error("Error updating search source connector:", err); + throw err; } - ); + }; - if (!response.ok) { - throw new Error(`Failed to update connector: ${response.statusText}`); - } + /** + * Delete a search source connector + */ + const deleteConnector = async (connectorId: number) => { + try { + const token = localStorage.getItem("surfsense_bearer_token"); - const updatedConnector = await response.json(); - const updatedConnectors = connectors.map(connector => - connector.id === connectorId ? updatedConnector : connector - ); - setConnectors(updatedConnectors); - updateConnectorSourceItems(updatedConnectors); - return updatedConnector; - } catch (err) { - console.error('Error updating search source connector:', err); - throw err; - } - }; + if (!token) { + throw new Error("No authentication token found"); + } - /** - * Delete a search source connector - */ - const deleteConnector = async (connectorId: number) => { - try { - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - throw new Error('No authentication token found'); - } + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, - { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } + if (!response.ok) { + throw new Error( + `Failed to delete connector: ${response.statusText}` + ); + } + + const updatedConnectors = connectors.filter( + (connector) => connector.id !== connectorId + ); + setConnectors(updatedConnectors); + updateConnectorSourceItems(updatedConnectors); + } catch (err) { + console.error("Error deleting search source connector:", err); + throw err; } - ); + }; - if (!response.ok) { - throw new Error(`Failed to delete connector: ${response.statusText}`); - } + /** + * Index content from a connector to a search space + */ + const indexConnector = async ( + connectorId: number, + searchSpaceId: string | number, + startDate?: string, + endDate?: string + ) => { + try { + const token = localStorage.getItem("surfsense_bearer_token"); - const updatedConnectors = connectors.filter(connector => connector.id !== connectorId); - setConnectors(updatedConnectors); - updateConnectorSourceItems(updatedConnectors); - } catch (err) { - console.error('Error deleting search source connector:', err); - throw err; - } - }; + if (!token) { + throw new Error("No authentication token found"); + } - /** - * Index content from a connector to a search space - */ - const indexConnector = async ( - connectorId: number, - searchSpaceId: string | number, - startDate?: string, - endDate?: string - ) => { - try { - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - throw new Error('No authentication token found'); - } + // Build query parameters + const params = new URLSearchParams({ + search_space_id: searchSpaceId.toString(), + }); + if (startDate) { + params.append("start_date", startDate); + } + if (endDate) { + params.append("end_date", endDate); + } - // Build query parameters - const params = new URLSearchParams({ search_space_id: searchSpaceId.toString() }); - if (startDate) { - params.append('start_date', startDate); - } - if (endDate) { - params.append('end_date', endDate); - } + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } + if (!response.ok) { + throw new Error( + `Failed to index connector content: ${response.statusText}` + ); + } + + const result = await response.json(); + + // Update the connector's last_indexed_at timestamp + const updatedConnectors = connectors.map((connector) => + connector.id === connectorId + ? { + ...connector, + last_indexed_at: new Date().toISOString(), + } + : connector + ); + setConnectors(updatedConnectors); + + return result; + } catch (err) { + console.error("Error indexing connector content:", err); + throw err; } - ); + }; - if (!response.ok) { - throw new Error(`Failed to index connector content: ${response.statusText}`); - } + /** + * Get connector source items - memoized to prevent unnecessary re-renders + */ + const getConnectorSourceItems = useCallback(() => { + return connectorSourceItems; + }, [connectorSourceItems]); - const result = await response.json(); - - // Update the connector's last_indexed_at timestamp - const updatedConnectors = connectors.map(connector => - connector.id === connectorId - ? { ...connector, last_indexed_at: new Date().toISOString() } - : connector - ); - setConnectors(updatedConnectors); - - return result; - } catch (err) { - console.error('Error indexing connector content:', err); - throw err; - } - }; - - /** - * Get connector source items - memoized to prevent unnecessary re-renders - */ - const getConnectorSourceItems = useCallback(() => { - return connectorSourceItems; - }, [connectorSourceItems]); - - return { - connectors, - isLoading, - error, - createConnector, - updateConnector, - deleteConnector, - indexConnector, - getConnectorSourceItems, - connectorSourceItems - }; -}; \ No newline at end of file + return { + connectors, + isLoading, + isLoaded, + error, + fetchConnectors, + createConnector, + updateConnector, + deleteConnector, + indexConnector, + getConnectorSourceItems, + connectorSourceItems, + }; +};