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 a0383d886..b9f28e56d 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 @@ -2,511 +2,473 @@ import { format } from "date-fns"; import { - Calendar, - ExternalLink, - MessageCircleMore, - MoreHorizontal, - Search, - Tag, - Trash2, + Calendar, + ExternalLink, + MessageCircleMore, + MoreHorizontal, + Search, + Tag, + Trash2, } from "lucide-react"; import { AnimatePresence, motion, type Variants } from "motion/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, } from "@/components/ui/pagination"; import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { useAtom, useAtomValue } from "jotai"; import { activeSearchSpaceChatsAtom } from "@/atoms/chats/chat-queries.atom"; import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutations.atom"; export interface Chat { - created_at: string; - id: number; - type: "DOCUMENT" | "CHAT"; - title: string; - search_space_id: number; - state_version: number; + created_at: string; + id: number; + type: "DOCUMENT" | "CHAT"; + title: string; + search_space_id: number; + state_version: number; } export interface ChatDetails { - type: "DOCUMENT" | "CHAT"; - title: string; - initial_connectors: string[]; - messages: any[]; - created_at: string; - id: number; - search_space_id: number; - state_version: number; + type: "DOCUMENT" | "CHAT"; + title: string; + initial_connectors: string[]; + messages: any[]; + created_at: string; + id: number; + search_space_id: number; + state_version: number; } interface ChatsPageClientProps { - searchSpaceId: string; + searchSpaceId: string; } const pageVariants: Variants = { - initial: { opacity: 0 }, - enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } }, - exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } }, + initial: { opacity: 0 }, + enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } }, + exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } }, }; const chatCardVariants: Variants = { - initial: { y: 20, opacity: 0 }, - animate: { y: 0, opacity: 1 }, - exit: { y: -20, opacity: 0 }, + initial: { y: 20, opacity: 0 }, + animate: { y: 0, opacity: 1 }, + exit: { y: -20, opacity: 0 }, }; const MotionCard = motion(Card); -export default function ChatsPageClient({ - searchSpaceId, -}: ChatsPageClientProps) { - const router = useRouter(); - const [filteredChats, setFilteredChats] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [selectedType, setSelectedType] = useState("all"); - const [sortOrder, setSortOrder] = useState("newest"); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [chatToDelete, setChatToDelete] = useState<{ - id: number; - title: string; - } | null>(null); - const { - isFetching: isFetchingChats, - data: chats, - error: fetchError, - } = useAtomValue(activeSearchSpaceChatsAtom); - const [ - { isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError } - ] = useAtom(deleteChatMutationAtom); +export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) { + const router = useRouter(); + const [filteredChats, setFilteredChats] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [selectedType, setSelectedType] = useState("all"); + const [sortOrder, setSortOrder] = useState("newest"); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [chatToDelete, setChatToDelete] = useState<{ + id: number; + title: string; + } | null>(null); + const { + isFetching: isFetchingChats, + data: chats, + error: fetchError, + } = useAtomValue(activeSearchSpaceChatsAtom); + const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] = + useAtom(deleteChatMutationAtom); - const chatsPerPage = 9; - const searchParams = useSearchParams(); + const chatsPerPage = 9; + const searchParams = useSearchParams(); - // Get initial page from URL params if it exists - useEffect(() => { - const pageParam = searchParams.get("page"); - if (pageParam) { - const pageNumber = parseInt(pageParam, 10); - if (!Number.isNaN(pageNumber) && pageNumber > 0) { - setCurrentPage(pageNumber); - } - } - }, [searchParams]); + // Get initial page from URL params if it exists + useEffect(() => { + const pageParam = searchParams.get("page"); + if (pageParam) { + const pageNumber = parseInt(pageParam, 10); + if (!Number.isNaN(pageNumber) && pageNumber > 0) { + setCurrentPage(pageNumber); + } + } + }, [searchParams]); - useEffect(() => { - if (fetchError) { - console.error("Error fetching chats:", fetchError); - } - }, [fetchError]); + useEffect(() => { + if (fetchError) { + console.error("Error fetching chats:", fetchError); + } + }, [fetchError]); - useEffect(() => { - if (deleteError) { - console.error("Error deleting chat:", deleteError); - } - }, [deleteError]); + useEffect(() => { + if (deleteError) { + console.error("Error deleting chat:", deleteError); + } + }, [deleteError]); - // Filter and sort chats based on search query, type, and sort order - useEffect(() => { - let result = [...(chats || [])]; + // Filter and sort chats based on search query, type, and sort order + useEffect(() => { + let result = [...(chats || [])]; - // Filter by search term - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter((chat) => - chat.title.toLowerCase().includes(query) - ); - } + // Filter by search term + if (searchQuery) { + const query = searchQuery.toLowerCase(); + result = result.filter((chat) => chat.title.toLowerCase().includes(query)); + } - // Filter by type - if (selectedType !== "all") { - result = result.filter((chat) => chat.type === selectedType); - } + // Filter by type + if (selectedType !== "all") { + result = result.filter((chat) => chat.type === selectedType); + } - // Sort chats - result.sort((a, b) => { - const dateA = new Date(a.created_at).getTime(); - const dateB = new Date(b.created_at).getTime(); + // Sort chats + result.sort((a, b) => { + const dateA = new Date(a.created_at).getTime(); + const dateB = new Date(b.created_at).getTime(); - return sortOrder === "newest" ? dateB - dateA : dateA - dateB; - }); + return sortOrder === "newest" ? dateB - dateA : dateA - dateB; + }); - setFilteredChats(result); - setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage))); + setFilteredChats(result); + setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage))); - // Reset to first page when filters change - if ( - currentPage !== 1 && - (searchQuery || selectedType !== "all" || sortOrder !== "newest") - ) { - setCurrentPage(1); - } - }, [chats, searchQuery, selectedType, sortOrder, currentPage]); + // Reset to first page when filters change + if (currentPage !== 1 && (searchQuery || selectedType !== "all" || sortOrder !== "newest")) { + setCurrentPage(1); + } + }, [chats, searchQuery, selectedType, sortOrder, currentPage]); - // Function to handle chat deletion - const handleDeleteChat = async () => { - if (!chatToDelete) return; + // Function to handle chat deletion + const handleDeleteChat = async () => { + if (!chatToDelete) return; - await deleteChat(chatToDelete.id); + await deleteChat(chatToDelete.id); - setDeleteDialogOpen(false); - setChatToDelete(null); - }; + setDeleteDialogOpen(false); + setChatToDelete(null); + }; - // Calculate pagination - const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page - const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page - const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); + // Calculate pagination + const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page + const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page + const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); - // Get unique chat types for filter dropdown - const chatTypes = chats - ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] - : []; + // Get unique chat types for filter dropdown + const chatTypes = chats ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] : []; - return ( - -
-
-

All Chats

-

- View, search, and manage all your chats. -

-
+ return ( + +
+
+

All Chats

+

View, search, and manage all your chats.

+
- {/* Filter and Search Bar */} -
-
-
- - setSearchQuery(e.target.value)} - /> -
+ {/* Filter and Search Bar */} +
+
+
+ + setSearchQuery(e.target.value)} + /> +
- -
+ +
-
- -
-
+
+ +
+
- {/* Status Messages */} - {isFetchingChats && ( -
-
-
-

Loading chats...

-
-
- )} + {/* Status Messages */} + {isFetchingChats && ( +
+
+
+

Loading chats...

+
+
+ )} - {fetchError && !isFetchingChats && ( -
-

Error loading chats

-

{fetchError.message}

-
- )} + {fetchError && !isFetchingChats && ( +
+

Error loading chats

+

{fetchError.message}

+
+ )} - {!isFetchingChats && !fetchError && filteredChats.length === 0 && ( -
- -

No chats found

-

- {searchQuery || selectedType !== "all" - ? "Try adjusting your search filters" - : "Start a new chat to get started"} -

-
- )} + {!isFetchingChats && !fetchError && filteredChats.length === 0 && ( +
+ +

No chats found

+

+ {searchQuery || selectedType !== "all" + ? "Try adjusting your search filters" + : "Start a new chat to get started"} +

+
+ )} - {/* Chat Grid */} - {!isFetchingChats && !fetchError && filteredChats.length > 0 && ( - -
- {currentChats.map((chat, index) => ( - - -
-
- - {chat.title || `Chat ${chat.id}`} - - - - - - {format(new Date(chat.created_at), "MMM d, yyyy")} - - - -
- - - - - - - router.push( - `/dashboard/${chat.search_space_id}/researcher/${chat.id}` - ) - } - > - - View Chat - - - { - e.stopPropagation(); - setChatToDelete({ - id: chat.id, - title: chat.title || `Chat ${chat.id}`, - }); - setDeleteDialogOpen(true); - }} - > - - Delete Chat - - - -
-
+ {/* Chat Grid */} + {!isFetchingChats && !fetchError && filteredChats.length > 0 && ( + +
+ {currentChats.map((chat, index) => ( + + +
+
+ + {chat.title || `Chat ${chat.id}`} + + + + + {format(new Date(chat.created_at), "MMM d, yyyy")} + + +
+ + + + + + + router.push( + `/dashboard/${chat.search_space_id}/researcher/${chat.id}` + ) + } + > + + View Chat + + + { + e.stopPropagation(); + setChatToDelete({ + id: chat.id, + title: chat.title || `Chat ${chat.id}`, + }); + setDeleteDialogOpen(true); + }} + > + + Delete Chat + + + +
+
- - - - {chat.type || "Unknown"} - - - -
- ))} -
-
- )} + + + + {chat.type || "Unknown"} + + + +
+ ))} +
+
+ )} - {/* Pagination */} - {!isFetchingChats && !fetchError && totalPages > 1 && ( - - - - { - e.preventDefault(); - if (currentPage > 1) setCurrentPage(currentPage - 1); - }} - className={ - currentPage <= 1 ? "pointer-events-none opacity-50" : "" - } - /> - + {/* Pagination */} + {!isFetchingChats && !fetchError && totalPages > 1 && ( + + + + { + e.preventDefault(); + if (currentPage > 1) setCurrentPage(currentPage - 1); + }} + className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""} + /> + - {Array.from({ length: totalPages }).map((_, index) => { - const pageNumber = index + 1; - const isVisible = - pageNumber === 1 || - pageNumber === totalPages || - (pageNumber >= currentPage - 1 && - pageNumber <= currentPage + 1); + {Array.from({ length: totalPages }).map((_, index) => { + const pageNumber = index + 1; + const isVisible = + pageNumber === 1 || + pageNumber === totalPages || + (pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1); - if (!isVisible) { - // Show ellipsis at appropriate positions - if (pageNumber === 2 || pageNumber === totalPages - 1) { - return ( - - - ... - - - ); - } - return null; - } + if (!isVisible) { + // Show ellipsis at appropriate positions + if (pageNumber === 2 || pageNumber === totalPages - 1) { + return ( + + ... + + ); + } + return null; + } - return ( - - { - e.preventDefault(); - setCurrentPage(pageNumber); - }} - isActive={pageNumber === currentPage} - > - {pageNumber} - - - ); - })} + return ( + + { + e.preventDefault(); + setCurrentPage(pageNumber); + }} + isActive={pageNumber === currentPage} + > + {pageNumber} + + + ); + })} - - { - e.preventDefault(); - if (currentPage < totalPages) - setCurrentPage(currentPage + 1); - }} - className={ - currentPage >= totalPages - ? "pointer-events-none opacity-50" - : "" - } - /> - - - - )} -
+ + { + e.preventDefault(); + if (currentPage < totalPages) setCurrentPage(currentPage + 1); + }} + className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""} + /> + + + + )} +
- {/* Delete Confirmation Dialog */} - - - - - - Delete Chat - - - Are you sure you want to delete{" "} - {chatToDelete?.title}? This - action cannot be undone. - - - - - - - - -
- ); + {/* Delete Confirmation Dialog */} + + + + + + Delete Chat + + + Are you sure you want to delete{" "} + {chatToDelete?.title}? This action cannot be + undone. + + + + + + + + + + ); } 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 40f6b2255..ee2922130 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -12,19 +12,9 @@ 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 "@/atoms/chats/chat-queries.atom"; @@ -32,255 +22,249 @@ import { chatUIAtom } from "@/atoms/chats/chat-uis.atom"; import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.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 { 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 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 { 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]); + // 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]); - 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]); + 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 loading screen while checking onboarding status (only on first load) - if (!hasCheckedOnboarding && loading && !isOnboardingPage) { - return ( -
- - - - {t("loading_config")} - - {t("checking_llm_prefs")} - - - - - -
- ); - } + // Show loading screen while checking onboarding status (only on first load) + if (!hasCheckedOnboarding && loading && !isOnboardingPage) { + return ( +
+ + + {t("loading_config")} + {t("checking_llm_prefs")} + + + + + +
+ ); + } - // 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 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 }} - > - - - - + 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/atoms/chats/chat-mutations.atom.ts b/surfsense_web/atoms/chats/chat-mutations.atom.ts index b1817faad..fbb30a01e 100644 --- a/surfsense_web/atoms/chats/chat-mutations.atom.ts +++ b/surfsense_web/atoms/chats/chat-mutations.atom.ts @@ -7,28 +7,31 @@ import { toast } from "sonner"; import { Chat } from "@/app/dashboard/[search_space_id]/chats/chats-client"; export const deleteChatMutationAtom = atomWithMutation((get) => { - const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const searchSpaceId = get(activeSearchSpaceIdAtom); + const authToken = localStorage.getItem("surfsense_bearer_token"); - return { - mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""), - enabled: !!searchSpaceId && !!authToken, - mutationFn: async (chatId: number) => { - if (!authToken) { - throw new Error("No authentication token found"); - } - if (!searchSpaceId) { - throw new Error("No search space id found"); - } + return { + mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""), + enabled: !!searchSpaceId && !!authToken, + mutationFn: async (chatId: number) => { + if (!authToken) { + throw new Error("No authentication token found"); + } + if (!searchSpaceId) { + throw new Error("No search space id found"); + } - return deleteChat(chatId, authToken); - }, + return deleteChat(chatId, authToken); + }, - onSuccess: (_, chatId) => { - toast.success("Chat deleted successfully"); - queryClient.setQueryData(cacheKeys.activeSearchSpace.chats(searchSpaceId!), (oldData: Chat[]) => { - return oldData.filter((chat) => chat.id !== chatId); - }); - }, - }; + onSuccess: (_, chatId) => { + toast.success("Chat deleted successfully"); + queryClient.setQueryData( + cacheKeys.activeSearchSpace.chats(searchSpaceId!), + (oldData: Chat[]) => { + return oldData.filter((chat) => chat.id !== chatId); + } + ); + }, + }; }); diff --git a/surfsense_web/atoms/chats/chat-queries.atom.ts b/surfsense_web/atoms/chats/chat-queries.atom.ts index 6cf8139d2..2463b65ef 100644 --- a/surfsense_web/atoms/chats/chat-queries.atom.ts +++ b/surfsense_web/atoms/chats/chat-queries.atom.ts @@ -2,63 +2,60 @@ import { atom } from "jotai"; import { atomWithQuery } from "jotai-tanstack-query"; import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client"; -import { - fetchChatDetails, - fetchChatsBySearchSpace, -} from "@/lib/apis/chats.api"; +import { fetchChatDetails, fetchChatsBySearchSpace } from "@/lib/apis/chats.api"; import { getPodcastByChatId } from "@/lib/apis/podcasts.api"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; type ActiveChatState = { - chatId: string | null; - chatDetails: ChatDetails | null; - podcast: PodcastItem | null; + chatId: string | null; + chatDetails: ChatDetails | null; + podcast: PodcastItem | null; }; export const activeChatIdAtom = atom(null); export const activeChatAtom = atomWithQuery((get) => { - const activeChatId = get(activeChatIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const activeChatId = get(activeChatIdAtom); + const authToken = localStorage.getItem("surfsense_bearer_token"); - return { - queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""), - enabled: !!activeChatId && !!authToken, - queryFn: async () => { - if (!authToken) { - throw new Error("No authentication token found"); - } - if (!activeChatId) { - throw new Error("No active chat id found"); - } + return { + queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""), + enabled: !!activeChatId && !!authToken, + queryFn: async () => { + if (!authToken) { + throw new Error("No authentication token found"); + } + if (!activeChatId) { + throw new Error("No active chat id found"); + } - const [podcast, chatDetails] = await Promise.all([ - getPodcastByChatId(activeChatId, authToken), - fetchChatDetails(activeChatId, authToken), - ]); + const [podcast, chatDetails] = await Promise.all([ + getPodcastByChatId(activeChatId, authToken), + fetchChatDetails(activeChatId, authToken), + ]); - return { chatId: activeChatId, chatDetails, podcast }; - }, - }; + return { chatId: activeChatId, chatDetails, podcast }; + }, + }; }); export const activeSearchSpaceChatsAtom = atomWithQuery((get) => { - const searchSpaceId = get(activeSearchSpaceIdAtom); - const authToken = localStorage.getItem("surfsense_bearer_token"); + const searchSpaceId = get(activeSearchSpaceIdAtom); + const authToken = localStorage.getItem("surfsense_bearer_token"); - return { - queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""), - enabled: !!searchSpaceId && !!authToken, - queryFn: async () => { - if (!authToken) { - throw new Error("No authentication token found"); - } - if (!searchSpaceId) { - throw new Error("No search space id found"); - } + return { + queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""), + enabled: !!searchSpaceId && !!authToken, + queryFn: async () => { + if (!authToken) { + throw new Error("No authentication token found"); + } + if (!searchSpaceId) { + throw new Error("No search space id found"); + } - return fetchChatsBySearchSpace(searchSpaceId, authToken); - }, - }; + return fetchChatsBySearchSpace(searchSpaceId, authToken); + }, + }; }); diff --git a/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts b/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts index dcdf6d550..4bccf496f 100644 --- a/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts +++ b/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts @@ -1,3 +1,3 @@ import { atom } from "jotai"; -export const activeSearchSpaceIdAtom = atom(null); \ No newline at end of file +export const activeSearchSpaceIdAtom = atom(null); diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 8a2b5c851..799e45ef6 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -1,38 +1,35 @@ "use client"; -import { - type ChatHandler, - ChatSection as LlamaIndexChatSection, -} from "@llamaindex/chat-ui"; +import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; import { useParams } from "next/navigation"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import type { Document } from "@/hooks/use-documents"; 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 { chat_id, search_space_id } = useParams(); return ( diff --git a/surfsense_web/contracts/types/auth.types.ts b/surfsense_web/contracts/types/auth.types.ts index 507ea18fc..318b656c9 100644 --- a/surfsense_web/contracts/types/auth.types.ts +++ b/surfsense_web/contracts/types/auth.types.ts @@ -1,32 +1,32 @@ import { z } from "zod"; export const loginRequest = z.object({ - email: z.string().email(), - password: z.string().min(1), - grant_type: z.string().optional(), + email: z.string().email(), + password: z.string().min(1), + grant_type: z.string().optional(), }); export const loginResponse = z.object({ - access_token: z.string(), - token_type: z.string(), + access_token: z.string(), + token_type: z.string(), }); export const registerRequest = z.object({ - email: z.string().email(), - password: z.string().min(1), - is_active: z.boolean().optional(), - is_superuser: z.boolean().optional(), - is_verified: z.boolean().optional(), + email: z.string().email(), + password: z.string().min(1), + is_active: z.boolean().optional(), + is_superuser: z.boolean().optional(), + is_verified: z.boolean().optional(), }); export const registerResponse = z.object({ - id: z.number(), - email: z.string().email(), - is_active: z.boolean(), - is_superuser: z.boolean(), - is_verified: z.boolean(), - pages_limit: z.number(), - pages_used: z.number(), + id: z.number(), + email: z.string().email(), + is_active: z.boolean(), + is_superuser: z.boolean(), + is_verified: z.boolean(), + pages_limit: z.number(), + pages_used: z.number(), }); export type LoginRequest = z.infer; diff --git a/surfsense_web/contracts/types/chat.types.ts b/surfsense_web/contracts/types/chat.types.ts index 3e7eb6911..7a8767940 100644 --- a/surfsense_web/contracts/types/chat.types.ts +++ b/surfsense_web/contracts/types/chat.types.ts @@ -5,43 +5,45 @@ import { paginationQueryParams } from "."; export const chatTypeEnum = z.enum(["QNA"]); export const chatSummary = z.object({ - created_at: z.string(), - id: z.number(), - type: chatTypeEnum, - title: z.string(), - search_space_id: z.number(), - state_version: z.number(), + created_at: z.string(), + id: z.number(), + type: chatTypeEnum, + title: z.string(), + search_space_id: z.number(), + state_version: z.number(), }); export const chatDetails = chatSummary.extend({ - initial_connectors: z.array(z.string()), - messages: z.array(z.any()), + initial_connectors: z.array(z.string()), + messages: z.array(z.any()), }); export const getChatDetailsRequest = chatSummary.pick({ id: true }); -export const getChatsBySearchSpaceRequest = chatSummary.pick({ - search_space_id: true, -}).merge(paginationQueryParams); +export const getChatsBySearchSpaceRequest = chatSummary + .pick({ + search_space_id: true, + }) + .merge(paginationQueryParams); export const deleteChatResponse = z.object({ - message: z.literal("Chat deleted successfully"), + message: z.literal("Chat deleted successfully"), }); export const deleteChatRequest = chatSummary.pick({ id: true }); -export const createChatRequest = chatDetails - .omit({ created_at: true, id: true, state_version: true }); +export const createChatRequest = chatDetails.omit({ + created_at: true, + id: true, + state_version: true, +}); -export const updateChatRequest = chatDetails - .omit({ created_at: true, state_version: true }); +export const updateChatRequest = chatDetails.omit({ created_at: true, state_version: true }); export type ChatSummary = z.infer; export type ChatDetails = z.infer & { messages: Message[] }; export type GetChatDetailsRequest = z.infer; -export type GetChatsBySearchSpaceRequest = z.infer< - typeof getChatsBySearchSpaceRequest ->; +export type GetChatsBySearchSpaceRequest = z.infer; export type DeleteChatResponse = z.infer; export type DeleteChatRequest = z.infer; export type CreateChatRequest = z.infer; diff --git a/surfsense_web/contracts/types/index.ts b/surfsense_web/contracts/types/index.ts index d6c65b5ec..d00f7903f 100644 --- a/surfsense_web/contracts/types/index.ts +++ b/surfsense_web/contracts/types/index.ts @@ -1,8 +1,8 @@ import { z } from "zod"; export const paginationQueryParams = z.object({ - limit: z.number().optional(), - skip: z.number().optional(), + limit: z.number().optional(), + skip: z.number().optional(), }); export type PaginationQueryParams = z.infer; diff --git a/surfsense_web/lib/apis/auth-api.service.ts b/surfsense_web/lib/apis/auth-api.service.ts index e283c2965..a9eea9be7 100644 --- a/surfsense_web/lib/apis/auth-api.service.ts +++ b/surfsense_web/lib/apis/auth-api.service.ts @@ -1,44 +1,35 @@ import { - loginRequest, - LoginRequest, - loginResponse, - registerRequest, - RegisterRequest, - registerResponse, + loginRequest, + LoginRequest, + loginResponse, + registerRequest, + RegisterRequest, + registerResponse, } from "@/contracts/types/auth.types"; import { baseApiService } from "./base-api.service"; export class AuthApiService { - login = async (request: LoginRequest) => { - // Validate the request - const parsedRequest = loginRequest.safeParse(request); + login = async (request: LoginRequest) => { + // Validate the request + const parsedRequest = loginRequest.safeParse(request); - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - return baseApiService.post( - `/auth/jwt/login`, - parsedRequest.data, - loginResponse, - { - contentType: "application/x-www-form-urlencoded", - } - ); - }; + return baseApiService.post(`/auth/jwt/login`, parsedRequest.data, loginResponse, { + contentType: "application/x-www-form-urlencoded", + }); + }; - register = async (request: RegisterRequest) => { - // Validate the request - const parsedRequest = registerRequest.safeParse(request); + register = async (request: RegisterRequest) => { + // Validate the request + const parsedRequest = registerRequest.safeParse(request); - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - return baseApiService.post( - `/auth/register`, - parsedRequest.data, - registerResponse - ); - }; + return baseApiService.post(`/auth/register`, parsedRequest.data, registerResponse); + }; } diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index f94481547..777da0094 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -1,179 +1,163 @@ import z from "zod"; -import { - AppError, - AuthenticationError, - AuthorizationError, - ValidationError, -} from "../error"; +import { AppError, AuthenticationError, AuthorizationError, ValidationError } from "../error"; export type RequestOptions = { - method: "GET" | "POST" | "PUT" | "DELETE"; - headers?: Record; - contentType?: "application/json" | "application/x-www-form-urlencoded"; - signal?: AbortSignal; - body?: any; - // Add more options as needed + method: "GET" | "POST" | "PUT" | "DELETE"; + headers?: Record; + contentType?: "application/json" | "application/x-www-form-urlencoded"; + signal?: AbortSignal; + body?: any; + // Add more options as needed }; export class BaseApiService { - bearerToken: string; - baseUrl: string; + bearerToken: string; + baseUrl: string; - constructor(bearerToken: string, baseUrl: string) { - this.bearerToken = bearerToken; - this.baseUrl = baseUrl; - } + constructor(bearerToken: string, baseUrl: string) { + this.bearerToken = bearerToken; + this.baseUrl = baseUrl; + } - setBearerToken(bearerToken: string) { - this.bearerToken = bearerToken; - } + setBearerToken(bearerToken: string) { + this.bearerToken = bearerToken; + } - async request( - url: string, - body?: any, - responseSchema?: z.ZodSchema, - options?: RequestOptions - ) : Promise { - const defaultOptions: RequestOptions = { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.bearerToken}`, - }, - method: "GET", - }; + async request( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: RequestOptions + ): Promise { + const defaultOptions: RequestOptions = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.bearerToken}`, + }, + method: "GET", + }; - const mergedOptions: RequestOptions = { - ...defaultOptions, - ...(options ?? {}), - headers: { - ...defaultOptions.headers, - ...(options?.headers ?? {}), - }, - }; + const mergedOptions: RequestOptions = { + ...defaultOptions, + ...(options ?? {}), + headers: { + ...defaultOptions.headers, + ...(options?.headers ?? {}), + }, + }; - let requestBody; + let requestBody; - // Serialize body - if (body) { - if ( - mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === - "application/json" - ) { - requestBody = JSON.stringify(body); - } + // Serialize body + if (body) { + if (mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === "application/json") { + requestBody = JSON.stringify(body); + } - if ( - mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === - "application/x-www-form-urlencoded" - ) { - requestBody = new URLSearchParams(body); - } + if ( + mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === + "application/x-www-form-urlencoded" + ) { + requestBody = new URLSearchParams(body); + } - mergedOptions.body = requestBody; - } + mergedOptions.body = requestBody; + } - if (!this.baseUrl) { - throw new AppError("Base URL is not set."); - } + if (!this.baseUrl) { + throw new AppError("Base URL is not set."); + } - if (!this.bearerToken) { - throw new AuthenticationError( - "You are not authenticated. Please login again." - ); - } + if (!this.bearerToken) { + throw new AuthenticationError("You are not authenticated. Please login again."); + } - const fullUrl = new URL(url, this.baseUrl).toString(); + const fullUrl = new URL(url, this.baseUrl).toString(); - const response = await fetch(fullUrl, mergedOptions); + const response = await fetch(fullUrl, mergedOptions); - if (!response.ok) { - if (response.status === 401) { - throw new AuthenticationError( - "You are not authenticated. Please login again." - ); - } + if (!response.ok) { + if (response.status === 401) { + throw new AuthenticationError("You are not authenticated. Please login again."); + } - if (response.status === 403) { - throw new AuthorizationError( - "You don't have permission to access this resource." - ); - } + if (response.status === 403) { + throw new AuthorizationError("You don't have permission to access this resource."); + } - throw new AppError(`API Error: ${response.statusText}`); - } + throw new AppError(`API Error: ${response.statusText}`); + } - let data; + let data; - try { - data = await response.json(); - } catch (error) { - throw new AppError(`Failed to parse response as JSON: ${error}`); - } + try { + data = await response.json(); + } catch (error) { + throw new AppError(`Failed to parse response as JSON: ${error}`); + } - if (!responseSchema) { - return data; - } + if (!responseSchema) { + return data; + } - const parsedData = responseSchema.safeParse(data); + const parsedData = responseSchema.safeParse(data); - if (!parsedData.success) { - throw new ValidationError( - `Invalid response: ${parsedData.error.message}` - ); - } + if (!parsedData.success) { + throw new ValidationError(`Invalid response: ${parsedData.error.message}`); + } - return parsedData.data; - } + return parsedData.data; + } - async get( - url: string, - responseSchema?: z.ZodSchema, - options?: Omit - ) { - return this.request(url, undefined, responseSchema, { - ...options, - method: "GET", - }); - } + async get( + url: string, + responseSchema?: z.ZodSchema, + options?: Omit + ) { + return this.request(url, undefined, responseSchema, { + ...options, + method: "GET", + }); + } - async post( - url: string, - body?: any, - responseSchema?: z.ZodSchema, - options?: Omit - ) { - return this.request(url, body, responseSchema, { - ...options, - method: "POST", - }); - } + async post( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: Omit + ) { + return this.request(url, body, responseSchema, { + ...options, + method: "POST", + }); + } - async put( - url: string, - body?: any, - responseSchema?: z.ZodSchema, - options?: Omit - ) { - return this.request(url, body, responseSchema, { - ...options, - method: "PUT", - }); - } + async put( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: Omit + ) { + return this.request(url, body, responseSchema, { + ...options, + method: "PUT", + }); + } - async delete( - url: string, - body?: any, - responseSchema?: z.ZodSchema, - options?: Omit - ) { - return this.request(url, body, responseSchema, { - ...options, - method: "DELETE", - }); - } + async delete( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: Omit + ) { + return this.request(url, body, responseSchema, { + ...options, + method: "DELETE", + }); + } } export const baseApiService = new BaseApiService( - typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "", - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "" + typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "", + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "" ); diff --git a/surfsense_web/lib/apis/chats-api.service.api.ts b/surfsense_web/lib/apis/chats-api.service.api.ts index 65dd88413..4019d35ea 100644 --- a/surfsense_web/lib/apis/chats-api.service.api.ts +++ b/surfsense_web/lib/apis/chats-api.service.api.ts @@ -1,114 +1,103 @@ import { ResearchMode } from "@/components/chat/types"; import { Message } from "@ai-sdk/react"; import { - chatDetails, - chatSummary, - createChatRequest, - CreateChatRequest, - deleteChatRequest, - DeleteChatRequest, - getChatDetailsRequest, - GetChatDetailsRequest, - getChatsBySearchSpaceRequest, - GetChatsBySearchSpaceRequest, - deleteChatResponse, - UpdateChatRequest, - updateChatRequest, + chatDetails, + chatSummary, + createChatRequest, + CreateChatRequest, + deleteChatRequest, + DeleteChatRequest, + getChatDetailsRequest, + GetChatDetailsRequest, + getChatsBySearchSpaceRequest, + GetChatsBySearchSpaceRequest, + deleteChatResponse, + UpdateChatRequest, + updateChatRequest, } from "@/contracts/types/chat.types"; import { z } from "zod"; import { baseApiService } from "./base-api.service"; export class ChatApiService { - fetchChatDetails = async ( - request: GetChatDetailsRequest - ) => { - // Validate the request - const parsedRequest = getChatDetailsRequest.safeParse(request); + fetchChatDetails = async (request: GetChatDetailsRequest) => { + // Validate the request + const parsedRequest = getChatDetailsRequest.safeParse(request); - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - return baseApiService.get(`/api/v1/chats/${request.id}`, chatDetails); - }; + return baseApiService.get(`/api/v1/chats/${request.id}`, chatDetails); + }; - fetchChatsBySearchSpace = async ( - request: GetChatsBySearchSpaceRequest - )=> { - // Validate the request - const parsedRequest = getChatsBySearchSpaceRequest.safeParse(request); + fetchChatsBySearchSpace = async (request: GetChatsBySearchSpaceRequest) => { + // Validate the request + const parsedRequest = getChatsBySearchSpaceRequest.safeParse(request); - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - return baseApiService.get( - `/api/v1/chats?search_space_id=${request.search_space_id}`, - z.array(chatSummary) - ); - }; + return baseApiService.get( + `/api/v1/chats?search_space_id=${request.search_space_id}`, + z.array(chatSummary) + ); + }; - deleteChat = async (request: DeleteChatRequest) => { - // Validate the request - const parsedRequest = deleteChatRequest.safeParse(request); + deleteChat = async (request: DeleteChatRequest) => { + // Validate the request + const parsedRequest = deleteChatRequest.safeParse(request); - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } - - - return baseApiService.delete(`/api/v1/chats/${request.id}`, undefined, deleteChatResponse); - }; + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - createChat = async ( - request: CreateChatRequest - ) => { - // Validate the request - const parsedRequest = createChatRequest.safeParse(request); + return baseApiService.delete(`/api/v1/chats/${request.id}`, undefined, deleteChatResponse); + }; - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } + createChat = async (request: CreateChatRequest) => { + // Validate the request + const parsedRequest = createChatRequest.safeParse(request); - const { type, title, initial_connectors, messages, search_space_id } = - parsedRequest.data; + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - return baseApiService.post( - `/api/v1/chats`, - { - type, - title, - initial_connectors, - messages, - search_space_id, - }, - chatSummary - ); - }; + const { type, title, initial_connectors, messages, search_space_id } = parsedRequest.data; - updateChat = async ( - request: UpdateChatRequest - ) => { - // Validate the request - const parsedRequest = updateChatRequest.safeParse(request); + return baseApiService.post( + `/api/v1/chats`, + { + type, + title, + initial_connectors, + messages, + search_space_id, + }, + chatSummary + ); + }; - if (!parsedRequest.success) { - throw new Error(`Invalid request: ${parsedRequest.error.message}`); - } + updateChat = async (request: UpdateChatRequest) => { + // Validate the request + const parsedRequest = updateChatRequest.safeParse(request); - const { type, title, initial_connectors, messages, search_space_id, id } = - parsedRequest.data; + if (!parsedRequest.success) { + throw new Error(`Invalid request: ${parsedRequest.error.message}`); + } - return baseApiService.put( - `/api/v1/chats/${id}`, - { - type, - title, - initial_connectors, - messages, - search_space_id, - }, - chatSummary - ); - }; + const { type, title, initial_connectors, messages, search_space_id, id } = parsedRequest.data; + + return baseApiService.put( + `/api/v1/chats/${id}`, + { + type, + title, + initial_connectors, + messages, + search_space_id, + }, + chatSummary + ); + }; } diff --git a/surfsense_web/lib/apis/chats.api.ts b/surfsense_web/lib/apis/chats.api.ts index ddd1704ae..c5e90ecd5 100644 --- a/surfsense_web/lib/apis/chats.api.ts +++ b/surfsense_web/lib/apis/chats.api.ts @@ -1,167 +1,157 @@ -import type { - Chat, - ChatDetails, -} from "@/app/dashboard/[search_space_id]/chats/chats-client"; +import type { Chat, ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import { ResearchMode } from "@/components/chat/types"; import { Message } from "@ai-sdk/react"; export const fetchChatDetails = async ( - chatId: string, - authToken: string + chatId: string, + authToken: string ): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number( - chatId - )}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + } + ); - 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}`); + } - return await response.json(); + return await response.json(); }; export const fetchChatsBySearchSpace = async ( - searchSpaceId: string, - authToken: string + searchSpaceId: string, + authToken: string ): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?search_space_id=${searchSpaceId}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - } - ); - if (!response.ok) { - throw new Error(`Failed to fetch chats: ${response.statusText}`); - } + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?search_space_id=${searchSpaceId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + } + ); + if (!response.ok) { + throw new Error(`Failed to fetch chats: ${response.statusText}`); + } - return await response.json(); + return await response.json(); }; export const deleteChat = async (chatId: number, authToken: string) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); - if (!response.ok) { - throw new Error(`Failed to delete chat: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Failed to delete chat: ${response.statusText}`); + } - return await response.json(); + return await response.json(); }; export const createChat = async ( - initialMessage: string, - researchMode: ResearchMode, - selectedConnectors: string[], - authToken: string, - searchSpaceId: number + initialMessage: string, + researchMode: ResearchMode, + selectedConnectors: string[], + authToken: string, + searchSpaceId: number ): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify({ - type: researchMode, - title: "Untitled Chat", - initial_connectors: selectedConnectors, - messages: [ - { - role: "user", - content: initialMessage, - }, - ], - search_space_id: searchSpaceId, - }), - } - ); + const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ + type: researchMode, + title: "Untitled Chat", + initial_connectors: selectedConnectors, + messages: [ + { + role: "user", + content: initialMessage, + }, + ], + search_space_id: searchSpaceId, + }), + }); - if (!response.ok) { - throw new Error(`Failed to create chat: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Failed to create chat: ${response.statusText}`); + } - return await response.json(); + return await response.json(); }; export const updateChat = async ( - chatId: string, - messages: Message[], - researchMode: ResearchMode, - selectedConnectors: string[], - authToken: string, - searchSpaceId: number + chatId: string, + messages: Message[], + researchMode: ResearchMode, + selectedConnectors: string[], + authToken: string, + searchSpaceId: number ) => { - const userMessages = messages.filter((msg) => msg.role === "user"); - if (userMessages.length === 0) return; + const userMessages = messages.filter((msg) => msg.role === "user"); + if (userMessages.length === 0) return; - const title = userMessages[0].content; + const title = userMessages[0].content; - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number( - chatId - )}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify({ - type: researchMode, - title: title, - initial_connectors: selectedConnectors, - messages: messages, - search_space_id: searchSpaceId, - }), - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ + type: researchMode, + title: title, + initial_connectors: selectedConnectors, + messages: messages, + search_space_id: searchSpaceId, + }), + } + ); - if (!response.ok) { - throw new Error(`Failed to update chat: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Failed to update chat: ${response.statusText}`); + } }; export const fetchChats = async ( - searchSpaceId: string, - limit: number, - skip: number, - authToken: string + searchSpaceId: string, + limit: number, + skip: number, + authToken: string ) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?limit=${limit}&skip=${skip}&search_space_id=${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?limit=${limit}&skip=${skip}&search_space_id=${searchSpaceId}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error(`Failed to fetch chats: ${response.status}`); - } + if (!response.ok) { + throw new Error(`Failed to fetch chats: ${response.status}`); + } - return await response.json(); + return await response.json(); }; diff --git a/surfsense_web/lib/apis/documents.api.ts b/surfsense_web/lib/apis/documents.api.ts index 155717911..7ef306a4c 100644 --- a/surfsense_web/lib/apis/documents.api.ts +++ b/surfsense_web/lib/apis/documents.api.ts @@ -3,273 +3,258 @@ import { DocumentTypeCount } from "@/hooks/use-document-types"; import { normalizeListResponse } from "../pagination"; export const uploadDocument = async (formData: FormData, authToken: string) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`, - { - method: "POST", - headers: { - Authorization: `Bearer ${authToken}`, - }, - body: formData, - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`, + { + method: "POST", + headers: { + Authorization: `Bearer ${authToken}`, + }, + body: formData, + } + ); - if (!response.ok) { - throw new Error("Failed to upload document"); - } + if (!response.ok) { + throw new Error("Failed to upload document"); + } - return await response.json(); + return await response.json(); }; export const createDocument = async (request: { - documentType: string; - content: any; - searchSpaceId: number; - authToken: string; + documentType: string; + content: any; + searchSpaceId: number; + authToken: string; }) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${request.authToken}`, - }, - body: JSON.stringify(request), - } - ); + const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${request.authToken}`, + }, + body: JSON.stringify(request), + }); - if (!response.ok) { - throw new Error("Failed to process document"); - } + if (!response.ok) { + throw new Error("Failed to process document"); + } - return await response.json(); + return await response.json(); }; -export const fetchDocumentByChunk = async ( - chunkId: number, - authToken: string -) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/by-chunk/${chunkId}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - }, - method: "GET", - } - ); +export const fetchDocumentByChunk = async (chunkId: number, authToken: string) => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/by-chunk/${chunkId}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + method: "GET", + } + ); - if (!response.ok) { - const errorText = await response.text(); - let errorMessage = "Failed to fetch document"; + if (!response.ok) { + const errorText = await response.text(); + let errorMessage = "Failed to fetch document"; - try { - const errorData = JSON.parse(errorText); - errorMessage = errorData.detail || errorMessage; - } catch { - // If parsing fails, use default message - } + try { + const errorData = JSON.parse(errorText); + errorMessage = errorData.detail || errorMessage; + } catch { + // If parsing fails, use default message + } - if (response.status === 404) { - errorMessage = "Chunk not found or you don't have access to it"; - } - throw new Error(errorMessage); - } + if (response.status === 404) { + errorMessage = "Chunk not found or you don't have access to it"; + } + throw new Error(errorMessage); + } - const data: DocumentWithChunks = await response.json(); + const data: DocumentWithChunks = await response.json(); - return data; + return data; }; export const fetchDocumentTypes = async (authToken: string) => { - if (!authToken) { - throw new Error("No authentication token found"); - } + if (!authToken) { + throw new Error("No authentication token found"); + } - // Build URL with optional search_space_id query parameter - const url = new URL( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/type-counts` - ); + // Build URL with optional search_space_id query parameter + const url = new URL( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/type-counts` + ); - const response = await fetch(url.toString(), { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - }); + const response = await fetch(url.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + }); - if (!response.ok) { - throw new Error(`Failed to fetch document types: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Failed to fetch document types: ${response.statusText}`); + } - const data = await response.json(); + const data = await response.json(); - // Convert the object to an array of DocumentTypeCount - const typeCounts: DocumentTypeCount[] = Object.entries(data).map( - ([type, count]) => ({ - type, - count: count as number, - }) - ); + // Convert the object to an array of DocumentTypeCount + const typeCounts: DocumentTypeCount[] = Object.entries(data).map(([type, count]) => ({ + type, + count: count as number, + })); - return typeCounts; + return typeCounts; }; export const fetchDocuments = async ( - searchSpaceId: number, - authToken: string, - fetchPage?: number, - fetchPageSize?: number, - fetchDocumentTypes?: string[] + searchSpaceId: number, + authToken: string, + fetchPage?: number, + fetchPageSize?: number, + fetchDocumentTypes?: string[] ) => { - // Build query params - const params = new URLSearchParams({ - search_space_id: searchSpaceId.toString(), - }); + // Build query params + const params = new URLSearchParams({ + search_space_id: searchSpaceId.toString(), + }); - // // Use passed parameters or fall back to state/options - // const effectivePage = fetchPage !== undefined ? fetchPage : page; - // const effectivePageSize = - // fetchPageSize !== undefined ? fetchPageSize : pageSize; - // const effectiveDocumentTypes = - // fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes; + // // Use passed parameters or fall back to state/options + // const effectivePage = fetchPage !== undefined ? fetchPage : page; + // const effectivePageSize = + // fetchPageSize !== undefined ? fetchPageSize : pageSize; + // const effectiveDocumentTypes = + // fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes; - // if (effectivePage !== undefined) { - // params.append("page", effectivePage.toString()); - // } - // if (effectivePageSize !== undefined) { - // params.append("page_size", effectivePageSize.toString()); - // } - // if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) { - // params.append("document_types", effectiveDocumentTypes.join(",")); - // } + // if (effectivePage !== undefined) { + // params.append("page", effectivePage.toString()); + // } + // if (effectivePageSize !== undefined) { + // params.append("page_size", effectivePageSize.toString()); + // } + // if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) { + // params.append("document_types", effectiveDocumentTypes.join(",")); + // } - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL - }/api/v1/documents?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error("Failed to fetch documents"); - } + if (!response.ok) { + throw new Error("Failed to fetch documents"); + } - const data = await response.json(); - return normalizeListResponse(data); + const data = await response.json(); + return normalizeListResponse(data); }; export const searchDocuments = async ( - searchSpaceId: number, - authToken: string, - searchQuery: string, - fetchPage?: number, - fetchPageSize?: number, - fetchDocumentTypes?: string[] + searchSpaceId: number, + authToken: string, + searchQuery: string, + fetchPage?: number, + fetchPageSize?: number, + fetchDocumentTypes?: string[] ) => { - // if (!searchQuery.trim()) { - // // If search is empty, fetch all documents - // // return fetchDocuments(fetchPage, fetchPageSize, fetchDocumentTypes); - // } + // if (!searchQuery.trim()) { + // // If search is empty, fetch all documents + // // return fetchDocuments(fetchPage, fetchPageSize, fetchDocumentTypes); + // } - // Build query params - const params = new URLSearchParams({ - search_space_id: searchSpaceId.toString(), - title: searchQuery, - }); + // Build query params + const params = new URLSearchParams({ + search_space_id: searchSpaceId.toString(), + title: searchQuery, + }); - // // Use passed parameters or fall back to state/options - // const effectivePage = fetchPage !== undefined ? fetchPage : page; - // const effectivePageSize = fetchPageSize !== undefined ? fetchPageSize : pageSize; - // const effectiveDocumentTypes = - // fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes; + // // Use passed parameters or fall back to state/options + // const effectivePage = fetchPage !== undefined ? fetchPage : page; + // const effectivePageSize = fetchPageSize !== undefined ? fetchPageSize : pageSize; + // const effectiveDocumentTypes = + // fetchDocumentTypes !== undefined ? fetchDocumentTypes : documentTypes; - // if (effectivePage !== undefined) { - // params.append("page", effectivePage.toString()); - // } - // if (effectivePageSize !== undefined) { - // params.append("page_size", effectivePageSize.toString()); - // } - // if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) { - // params.append("document_types", effectiveDocumentTypes.join(",")); - // } + // if (effectivePage !== undefined) { + // params.append("page", effectivePage.toString()); + // } + // if (effectivePageSize !== undefined) { + // params.append("page_size", effectivePageSize.toString()); + // } + // if (effectiveDocumentTypes && effectiveDocumentTypes.length > 0) { + // params.append("document_types", effectiveDocumentTypes.join(",")); + // } - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL - }/api/v1/documents/search?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/search?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error("Failed to search documents"); - } + if (!response.ok) { + throw new Error("Failed to search documents"); + } - const data = await response.json(); - const normalized = normalizeListResponse(data); - return normalized; + const data = await response.json(); + const normalized = normalizeListResponse(data); + return normalized; }; export const deleteDocument = async (documentId: number, authToken: string) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "DELETE", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "DELETE", + } + ); - if (!response.ok) { - throw new Error("Failed to delete document"); - } + if (!response.ok) { + throw new Error("Failed to delete document"); + } - return await response.json(); + return await response.json(); }; -export const getDocumentTypeCounts = async ( - searchSpaceId: number, - authToken: string -) => { - try { - const params = new URLSearchParams({ - search_space_id: searchSpaceId.toString(), - }); +export const getDocumentTypeCounts = async (searchSpaceId: number, authToken: string) => { + try { + const params = new URLSearchParams({ + search_space_id: searchSpaceId.toString(), + }); - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL - }/api/v1/documents/type-counts?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${ + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL + }/api/v1/documents/type-counts?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error("Failed to fetch document type counts"); - } + if (!response.ok) { + throw new Error("Failed to fetch document type counts"); + } - const counts = await response.json(); - return counts as Record; - } catch (err: any) { - console.error("Error fetching document type counts:", err); - return {}; - } + const counts = await response.json(); + return counts as Record; + } catch (err: any) { + console.error("Error fetching document type counts:", err); + return {}; + } }; diff --git a/surfsense_web/lib/apis/llm-configs.api.ts b/surfsense_web/lib/apis/llm-configs.api.ts index 86495f7e2..2f9608d2f 100644 --- a/surfsense_web/lib/apis/llm-configs.api.ts +++ b/surfsense_web/lib/apis/llm-configs.api.ts @@ -1,98 +1,90 @@ import { CreateLLMConfig, LLMConfig, UpdateLLMConfig } from "@/hooks/use-llm-configs"; -export const fetchLLMConfigs = async ( - searchSpaceId: number, - authToken: string -) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs?search_space_id=${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); +export const fetchLLMConfigs = async (searchSpaceId: number, authToken: string) => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs?search_space_id=${searchSpaceId}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error("Failed to fetch LLM configurations"); - } + if (!response.ok) { + throw new Error("Failed to fetch LLM configurations"); + } - return await response.json(); + return await response.json(); }; export const createLLMConfig = async ( - config: CreateLLMConfig, - authToken: string + config: CreateLLMConfig, + authToken: string ): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify(config), - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(config), + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to create LLM configuration"); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to create LLM configuration"); + } - const newConfig = await response.json(); + const newConfig = await response.json(); - return newConfig; + return newConfig; }; -export const deleteLLMConfig = async ( - id: number, - authToken: string -): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - ); +export const deleteLLMConfig = async (id: number, authToken: string): Promise => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); - if (!response.ok) { - throw new Error("Failed to delete LLM configuration"); - } + if (!response.ok) { + throw new Error("Failed to delete LLM configuration"); + } - return await response.json(); + return await response.json(); }; export const updateLLMConfig = async ( - id: number, - config: UpdateLLMConfig, - authToken: string + id: number, + config: UpdateLLMConfig, + authToken: string ): Promise => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(config), + } + ); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/${id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify(config), - } - ); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to update LLM configuration"); + } - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to update LLM configuration"); - } - - const updatedConfig = await response.json(); - - return updatedConfig; + const updatedConfig = await response.json(); + return updatedConfig; }; diff --git a/surfsense_web/lib/apis/podcasts.api.ts b/surfsense_web/lib/apis/podcasts.api.ts index fa9c680fe..beaa475ca 100644 --- a/surfsense_web/lib/apis/podcasts.api.ts +++ b/surfsense_web/lib/apis/podcasts.api.ts @@ -2,78 +2,73 @@ import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/pod import type { GeneratePodcastRequest } from "@/components/chat/ChatPanel/ChatPanelContainer"; export const getPodcastByChatId = async (chatId: string, authToken: string) => { - const response = await fetch( - `${ - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL - }/api/v1/podcasts/by-chat/${Number(chatId)}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/by-chat/${Number(chatId)}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.detail || "Failed to fetch podcast"); - } + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to fetch podcast"); + } - return (await response.json()) as PodcastItem | null; + return (await response.json()) as PodcastItem | null; }; -export const generatePodcast = async ( - request: GeneratePodcastRequest, - authToken: string -) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`, - { - method: "POST", - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(request), - } - ); +export const generatePodcast = async (request: GeneratePodcastRequest, authToken: string) => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`, + { + method: "POST", + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + } + ); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.detail || "Failed to generate podcast"); - } + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to generate podcast"); + } - return await response.json(); + return await response.json(); }; export const loadPodcast = async (podcast: PodcastItem, authToken: string) => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - signal: controller.signal, - } - ); + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + signal: controller.signal, + } + ); - if (!response.ok) { - throw new Error(`Failed to fetch audio stream: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Failed to fetch audio stream: ${response.statusText}`); + } - const blob = await response.blob(); - const objectUrl = URL.createObjectURL(blob); - return objectUrl; - } catch (error) { - if (error instanceof DOMException && error.name === "AbortError") { - throw new Error("Request timed out. Please try again."); - } - throw error; - } finally { - clearTimeout(timeoutId); - } + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + return objectUrl; + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + throw new Error("Request timed out. Please try again."); + } + throw error; + } finally { + clearTimeout(timeoutId); + } }; diff --git a/surfsense_web/lib/apis/search-source-connectors.api.ts b/surfsense_web/lib/apis/search-source-connectors.api.ts index 2a9e59301..c84d0be20 100644 --- a/surfsense_web/lib/apis/search-source-connectors.api.ts +++ b/surfsense_web/lib/apis/search-source-connectors.api.ts @@ -1,113 +1,107 @@ import { Connector, CreateConnectorRequest } from "@/hooks/use-connectors"; export const createConnector = async ( - data: CreateConnectorRequest, - authToken: string + data: CreateConnectorRequest, + authToken: string ): Promise => { - 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 ${authToken}`, - }, - body: JSON.stringify(data), - } - ); + 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 ${authToken}`, + }, + body: JSON.stringify(data), + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to create connector"); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to create connector"); + } - return response.json(); + return response.json(); }; export const getConnectors = async ( - skip = 0, - limit = 100, - authToken: string + skip = 0, + limit = 100, + authToken: string ): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors?skip=${skip}&limit=${limit}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors?skip=${skip}&limit=${limit}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to fetch connectors"); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to fetch connectors"); + } - return response.json(); + return response.json(); }; -export const getConnector = async ( - connectorId: number, - authToken: string -): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - ); +export const getConnector = async (connectorId: number, authToken: string): Promise => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to fetch connector"); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to fetch connector"); + } - return response.json(); + return response.json(); }; export const updateConnector = async ( - connectorId: number, - data: CreateConnectorRequest, - authToken: string + connectorId: number, + data: CreateConnectorRequest, + authToken: string ): Promise => { - 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 ${authToken}`, - }, - body: JSON.stringify(data), - } - ); + 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 ${authToken}`, + }, + body: JSON.stringify(data), + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to update connector"); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to update connector"); + } - return response.json(); + return response.json(); }; -export const deleteConnector = async ( - connectorId: number, - authToken: string -): Promise => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - ); +export const deleteConnector = async (connectorId: number, authToken: string): Promise => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to delete connector"); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || "Failed to delete connector"); + } }; diff --git a/surfsense_web/lib/apis/search-spaces.api.ts b/surfsense_web/lib/apis/search-spaces.api.ts index 2ded72f44..d63273f9d 100644 --- a/surfsense_web/lib/apis/search-spaces.api.ts +++ b/surfsense_web/lib/apis/search-spaces.api.ts @@ -1,112 +1,98 @@ export const fetchSearchSpaces = async () => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem( - "surfsense_bearer_token" - )}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error("Not authenticated"); - } + if (!response.ok) { + throw new Error("Not authenticated"); + } - return await response.json(); + return await response.json(); }; export const deleteSearchSpace = async (id: number) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${id}`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${localStorage.getItem( - "surfsense_bearer_token" - )}`, - }, - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${id}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + } + ); - if (!response.ok) { - throw new Error("Failed to delete search space"); - } + if (!response.ok) { + throw new Error("Failed to delete search space"); + } - return await response.json(); + return await response.json(); }; -export const createSearchSpace = async (data: { - name: string; - description: string; -}) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem( - "surfsense_bearer_token" - )}`, - }, - body: JSON.stringify(data), - } - ); +export const createSearchSpace = async (data: { name: string; description: string }) => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + body: JSON.stringify(data), + } + ); - if (!response.ok) { - throw new Error("Failed to create search space"); - } + if (!response.ok) { + throw new Error("Failed to create search space"); + } - return await response.json(); + return await response.json(); }; export const fetchSearchSpace = async (searchSpaceId: string) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem( - "surfsense_bearer_token" - )}`, - }, - method: "GET", - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + method: "GET", + } + ); - if (response.status === 401) { - // Clear token and redirect to home - localStorage.removeItem("surfsense_bearer_token"); - window.location.href = "/"; - throw new Error("Unauthorized: Redirecting to login page"); - } + if (response.status === 401) { + // Clear token and redirect to home + localStorage.removeItem("surfsense_bearer_token"); + window.location.href = "/"; + throw new Error("Unauthorized: Redirecting to login page"); + } - if (!response.ok) { - throw new Error(`Failed to fetch search space: ${response.status}`); - } + if (!response.ok) { + throw new Error(`Failed to fetch search space: ${response.status}`); + } - return await response.json(); + return await response.json(); }; -export const fetchSearchSpacePreferences = async ( - searchSpaceId: number, - authToken: string -) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - method: "GET", - } - ); +export const fetchSearchSpacePreferences = async (searchSpaceId: number, authToken: string) => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + method: "GET", + } + ); - if (!response.ok) { - throw new Error("Failed to fetch LLM preferences"); - } + if (!response.ok) { + throw new Error("Failed to fetch LLM preferences"); + } - return await response.json(); + return await response.json(); }; diff --git a/surfsense_web/lib/error.ts b/surfsense_web/lib/error.ts index 03259c7b5..4e8ab0e9d 100644 --- a/surfsense_web/lib/error.ts +++ b/surfsense_web/lib/error.ts @@ -1,30 +1,30 @@ export class AppError extends Error { - constructor(message: string) { - super(message); - this.name = this.constructor.name; - } + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } } export class NetworkError extends AppError { - constructor(message: string) { - super(message); - } + constructor(message: string) { + super(message); + } } export class ValidationError extends AppError { - constructor(message: string) { - super(message); - } + constructor(message: string) { + super(message); + } } export class AuthenticationError extends AppError { - constructor(message: string) { - super(message); - } + constructor(message: string) { + super(message); + } } export class AuthorizationError extends AppError { - constructor(message: string) { - super(message); - } + constructor(message: string) { + super(message); + } } diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index 5b74092e1..6b9b2df04 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -1,6 +1,6 @@ export const cacheKeys = { activeSearchSpace: { - chats : (searchSpaceId: string) => ["active-search-space", "chats", searchSpaceId] as const, - activeChat : (chatId: string) => ["active-search-space", "active-chat", chatId] as const, + chats: (searchSpaceId: string) => ["active-search-space", "chats", searchSpaceId] as const, + activeChat: (chatId: string) => ["active-search-space", "active-chat", chatId] as const, }, }; diff --git a/surfsense_web/lib/utils.ts b/surfsense_web/lib/utils.ts index 3b27a4c93..e8b097d15 100644 --- a/surfsense_web/lib/utils.ts +++ b/surfsense_web/lib/utils.ts @@ -6,9 +6,8 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } - export function getChatTitleFromMessages(messages: Message[]) { const userMessages = messages.filter((msg) => msg.role === "user"); if (userMessages.length === 0) return "Untitled Chat"; return userMessages[0].content; -} \ No newline at end of file +}