diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx index 9517431b4..afeb42475 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx @@ -50,6 +50,8 @@ import { SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; +import { useAtomValue } from "jotai"; +import { activeSearchSpaceChatsAtom } from "@/stores/chats/active-search-space-chats.atom"; export interface Chat { created_at: string; @@ -91,10 +93,10 @@ const MotionCard = motion(Card); export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) { const router = useRouter(); - const [chats, setChats] = useState([]); + // const [chats, setChats] = useState([]); const [filteredChats, setFilteredChats] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + // const [isFetching, setIsLoading] = useState(true); + // const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); @@ -103,6 +105,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [chatToDelete, setChatToDelete] = useState<{ id: number; title: string } | null>(null); const [isDeleting, setIsDeleting] = useState(false); + const {isFetching , data : chats, error} = useAtomValue(activeSearchSpaceChatsAtom); const chatsPerPage = 9; const searchParams = useSearchParams(); @@ -118,58 +121,67 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) } }, [searchParams]); - // Fetch chats from API + useEffect(() => { - const fetchChats = async () => { - try { - setIsLoading(true); + if (error) { + console.error("Error fetching chats:", error); + } + }, [error]); - // Get token from localStorage - const token = localStorage.getItem("surfsense_bearer_token"); + // Fetch chats from API + // useEffect(() => { + // const fetchChats = async () => { + // try { + // setIsLoading(true); - if (!token) { - setError("Authentication token not found. Please log in again."); - setIsLoading(false); - return; - } + // // Get token from localStorage + // const token = localStorage.getItem("surfsense_bearer_token"); - // Fetch all chats for this search space - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?search_space_id=${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - cache: "no-store", - } - ); + // if (!token) { + // setError("Authentication token not found. Please log in again."); + // setIsLoading(false); + // return; + // } - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(`Failed to fetch chats: ${response.status} ${errorData?.error || ""}`); - } + // // Fetch all chats for this search space + // const response = await fetch( + // `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?search_space_id=${searchSpaceId}`, + // { + // headers: { + // Authorization: `Bearer ${token}`, + // "Content-Type": "application/json", + // }, + // cache: "no-store", + // } + // ); - const data: Chat[] = await response.json(); - setChats(data); - setFilteredChats(data); - setError(null); - } catch (error) { - console.error("Error fetching chats:", error); - setError(error instanceof Error ? error.message : "Unknown error occurred"); - setChats([]); - setFilteredChats([]); - } finally { - setIsLoading(false); - } - }; + // if (!response.ok) { + // const errorData = await response.json().catch(() => null); + // throw new Error(`Failed to fetch chats: ${response.status} ${errorData?.error || ""}`); + // } - fetchChats(); - }, [searchSpaceId]); + // const data: Chat[] = await response.json(); + // setChats(data); + // setFilteredChats(data); + // setError(null); + // } catch (error) { + // console.error("Error fetching chats:", error); + // setError(error instanceof Error ? error.message : "Unknown error occurred"); + // setChats([]); + // setFilteredChats([]); + // } finally { + // setIsLoading(false); + // } + // }; + + // fetchChats(); + // }, [searchSpaceId]); // Filter and sort chats based on search query, type, and sort order useEffect(() => { - let result = [...chats]; + let result = [...(chats || [])]; + + console.log("chats", chats); // Filter by search term if (searchQuery) { @@ -201,42 +213,42 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) // Function to handle chat deletion const handleDeleteChat = async () => { - if (!chatToDelete) return; + // if (!chatToDelete) return; - setIsDeleting(true); - try { - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) { - setIsDeleting(false); - return; - } + // setIsDeleting(true); + // try { + // const token = localStorage.getItem("surfsense_bearer_token"); + // if (!token) { + // setIsDeleting(false); + // return; + // } - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatToDelete.id}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - } - ); + // const response = await fetch( + // `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatToDelete.id}`, + // { + // method: "DELETE", + // headers: { + // Authorization: `Bearer ${token}`, + // "Content-Type": "application/json", + // }, + // } + // ); - if (!response.ok) { - throw new Error(`Failed to delete chat: ${response.statusText}`); - } + // if (!response.ok) { + // throw new Error(`Failed to delete chat: ${response.statusText}`); + // } - // Close dialog and refresh chats - setDeleteDialogOpen(false); - setChatToDelete(null); + // // Close dialog and refresh chats + // setDeleteDialogOpen(false); + // setChatToDelete(null); - // Update local state by removing the deleted chat - setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatToDelete.id)); - } catch (error) { - console.error("Error deleting chat:", error); - } finally { - setIsDeleting(false); - } + // // Update local state by removing the deleted chat + // setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatToDelete.id)); + // } catch (error) { + // console.error("Error deleting chat:", error); + // } finally { + // setIsDeleting(false); + // } }; // Calculate pagination @@ -245,7 +257,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); // Get unique chat types for filter dropdown - const chatTypes = ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))]; + const chatTypes = chats ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] : []; return ( {/* Status Messages */} - {isLoading && ( + {isFetching && (
@@ -316,14 +328,14 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
)} - {error && !isLoading && ( + {error && !isFetching && (

Error loading chats

-

{error}

+

{error.message}

)} - {!isLoading && !error && filteredChats.length === 0 && ( + {!isFetching && !error && filteredChats.length === 0 && (

No chats found

@@ -336,7 +348,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) )} {/* Chat Grid */} - {!isLoading && !error && filteredChats.length > 0 && ( + {!isFetching && !error && filteredChats.length > 0 && (
{currentChats.map((chat, index) => ( @@ -422,7 +434,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) )} {/* Pagination */} - {!isLoading && !error && totalPages > 1 && ( + {!isFetching && !error && totalPages > 1 && ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index cc35bdb97..3ad1e94b5 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -1,9 +1,9 @@ "use client"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { Loader2, PanelRight } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { usePathname, useRouter } from "next/navigation"; +import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import type React from "react"; import { useEffect, useMemo, useState } from "react"; @@ -12,233 +12,275 @@ import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; import { ThemeTogglerComponent } from "@/components/theme/theme-toggle"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; -import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; import { useLLMPreferences } from "@/hooks/use-llm-configs"; import { cn } from "@/lib/utils"; import { activeChatIdAtom } from "@/stores/chats/active-chat.atom"; import { chatUIAtom } from "@/stores/chats/active-chat-ui.atom"; +import { activeSearchSpaceIdAtom } from "@/stores/seach-space/active-seach-space.atom"; export function DashboardClientLayout({ - children, - searchSpaceId, - navSecondary, - navMain, + children, + searchSpaceId, + navSecondary, + navMain, }: { - children: React.ReactNode; - searchSpaceId: string; - navSecondary: any[]; - navMain: any[]; + children: React.ReactNode; + searchSpaceId: string; + navSecondary: any[]; + navMain: any[]; }) { - const t = useTranslations("dashboard"); - const router = useRouter(); - const pathname = usePathname(); - const searchSpaceIdNum = Number(searchSpaceId); + const t = useTranslations("dashboard"); + const router = useRouter(); + const pathname = usePathname(); + const searchSpaceIdNum = Number(searchSpaceId); + const { search_space_id, chat_id } = useParams(); + const [chatUIState, setChatUIState] = useAtom(chatUIAtom); + const activeChatId = useAtomValue(activeChatIdAtom); + const setActiveSearchSpaceIdState = useSetAtom(activeSearchSpaceIdAtom); + const setActiveChatIdState = useSetAtom(activeChatIdAtom); + const [showIndicator, setShowIndicator] = useState(false); - const [chatUIState, setChatUIState] = useAtom(chatUIAtom); - const activeChatId = useAtomValue(activeChatIdAtom); - const [showIndicator, setShowIndicator] = useState(false); + const { isChatPannelOpen } = chatUIState; - const { isChatPannelOpen } = chatUIState; + // Check if we're on the researcher page + const isResearcherPage = pathname?.includes("/researcher"); - // Check if we're on the researcher page - const isResearcherPage = pathname?.includes("/researcher"); + // Show indicator when chat becomes active and panel is closed + useEffect(() => { + if (activeChatId && !isChatPannelOpen) { + setShowIndicator(true); + // Hide indicator after 5 seconds + const timer = setTimeout(() => setShowIndicator(false), 5000); + return () => clearTimeout(timer); + } else { + setShowIndicator(false); + } + }, [activeChatId, isChatPannelOpen]); - // Show indicator when chat becomes active and panel is closed - useEffect(() => { - if (activeChatId && !isChatPannelOpen) { - setShowIndicator(true); - // Hide indicator after 5 seconds - const timer = setTimeout(() => setShowIndicator(false), 5000); - return () => clearTimeout(timer); - } else { - setShowIndicator(false); - } - }, [activeChatId, isChatPannelOpen]); + const { loading, error, isOnboardingComplete } = + useLLMPreferences(searchSpaceIdNum); + const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false); - const { loading, error, isOnboardingComplete } = useLLMPreferences(searchSpaceIdNum); - const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false); + // Skip onboarding check if we're already on the onboarding page + const isOnboardingPage = pathname?.includes("/onboard"); - // Skip onboarding check if we're already on the onboarding page - const isOnboardingPage = pathname?.includes("/onboard"); + // Translate navigation items + const tNavMenu = useTranslations("nav_menu"); + const translatedNavMain = useMemo(() => { + return navMain.map((item) => ({ + ...item, + title: tNavMenu(item.title.toLowerCase().replace(/ /g, "_")), + items: item.items?.map((subItem: any) => ({ + ...subItem, + title: tNavMenu(subItem.title.toLowerCase().replace(/ /g, "_")), + })), + })); + }, [navMain, tNavMenu]); - // Translate navigation items - const tNavMenu = useTranslations("nav_menu"); - const translatedNavMain = useMemo(() => { - return navMain.map((item) => ({ - ...item, - title: tNavMenu(item.title.toLowerCase().replace(/ /g, "_")), - items: item.items?.map((subItem: any) => ({ - ...subItem, - title: tNavMenu(subItem.title.toLowerCase().replace(/ /g, "_")), - })), - })); - }, [navMain, tNavMenu]); + const translatedNavSecondary = useMemo(() => { + return navSecondary.map((item) => ({ + ...item, + title: + item.title === "All Search Spaces" + ? tNavMenu("all_search_spaces") + : item.title, + })); + }, [navSecondary, tNavMenu]); - const translatedNavSecondary = useMemo(() => { - return navSecondary.map((item) => ({ - ...item, - title: item.title === "All Search Spaces" ? tNavMenu("all_search_spaces") : item.title, - })); - }, [navSecondary, tNavMenu]); + const [open, setOpen] = useState(() => { + try { + const match = document.cookie.match(/(?:^|; )sidebar_state=([^;]+)/); + if (match) return match[1] === "true"; + } catch { + // ignore + } + return true; + }); - const [open, setOpen] = useState(() => { - try { - const match = document.cookie.match(/(?:^|; )sidebar_state=([^;]+)/); - if (match) return match[1] === "true"; - } catch { - // ignore - } - return true; - }); + useEffect(() => { + // Skip check if already on onboarding page + if (isOnboardingPage) { + setHasCheckedOnboarding(true); + return; + } - useEffect(() => { - // Skip check if already on onboarding page - if (isOnboardingPage) { - setHasCheckedOnboarding(true); - return; - } + // Only check once after preferences have loaded + if (!loading && !hasCheckedOnboarding) { + const onboardingComplete = isOnboardingComplete(); - // Only check once after preferences have loaded - if (!loading && !hasCheckedOnboarding) { - const onboardingComplete = isOnboardingComplete(); + if (!onboardingComplete) { + router.push(`/dashboard/${searchSpaceId}/onboard`); + } - if (!onboardingComplete) { - router.push(`/dashboard/${searchSpaceId}/onboard`); - } + setHasCheckedOnboarding(true); + } + }, [ + loading, + isOnboardingComplete, + isOnboardingPage, + router, + searchSpaceId, + hasCheckedOnboarding, + ]); - setHasCheckedOnboarding(true); - } - }, [ - loading, - isOnboardingComplete, - isOnboardingPage, - router, - searchSpaceId, - hasCheckedOnboarding, - ]); + // Synchronize active search space and chat IDs with URL + useEffect(() => { + const activeSeacrhSpaceId = + typeof search_space_id === "string" + ? search_space_id + : Array.isArray(search_space_id) && search_space_id.length > 0 + ? search_space_id[0] + : ""; + if (!activeSeacrhSpaceId) return; + setActiveSearchSpaceIdState(activeSeacrhSpaceId); + }, [search_space_id]); - // Show loading screen while checking onboarding status (only on first load) - if (!hasCheckedOnboarding && loading && !isOnboardingPage) { - return ( -
- - - {t("loading_config")} - {t("checking_llm_prefs")} - - - - - -
- ); - } + useEffect(() => { + const activeChatId = + typeof chat_id === "string" ? chat_id : Array.isArray(chat_id) && chat_id.length > 0 ? chat_id[0] : ""; + if (!activeChatId) return; + setActiveChatIdState(activeChatId); + }, [chat_id, search_space_id]); - // Show error screen if there's an error loading preferences (but not on onboarding page) - if (error && !hasCheckedOnboarding && !isOnboardingPage) { - return ( -
- - - - {t("config_error")} - - {t("failed_load_llm_config")} - - -

{error}

-
-
-
- ); - } + // Show loading screen while checking onboarding status (only on first load) + if (!hasCheckedOnboarding && loading && !isOnboardingPage) { + return ( +
+ + + + {t("loading_config")} + + {t("checking_llm_prefs")} + + + + + +
+ ); + } - return ( - - {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} - - -
-
-
-
-
- - - -
-
- - - {/* Only show artifacts toggle on researcher page */} - {isResearcherPage && ( - - { - setChatUIState((prev) => ({ - ...prev, - isChatPannelOpen: !isChatPannelOpen, - })); - setShowIndicator(false); - }} - className={cn( - "shrink-0 rounded-full p-2 transition-all duration-300 relative", - showIndicator - ? "bg-primary/20 hover:bg-primary/30 shadow-lg shadow-primary/25" - : "hover:bg-muted", - activeChatId && !showIndicator && "hover:bg-primary/10" - )} - title="Toggle Artifacts Panel" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - - - - + // Show error screen if there's an error loading preferences (but not on onboarding page) + if (error && !hasCheckedOnboarding && !isOnboardingPage) { + return ( +
+ + + + {t("config_error")} + + {t("failed_load_llm_config")} + + +

{error}

+
+
+
+ ); + } + + return ( + + {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} + + +
+
+
+
+
+ + + +
+
+ + + {/* Only show artifacts toggle on researcher page */} + {isResearcherPage && ( + + { + setChatUIState((prev) => ({ + ...prev, + isChatPannelOpen: !isChatPannelOpen, + })); + setShowIndicator(false); + }} + className={cn( + "shrink-0 rounded-full p-2 transition-all duration-300 relative", + showIndicator + ? "bg-primary/20 hover:bg-primary/30 shadow-lg shadow-primary/25" + : "hover:bg-muted", + activeChatId && + !showIndicator && + "hover:bg-primary/10" + )} + title="Toggle Artifacts Panel" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + + + {/* Pulsing indicator badge */} diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 9aa280eb7..8a2b5c851 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -1,46 +1,38 @@ "use client"; -import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; -import { useSetAtom } from "jotai"; +import { + type ChatHandler, + ChatSection as LlamaIndexChatSection, +} from "@llamaindex/chat-ui"; import { useParams } from "next/navigation"; -import { useEffect } from "react"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import type { Document } from "@/hooks/use-documents"; -import { activeChatIdAtom } from "@/stores/chats/active-chat.atom"; -import { ChatPanelContainer } from "./ChatPanel/ChatPanelContainer"; interface ChatInterfaceProps { - handler: ChatHandler; - onDocumentSelectionChange?: (documents: Document[]) => void; - selectedDocuments?: Document[]; - onConnectorSelectionChange?: (connectorTypes: string[]) => void; - selectedConnectors?: string[]; - searchMode?: "DOCUMENTS" | "CHUNKS"; - onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; - topK?: number; - onTopKChange?: (topK: number) => void; + handler: ChatHandler; + onDocumentSelectionChange?: (documents: Document[]) => void; + selectedDocuments?: Document[]; + onConnectorSelectionChange?: (connectorTypes: string[]) => void; + selectedConnectors?: string[]; + searchMode?: "DOCUMENTS" | "CHUNKS"; + onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; + topK?: number; + onTopKChange?: (topK: number) => void; } export default function ChatInterface({ - handler, - onDocumentSelectionChange, - selectedDocuments = [], - onConnectorSelectionChange, - selectedConnectors = [], - searchMode, - onSearchModeChange, - topK = 10, - onTopKChange, + handler, + onDocumentSelectionChange, + selectedDocuments = [], + onConnectorSelectionChange, + selectedConnectors = [], + searchMode, + onSearchModeChange, + topK = 10, + onTopKChange, }: ChatInterfaceProps) { - const { chat_id, search_space_id } = useParams(); - const setActiveChatIdState = useSetAtom(activeChatIdAtom); - - useEffect(() => { - const id = typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : ""; - if (!id) return; - setActiveChatIdState(id); - }, [chat_id, search_space_id]); + const { chat_id, search_space_id } = useParams(); return (