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 8e831adcd..dc0d9ff94 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
@@ -1,241 +1,241 @@
"use client";
-import { useChat, Message, CreateMessage } from "@ai-sdk/react";
+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 { ResearchMode } from "@/components/chat";
-import { useChatState, useChatAPI } from "@/hooks/useChat";
-import { Document } from "@/hooks/use-documents";
+import type { Document } from "@/hooks/use-documents";
+import { useChatAPI, useChatState } from "@/hooks/useChat";
export default function ResearchChatPageV2() {
- const { search_space_id, chat_id } = useParams();
- const router = useRouter();
+ const { search_space_id, chat_id } = useParams();
+ const router = useRouter();
- const chatIdParam = Array.isArray(chat_id) ? chat_id[0] : chat_id;
- const isNewChat = !chatIdParam;
+ const chatIdParam = Array.isArray(chat_id) ? chat_id[0] : chat_id;
+ const isNewChat = !chatIdParam;
- const {
- token,
- isLoading,
- setIsLoading,
- searchMode,
- setSearchMode,
- researchMode,
- setResearchMode,
- selectedConnectors,
- setSelectedConnectors,
- selectedDocuments,
- setSelectedDocuments,
- } = useChatState({
- search_space_id: search_space_id as string,
- chat_id: chatIdParam,
- });
+ const {
+ token,
+ isLoading,
+ setIsLoading,
+ searchMode,
+ setSearchMode,
+ researchMode,
+ setResearchMode,
+ selectedConnectors,
+ setSelectedConnectors,
+ selectedDocuments,
+ setSelectedDocuments,
+ } = useChatState({
+ search_space_id: search_space_id as string,
+ chat_id: chatIdParam,
+ });
- const { fetchChatDetails, updateChat, createChat } = useChatAPI({
- token,
- search_space_id: search_space_id as string,
- });
+ const { fetchChatDetails, updateChat, createChat } = useChatAPI({
+ token,
+ search_space_id: search_space_id as string,
+ });
- // Memoize document IDs to prevent infinite re-renders
- const documentIds = useMemo(() => {
- return selectedDocuments.map((doc) => doc.id);
- }, [selectedDocuments]);
+ // Memoize document IDs to prevent infinite re-renders
+ const documentIds = useMemo(() => {
+ return selectedDocuments.map((doc) => doc.id);
+ }, [selectedDocuments]);
- // Memoize connector types to prevent infinite re-renders
- const connectorTypes = useMemo(() => {
- return selectedConnectors;
- }, [selectedConnectors]);
+ // 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;
- }
+ // Unified localStorage management for chat state
+ interface ChatState {
+ selectedDocuments: Document[];
+ selectedConnectors: string[];
+ searchMode: "DOCUMENTS" | "CHUNKS";
+ researchMode: ResearchMode;
+ }
- const getChatStateStorageKey = (searchSpaceId: string, chatId: string) =>
- `surfsense_chat_state_${searchSpaceId}_${chatId}`;
+ const getChatStateStorageKey = (searchSpaceId: string, chatId: string) =>
+ `surfsense_chat_state_${searchSpaceId}_${chatId}`;
- const storeChatState = (
- searchSpaceId: string,
- chatId: string,
- state: ChatState
- ) => {
- const key = getChatStateStorageKey(searchSpaceId, chatId);
- localStorage.setItem(key, JSON.stringify(state));
- };
+ const storeChatState = (
+ searchSpaceId: string,
+ chatId: string,
+ state: ChatState,
+ ) => {
+ const key = getChatStateStorageKey(searchSpaceId, chatId);
+ localStorage.setItem(key, JSON.stringify(state));
+ };
- const restoreChatState = (
- searchSpaceId: string,
- chatId: string
- ): ChatState | null => {
- const key = getChatStateStorageKey(searchSpaceId, chatId);
- const stored = localStorage.getItem(key);
- if (stored) {
- localStorage.removeItem(key); // Clean up after restoration
- try {
- return JSON.parse(stored);
- } catch (error) {
- console.error("Error parsing stored chat state:", error);
- return null;
- }
- }
- return null;
- };
+ const restoreChatState = (
+ searchSpaceId: string,
+ chatId: string,
+ ): ChatState | null => {
+ const key = getChatStateStorageKey(searchSpaceId, chatId);
+ const stored = localStorage.getItem(key);
+ if (stored) {
+ localStorage.removeItem(key); // Clean up after restoration
+ try {
+ return JSON.parse(stored);
+ } catch (error) {
+ console.error("Error parsing stored chat state:", error);
+ return null;
+ }
+ }
+ return null;
+ };
- const handler = useChat({
- api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`,
- streamProtocol: "data",
- initialMessages: [],
- headers: {
- ...(token && { Authorization: `Bearer ${token}` }),
- },
- body: {
- data: {
- search_space_id: search_space_id,
- selected_connectors: connectorTypes,
- research_mode: researchMode,
- search_mode: searchMode,
- document_ids_to_add_in_context: documentIds,
- },
- },
- onError: (error) => {
- console.error("Chat error:", error);
- },
- });
+ const handler = useChat({
+ api: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chat`,
+ streamProtocol: "data",
+ initialMessages: [],
+ headers: {
+ ...(token && { Authorization: `Bearer ${token}` }),
+ },
+ body: {
+ data: {
+ search_space_id: search_space_id,
+ selected_connectors: connectorTypes,
+ research_mode: researchMode,
+ search_mode: searchMode,
+ document_ids_to_add_in_context: documentIds,
+ },
+ },
+ onError: (error) => {
+ console.error("Chat error:", error);
+ },
+ });
- const customHandlerAppend = async (
- message: Message | CreateMessage,
- chatRequestOptions?: { data?: any }
- ) => {
- 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,
- });
- router.replace(`/dashboard/${search_space_id}/v2/${newChatId}`);
- }
- return newChatId;
- };
+ const customHandlerAppend = async (
+ message: Message | CreateMessage,
+ chatRequestOptions?: { data?: any },
+ ) => {
+ 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,
+ });
+ router.replace(`/dashboard/${search_space_id}/v2/${newChatId}`);
+ }
+ return newChatId;
+ };
- useEffect(() => {
- if (token && !isNewChat && chatIdParam) {
- setIsLoading(true);
- loadChatData(chatIdParam);
- }
- }, [token, isNewChat, chatIdParam]);
+ useEffect(() => {
+ if (token && !isNewChat && chatIdParam) {
+ setIsLoading(true);
+ loadChatData(chatIdParam);
+ }
+ }, [token, isNewChat, chatIdParam]);
- // Restore chat state from localStorage on page load
- useEffect(() => {
- if (chatIdParam && search_space_id) {
- const restoredState = restoreChatState(
- search_space_id as string,
- chatIdParam
- );
- if (restoredState) {
- setSelectedDocuments(restoredState.selectedDocuments);
- setSelectedConnectors(restoredState.selectedConnectors);
- setSearchMode(restoredState.searchMode);
- setResearchMode(restoredState.researchMode);
- }
- }
- }, [
- chatIdParam,
- search_space_id,
- setSelectedDocuments,
- setSelectedConnectors,
- setSearchMode,
- setResearchMode,
- ]);
+ // Restore chat state from localStorage on page load
+ useEffect(() => {
+ if (chatIdParam && search_space_id) {
+ const restoredState = restoreChatState(
+ search_space_id as string,
+ chatIdParam,
+ );
+ if (restoredState) {
+ setSelectedDocuments(restoredState.selectedDocuments);
+ setSelectedConnectors(restoredState.selectedConnectors);
+ setSearchMode(restoredState.searchMode);
+ setResearchMode(restoredState.researchMode);
+ }
+ }
+ }, [
+ chatIdParam,
+ search_space_id,
+ setSelectedDocuments,
+ setSelectedConnectors,
+ setSearchMode,
+ setResearchMode,
+ ]);
- const loadChatData = async (chatId: string) => {
- try {
- const chatData = await fetchChatDetails(chatId);
- if (!chatData) return;
+ const loadChatData = async (chatId: string) => {
+ try {
+ const chatData = await fetchChatDetails(chatId);
+ if (!chatData) return;
- // Update configuration from chat data
- if (chatData.type) {
- setResearchMode(chatData.type as ResearchMode);
- }
+ // Update configuration from chat data
+ if (chatData.type) {
+ setResearchMode(chatData.type as ResearchMode);
+ }
- if (
- chatData.initial_connectors &&
- Array.isArray(chatData.initial_connectors)
- ) {
- setSelectedConnectors(chatData.initial_connectors);
- }
+ if (
+ chatData.initial_connectors &&
+ Array.isArray(chatData.initial_connectors)
+ ) {
+ setSelectedConnectors(chatData.initial_connectors);
+ }
- // Load existing messages
- if (chatData.messages && Array.isArray(chatData.messages)) {
- if (
- chatData.messages.length === 1 &&
- chatData.messages[0].role === "user"
- ) {
- // Single user message - append to trigger LLM response
- handler.append({
- role: "user",
- content: chatData.messages[0].content,
- });
- } else if (chatData.messages.length > 1) {
- // Multiple messages - set them all
- handler.setMessages(chatData.messages);
- }
- }
- } finally {
- setIsLoading(false);
- }
- };
+ // Load existing messages
+ if (chatData.messages && Array.isArray(chatData.messages)) {
+ if (
+ chatData.messages.length === 1 &&
+ chatData.messages[0].role === "user"
+ ) {
+ // Single user message - append to trigger LLM response
+ handler.append({
+ role: "user",
+ content: chatData.messages[0].content,
+ });
+ } else if (chatData.messages.length > 1) {
+ // Multiple messages - set them all
+ handler.setMessages(chatData.messages);
+ }
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
- // Auto-update chat when messages change (only for existing chats)
- useEffect(() => {
- if (
- !isNewChat &&
- chatIdParam &&
- handler.status === "ready" &&
- handler.messages.length > 0 &&
- handler.messages[handler.messages.length - 1]?.role === "assistant"
- ) {
- updateChat(
- chatIdParam,
- handler.messages,
- researchMode,
- selectedConnectors
- );
- }
- }, [handler.messages, handler.status, chatIdParam, isNewChat]);
+ // Auto-update chat when messages change (only for existing chats)
+ useEffect(() => {
+ if (
+ !isNewChat &&
+ chatIdParam &&
+ handler.status === "ready" &&
+ handler.messages.length > 0 &&
+ handler.messages[handler.messages.length - 1]?.role === "assistant"
+ ) {
+ updateChat(
+ chatIdParam,
+ handler.messages,
+ researchMode,
+ selectedConnectors,
+ );
+ }
+ }, [handler.messages, handler.status, chatIdParam, isNewChat]);
- if (isLoading) {
- return (
-
- );
- }
+ if (isLoading) {
+ return (
+
+ );
+ }
- return (
-
- );
+ return (
+
+ );
}
diff --git a/surfsense_web/components/chat_v2/ChatCitation.tsx b/surfsense_web/components/chat_v2/ChatCitation.tsx
index c5570b171..14b61c675 100644
--- a/surfsense_web/components/chat_v2/ChatCitation.tsx
+++ b/surfsense_web/components/chat_v2/ChatCitation.tsx
@@ -1,56 +1,62 @@
-"use client"
+"use client";
import React from "react";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
import { ExternalLink } from "lucide-react";
-export const CitationDisplay: React.FC<{index: number, node: any}> = ({index, node}) => {
+export const CitationDisplay: React.FC<{ index: number; node: any }> = ({
+ index,
+ node,
+}) => {
+ const truncateText = (text: string, maxLength: number = 200) => {
+ if (text.length <= maxLength) return text;
+ return text.substring(0, maxLength) + "...";
+ };
- const truncateText = (text: string, maxLength: number = 200) => {
- if (text.length <= maxLength) return text;
- return text.substring(0, maxLength) + '...';
- };
+ const handleUrlClick = (e: React.MouseEvent, url: string) => {
+ e.preventDefault();
+ e.stopPropagation();
+ window.open(url, "_blank", "noopener,noreferrer");
+ };
- const handleUrlClick = (e: React.MouseEvent, url: string) => {
- e.preventDefault();
- e.stopPropagation();
- window.open(url, '_blank', 'noopener,noreferrer');
- };
+ return (
+
+
+
+ {index + 1}
+
+
+
+ {/* External Link Button - Top Right */}
+ {node?.url && (
+
+ )}
- return (
-
-
-
- {index + 1}
-
-
-
- {/* External Link Button - Top Right */}
- {node?.url && (
-
- )}
-
- {/* Heading */}
-
- {node?.metadata?.group_name || 'Source'}
-
-
- {/* Source */}
-
- {node?.metadata?.title || 'Untitled'}
-
-
- {/* Body */}
-
- {truncateText(node?.text || 'No content available')}
-
-
-
- );
-}
\ No newline at end of file
+ {/* Heading */}
+
+ {node?.metadata?.group_name || "Source"}
+
+
+ {/* Source */}
+
+ {node?.metadata?.title || "Untitled"}
+
+
+ {/* Body */}
+
+ {truncateText(node?.text || "No content available")}
+
+
+
+ );
+};
diff --git a/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx b/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx
index a3b1ea97e..580052fc6 100644
--- a/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx
+++ b/surfsense_web/components/chat_v2/ChatFurtherQuestions.tsx
@@ -3,34 +3,43 @@
import { SuggestedQuestions } from "@llamaindex/chat-ui/widgets";
import { getAnnotationData, Message, useChatUI } from "@llamaindex/chat-ui";
import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
} from "@/components/ui/accordion";
-import { Card } from "../ui/card";
-export const ChatFurtherQuestions: React.FC<{message: Message}> = ({message}) => {
- const annotations: string[][] = getAnnotationData(message, "FURTHER_QUESTIONS");
- const { append, requestData } = useChatUI();
+export const ChatFurtherQuestions: React.FC<{ message: Message }> = ({
+ message,
+}) => {
+ const annotations: string[][] = getAnnotationData(
+ message,
+ "FURTHER_QUESTIONS",
+ );
+ const { append, requestData } = useChatUI();
- console.log('🔥 annotations', annotations);
-
+ if (annotations.length !== 1 || annotations[0].length === 0) {
+ return <>>;
+ }
- if (annotations.length !== 1 || annotations[0].length === 0) {
- return <>>;
- }
-
- return (
-
-
-
- Suggested Questions
-
-
-
-
-
-
- );
-};
\ No newline at end of file
+ return (
+
+
+
+ Suggested Questions
+
+
+
+
+
+
+ );
+};
diff --git a/surfsense_web/components/chat_v2/ChatInputGroup.tsx b/surfsense_web/components/chat_v2/ChatInputGroup.tsx
index 4aea225e4..d4070a2b6 100644
--- a/surfsense_web/components/chat_v2/ChatInputGroup.tsx
+++ b/surfsense_web/components/chat_v2/ChatInputGroup.tsx
@@ -4,19 +4,19 @@ import { ChatInput } from "@llamaindex/chat-ui";
import { FolderOpen, Check, Zap, Brain } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogTitle,
- DialogTrigger,
- DialogFooter,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogTitle,
+ DialogTrigger,
+ DialogFooter,
} from "@/components/ui/dialog";
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Suspense, useState, useCallback } from "react";
@@ -25,621 +25,593 @@ import { useDocuments, Document } from "@/hooks/use-documents";
import { DocumentsDataTable } from "@/components/chat_v2/DocumentsDataTable";
import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
import {
- getConnectorIcon,
- ConnectorButton as ConnectorButtonComponent,
+ getConnectorIcon,
+ ConnectorButton as ConnectorButtonComponent,
} from "@/components/chat/ConnectorComponents";
import { ResearchMode } from "@/components/chat";
import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
import React from "react";
const DocumentSelector = React.memo(
- ({
- onSelectionChange,
- selectedDocuments = [],
- }: {
- onSelectionChange?: (documents: Document[]) => void;
- selectedDocuments?: Document[];
- }) => {
- const { search_space_id } = useParams();
- const [isOpen, setIsOpen] = useState(false);
+ ({
+ onSelectionChange,
+ selectedDocuments = [],
+ }: {
+ onSelectionChange?: (documents: Document[]) => void;
+ selectedDocuments?: Document[];
+ }) => {
+ const { search_space_id } = useParams();
+ const [isOpen, setIsOpen] = useState(false);
- const { documents, loading, isLoaded, fetchDocuments } = useDocuments(
- Number(search_space_id),
- true
- );
+ const { documents, loading, isLoaded, fetchDocuments } = useDocuments(
+ Number(search_space_id),
+ true,
+ );
- const handleOpenChange = useCallback(
- (open: boolean) => {
- setIsOpen(open);
- if (open && !isLoaded) {
- fetchDocuments();
- }
- },
- [fetchDocuments, isLoaded]
- );
+ const handleOpenChange = useCallback(
+ (open: boolean) => {
+ setIsOpen(open);
+ if (open && !isLoaded) {
+ fetchDocuments();
+ }
+ },
+ [fetchDocuments, isLoaded],
+ );
- const handleSelectionChange = useCallback(
- (documents: Document[]) => {
- onSelectionChange?.(documents);
- },
- [onSelectionChange]
- );
+ const handleSelectionChange = useCallback(
+ (documents: Document[]) => {
+ onSelectionChange?.(documents);
+ },
+ [onSelectionChange],
+ );
- const handleDone = useCallback(() => {
- setIsOpen(false);
- }, []);
+ const handleDone = useCallback(() => {
+ setIsOpen(false);
+ }, []);
- const selectedCount = React.useMemo(
- () => selectedDocuments.length,
- [selectedDocuments.length]
- );
+ const selectedCount = React.useMemo(
+ () => selectedDocuments.length,
+ [selectedDocuments.length],
+ );
- return (
-