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 && (
-
-
-
-
- )}
- {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 && (
-
- )}
-
-
- )
- )}
-
- );
- })()}
-
-
- {/* 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 */}
-
-
-
-
- {/* Enhanced Document Selection Dialog */}
-
-
- {/* Connector Selection Dialog */}
-
-
- {/* 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) => (