diff --git a/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx similarity index 98% rename from surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx rename to surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx index dc0d9ff..2734373 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/v2/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx @@ -4,11 +4,11 @@ import { type CreateMessage, type Message, useChat } from "@ai-sdk/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo } from "react"; import type { ResearchMode } from "@/components/chat"; -import ChatInterface from "@/components/chat_v2/ChatInterface"; +import ChatInterface from "@/components/chat/ChatInterface"; import type { Document } from "@/hooks/use-documents"; import { useChatAPI, useChatState } from "@/hooks/useChat"; -export default function ResearchChatPageV2() { +export default function ResearcherPage() { const { search_space_id, chat_id } = useParams(); const router = useRouter(); 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 deleted file mode 100644 index 4d4aa74..0000000 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx +++ /dev/null @@ -1,2546 +0,0 @@ -"use client"; -import React, { - 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, -} 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, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - 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, -} from "@/components/chat"; -import { MarkdownViewer } from "@/components/markdown-viewer"; -import { Logo } from "@/components/Logo"; -import { useSearchSourceConnectors } from "@/hooks"; -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; -} - -interface ConnectorSource { - 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"; - -/** - * 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") => { - 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} - -
- ))} -
-
- ); -}; - -/** - * Button that displays selected connectors and opens connector selection dialog - */ -const ConnectorButton = ({ - selectedConnectors, - onClick, -}: { - selectedConnectors: string[]; - onClick: () => void; -}) => { - const { connectorSourceItems } = useSearchSourceConnectors(); - - return ( - - ); -}; - -/** - * Button that displays selected documents count and opens document selection dialog - */ -const DocumentSelectorButton = ({ - selectedDocuments, - onClick, - documentsCount, -}: { - selectedDocuments: number[]; - onClick: () => void; - documentsCount: number; -}) => { - 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: 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 || []; - - // 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 filteredSources = getFilteredSourcesSafe() || []; - const paginatedSources = getPaginatedSourcesSafe() || []; - - // 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}"

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

- {source.title} -

-

- {source.description} -

-
- -
-
- ))} - - {!expandedSources && - paginatedSources.length < filteredSources.length && ( - - )} - - {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, - isLoaded: isConnectorsLoaded, - fetchConnectors, - } = useSearchSourceConnectors(); - const { llmConfigs } = useLLMConfigs(); - const { preferences, updatePreferences } = useLLMPreferences(); - - const INITIAL_SOURCES_DISPLAY = 3; - - 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)); - - // Debounced search effect (proper implementation) - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedDocumentFilter(documentFilter); - setDocumentsPage(1); // Reset page when search changes - }, 300); - - return () => { - clearTimeout(handler); - }; - }, [documentFilter]); - - // 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) { - terminalMessagesRef.current.scrollTop = - terminalMessagesRef.current.scrollHeight; - } - }; - - // 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]); - }, []); - - // 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; - } - .no-shadow-input:focus-visible { - box-shadow: none !important; - outline: none !important; - } - .shadcn-selector { - transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); - border: 1px solid hsl(var(--border)); - background-color: transparent; - position: relative; - overflow: hidden; - } - .shadcn-selector:hover { - background-color: hsl(var(--muted)); - border-color: hsl(var(--primary) / 0.5); - } - .shadcn-selector:after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 0; - background: hsl(var(--primary) / 0.1); - transition: height 300ms ease; - } - .shadcn-selector:hover:after { - height: 100%; - } - .shadcn-selector-primary { - color: hsl(var(--primary)); - border-color: hsl(var(--primary) / 0.3); - } - .shadcn-selector-primary:hover { - border-color: hsl(var(--primary)); - background-color: hsl(var(--primary) / 0.1); - } - /* Fix for scrollbar layout shifts */ - html { - overflow-y: scroll; - } - body { - scrollbar-gutter: stable; - } - /* For Firefox */ - * { - scrollbar-width: thin; - } - /* For Webkit browsers */ - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - ::-webkit-scrollbar-track { - background: transparent; - } - ::-webkit-scrollbar-thumb { - background-color: rgba(155, 155, 155, 0.5); - border-radius: 20px; - } - /* Line clamp utility */ - .line-clamp-2 { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - } - /* Hide scrollbar by default, show on hover */ - .scrollbar-hover { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ - } - .scrollbar-hover::-webkit-scrollbar { - display: none; /* Chrome, Safari and Opera */ - } - .scrollbar-hover:hover { - -ms-overflow-style: auto; /* IE and Edge */ - scrollbar-width: thin; /* Firefox */ - } - .scrollbar-hover:hover::-webkit-scrollbar { - display: block; /* Chrome, Safari and Opera */ - height: 6px; - } - .scrollbar-hover:hover::-webkit-scrollbar-track { - background: hsl(var(--muted)); - border-radius: 3px; - } - .scrollbar-hover:hover::-webkit-scrollbar-thumb { - background: hsl(var(--muted-foreground) / 0.3); - border-radius: 3px; - } - .scrollbar-hover:hover::-webkit-scrollbar-thumb:hover { - background: hsl(var(--muted-foreground) / 0.5); - } - `; - document.head.appendChild(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 - }, - }); - - // 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); - - 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}` - ); - } - - 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 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); - } - }) - .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} - -
- )); - }; - - 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 - ) => ( - - ) - )} -
-
-
-
-
-
- ); - })()} - {/* 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 */} -
- -
-
-
-
- - {/* Reference for auto-scrolling */} -
-
- - ); -}; - -export default ChatPage; diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx deleted file mode 100644 index 5e18082..0000000 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; -import React, { useEffect } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import { Loader2 } from 'lucide-react'; - -const ResearcherPage = () => { - const router = useRouter(); - const { search_space_id } = useParams(); - const [isCreating, setIsCreating] = React.useState(true); - const [error, setError] = React.useState(null); - - useEffect(() => { - const createChat = async () => { - try { - // Get token from localStorage - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - setError('Authentication token not found'); - setIsCreating(false); - return; - } - - // Create a new chat - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ - type: "QNA", - title: "Untitled Chat", // Empty title initially - initial_connectors: [], // No default connectors - messages: [], - search_space_id: Number(search_space_id) - }) - }); - - if (!response.ok) { - throw new Error(`Failed to create chat: ${response.statusText}`); - } - - const data = await response.json(); - - // Redirect to the new chat page - router.push(`/dashboard/${search_space_id}/researcher/${data.id}`); - } catch (err) { - console.error('Error creating chat:', err); - setError(err instanceof Error ? err.message : 'Failed to create chat'); - setIsCreating(false); - } - }; - - createChat(); - }, [search_space_id, router]); - - if (error) { - return ( -
-
Error: {error}
- -
- ); - } - - return ( -
- -

Creating new research chat...

-
- ); -}; - -export default ResearcherPage; \ No newline at end of file diff --git a/surfsense_web/components/chat_v2/ChatCitation.tsx b/surfsense_web/components/chat/ChatCitation.tsx similarity index 100% rename from surfsense_web/components/chat_v2/ChatCitation.tsx rename to surfsense_web/components/chat/ChatCitation.tsx diff --git a/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx b/surfsense_web/components/chat/ChatFurtherQuestions.tsx similarity index 100% rename from surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx rename to surfsense_web/components/chat/ChatFurtherQuestions.tsx diff --git a/surfsense_web/components/chat_v2/ChatInputGroup.tsx b/surfsense_web/components/chat/ChatInputGroup.tsx similarity index 99% rename from surfsense_web/components/chat_v2/ChatInputGroup.tsx rename to surfsense_web/components/chat/ChatInputGroup.tsx index d4070a2..142f0bc 100644 --- a/surfsense_web/components/chat_v2/ChatInputGroup.tsx +++ b/surfsense_web/components/chat/ChatInputGroup.tsx @@ -22,7 +22,7 @@ import { Badge } from "@/components/ui/badge"; 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 { DocumentsDataTable } from "@/components/chat/DocumentsDataTable"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { getConnectorIcon, diff --git a/surfsense_web/components/chat_v2/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx similarity index 92% rename from surfsense_web/components/chat_v2/ChatInterface.tsx rename to surfsense_web/components/chat/ChatInterface.tsx index 327c215..684b319 100644 --- a/surfsense_web/components/chat_v2/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -6,9 +6,9 @@ import { ChatHandler, } from "@llamaindex/chat-ui"; import { Document } from "@/hooks/use-documents"; -import { ChatInputUI } from "@/components/chat_v2/ChatInputGroup"; +import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ResearchMode } from "@/components/chat"; -import { ChatMessagesUI } from "@/components/chat_v2/ChatMessages"; +import { ChatMessagesUI } from "@/components/chat/ChatMessages"; interface ChatInterfaceProps { handler: ChatHandler; diff --git a/surfsense_web/components/chat_v2/ChatMessages.tsx b/surfsense_web/components/chat/ChatMessages.tsx similarity index 82% rename from surfsense_web/components/chat_v2/ChatMessages.tsx rename to surfsense_web/components/chat/ChatMessages.tsx index c5f2c92..5720cf4 100644 --- a/surfsense_web/components/chat_v2/ChatMessages.tsx +++ b/surfsense_web/components/chat/ChatMessages.tsx @@ -7,16 +7,17 @@ import { Message, useChatUI, } from "@llamaindex/chat-ui"; -import TerminalDisplay from "@/components/chat_v2/ChatTerminal"; -import ChatSourcesDisplay from "@/components/chat_v2/ChatSources"; -import { CitationDisplay } from "@/components/chat_v2/ChatCitation"; -import { ChatFurtherQuestions } from "@/components/chat_v2/ChatFurtherQuestions"; +import TerminalDisplay from "@/components/chat/ChatTerminal"; +import ChatSourcesDisplay from "@/components/chat/ChatSources"; +import { CitationDisplay } from "@/components/chat/ChatCitation"; +import { ChatFurtherQuestions } from "@/components/chat/ChatFurtherQuestions"; export function ChatMessagesUI() { const { messages } = useChatUI(); return ( + {messages.map((message, index) => (