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 && (
-
-
-
-
- )}
- {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) => (
-
- ))}
-
+ return (
+ <>
+
+ {messages.length === 0 && (
+
+
+
-
- );
- })()}
- {/* 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 && (
+
+ )}
+
+
+ )
+ )}
+
+ );
+ })()}
+
+
+ {/* 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 */}
+
+
+
+
-
-
-
- );
- }
+
- 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 */}
-
-
- >
- );
+ {/* 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 (
+
+ );
+ }
+);
+
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,
+ };
+};