"use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { Inbox, LogOut, SquareLibrary, Trash2 } from "lucide-react"; import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; import { useCallback, useMemo, useState } from "react"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { useInbox } from "@/hooks/use-inbox"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { deleteThread, fetchThreads } from "@/lib/chat/thread-persistence"; import { cleanupElectric } from "@/lib/electric/client"; import { resetUser, trackLogout } from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types"; import { CreateSearchSpaceDialog } from "../ui/dialogs"; import { LayoutShell } from "../ui/shell"; import { AllPrivateChatsSidebar } from "../ui/sidebar/AllPrivateChatsSidebar"; import { AllSharedChatsSidebar } from "../ui/sidebar/AllSharedChatsSidebar"; import { InboxSidebar } from "../ui/sidebar/InboxSidebar"; interface LayoutDataProviderProps { searchSpaceId: string; children: React.ReactNode; breadcrumb?: React.ReactNode; } export function LayoutDataProvider({ searchSpaceId, children, breadcrumb, }: LayoutDataProviderProps) { const t = useTranslations("dashboard"); const tCommon = useTranslations("common"); const router = useRouter(); const params = useParams(); const pathname = usePathname(); const queryClient = useQueryClient(); const { theme, setTheme } = useTheme(); // Atoms const { data: user } = useAtomValue(currentUserAtom); const { data: searchSpacesData, refetch: refetchSearchSpaces } = useAtomValue(searchSpacesAtom); const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom); // Current IDs from URL const currentChatId = params?.chat_id ? Number(Array.isArray(params.chat_id) ? params.chat_id[0] : params.chat_id) : null; // Fetch current search space (for caching purposes) useQuery({ queryKey: cacheKeys.searchSpaces.detail(searchSpaceId), queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }), enabled: !!searchSpaceId, }); // Fetch threads const { data: threadsData } = useQuery({ queryKey: ["threads", searchSpaceId, { limit: 4 }], queryFn: () => fetchThreads(Number(searchSpaceId), 4), enabled: !!searchSpaceId, }); // Separate sidebar states for shared and private chats const [isAllSharedChatsSidebarOpen, setIsAllSharedChatsSidebarOpen] = useState(false); const [isAllPrivateChatsSidebarOpen, setIsAllPrivateChatsSidebarOpen] = useState(false); // Inbox sidebar state const [isInboxSidebarOpen, setIsInboxSidebarOpen] = useState(false); // Search space dialog state const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false); // Inbox hook const userId = user?.id ? String(user.id) : null; const { inboxItems, unreadCount, loading: inboxLoading, loadingMore: inboxLoadingMore, hasMore: inboxHasMore, loadMore: inboxLoadMore, markAsRead, markAllAsRead, } = useInbox(userId, Number(searchSpaceId) || null, null); // Delete dialogs state const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false); const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null); const [isDeletingChat, setIsDeletingChat] = useState(false); // Delete/Leave search space dialog state const [showDeleteSearchSpaceDialog, setShowDeleteSearchSpaceDialog] = useState(false); const [showLeaveSearchSpaceDialog, setShowLeaveSearchSpaceDialog] = useState(false); const [searchSpaceToDelete, setSearchSpaceToDelete] = useState(null); const [searchSpaceToLeave, setSearchSpaceToLeave] = useState(null); const [isDeletingSearchSpace, setIsDeletingSearchSpace] = useState(false); const [isLeavingSearchSpace, setIsLeavingSearchSpace] = useState(false); const searchSpaces: SearchSpace[] = useMemo(() => { if (!searchSpacesData || !Array.isArray(searchSpacesData)) return []; return searchSpacesData.map((space) => ({ id: space.id, name: space.name, description: space.description, isOwner: space.is_owner, memberCount: space.member_count || 0, createdAt: space.created_at, })); }, [searchSpacesData]); // Find active search space from list (has is_owner and member_count) const activeSearchSpace: SearchSpace | null = useMemo(() => { if (!searchSpaceId || !searchSpaces.length) return null; return searchSpaces.find((s) => s.id === Number(searchSpaceId)) ?? null; }, [searchSpaceId, searchSpaces]); // Transform and split chats into private and shared based on visibility const { myChats, sharedChats } = useMemo(() => { if (!threadsData?.threads) return { myChats: [], sharedChats: [] }; const privateChats: ChatItem[] = []; const sharedChatsList: ChatItem[] = []; for (const thread of threadsData.threads) { const chatItem: ChatItem = { id: thread.id, name: thread.title || `Chat ${thread.id}`, url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`, visibility: thread.visibility, isOwnThread: thread.is_own_thread, }; // Split based on visibility, not ownership: // - PRIVATE chats go to "Private Chats" section // - SEARCH_SPACE chats go to "Shared Chats" section if (thread.visibility === "SEARCH_SPACE") { sharedChatsList.push(chatItem); } else { privateChats.push(chatItem); } } return { myChats: privateChats, sharedChats: sharedChatsList }; }, [threadsData, searchSpaceId]); // Navigation items const navItems: NavItem[] = useMemo( () => [ { title: "Documents", url: `/dashboard/${searchSpaceId}/documents`, icon: SquareLibrary, isActive: pathname?.includes("/documents"), }, { title: "Inbox", url: "#inbox", // Special URL to indicate this is handled differently icon: Inbox, isActive: isInboxSidebarOpen, badge: unreadCount > 0 ? (unreadCount > 99 ? "99+" : unreadCount) : undefined, }, ], [searchSpaceId, pathname, isInboxSidebarOpen, unreadCount] ); // Handlers const handleSearchSpaceSelect = useCallback( (id: number) => { router.push(`/dashboard/${id}/new-chat`); }, [router] ); const handleAddSearchSpace = useCallback(() => { setIsCreateSearchSpaceDialogOpen(true); }, []); const handleUserSettings = useCallback(() => { router.push("/dashboard/user/settings"); }, [router]); const handleSearchSpaceSettings = useCallback( (space: SearchSpace) => { router.push(`/dashboard/${space.id}/settings`); }, [router] ); const handleSearchSpaceDeleteClick = useCallback((space: SearchSpace) => { // If user is owner, show delete dialog; otherwise show leave dialog if (space.isOwner) { setSearchSpaceToDelete(space); setShowDeleteSearchSpaceDialog(true); } else { setSearchSpaceToLeave(space); setShowLeaveSearchSpaceDialog(true); } }, []); const confirmDeleteSearchSpace = useCallback(async () => { if (!searchSpaceToDelete) return; setIsDeletingSearchSpace(true); try { await deleteSearchSpace({ id: searchSpaceToDelete.id }); refetchSearchSpaces(); if (Number(searchSpaceId) === searchSpaceToDelete.id && searchSpaces.length > 1) { const remaining = searchSpaces.filter((s) => s.id !== searchSpaceToDelete.id); if (remaining.length > 0) { router.push(`/dashboard/${remaining[0].id}/new-chat`); } } else if (searchSpaces.length === 1) { router.push("/dashboard"); } } catch (error) { console.error("Error deleting search space:", error); } finally { setIsDeletingSearchSpace(false); setShowDeleteSearchSpaceDialog(false); setSearchSpaceToDelete(null); } }, [ searchSpaceToDelete, deleteSearchSpace, refetchSearchSpaces, searchSpaceId, searchSpaces, router, ]); const confirmLeaveSearchSpace = useCallback(async () => { if (!searchSpaceToLeave) return; setIsLeavingSearchSpace(true); try { await searchSpacesApiService.leaveSearchSpace(searchSpaceToLeave.id); refetchSearchSpaces(); if (Number(searchSpaceId) === searchSpaceToLeave.id && searchSpaces.length > 1) { const remaining = searchSpaces.filter((s) => s.id !== searchSpaceToLeave.id); if (remaining.length > 0) { router.push(`/dashboard/${remaining[0].id}/new-chat`); } } else if (searchSpaces.length === 1) { router.push("/dashboard"); } } catch (error) { console.error("Error leaving search space:", error); } finally { setIsLeavingSearchSpace(false); setShowLeaveSearchSpaceDialog(false); setSearchSpaceToLeave(null); } }, [searchSpaceToLeave, refetchSearchSpaces, searchSpaceId, searchSpaces, router]); const handleNavItemClick = useCallback( (item: NavItem) => { // Handle inbox specially - open sidebar instead of navigating if (item.url === "#inbox") { setIsInboxSidebarOpen(true); return; } router.push(item.url); }, [router] ); const handleNewChat = useCallback(() => { router.push(`/dashboard/${searchSpaceId}/new-chat`); }, [router, searchSpaceId]); const handleChatSelect = useCallback( (chat: ChatItem) => { router.push(chat.url); }, [router] ); const handleChatDelete = useCallback((chat: ChatItem) => { setChatToDelete({ id: chat.id, name: chat.name }); setShowDeleteChatDialog(true); }, []); const handleSettings = useCallback(() => { router.push(`/dashboard/${searchSpaceId}/settings`); }, [router, searchSpaceId]); const handleManageMembers = useCallback(() => { router.push(`/dashboard/${searchSpaceId}/team`); }, [router, searchSpaceId]); const handleLogout = useCallback(async () => { try { trackLogout(); resetUser(); // Best-effort cleanup of Electric SQL / PGlite // Even if this fails, login-time cleanup will handle it try { await cleanupElectric(); } catch (err) { console.warn("[Logout] Electric cleanup failed (will be handled on next login):", err); } if (typeof window !== "undefined") { localStorage.removeItem("surfsense_bearer_token"); router.push("/"); } } catch (error) { console.error("Error during logout:", error); router.push("/"); } }, [router]); const handleViewAllSharedChats = useCallback(() => { setIsAllSharedChatsSidebarOpen(true); }, []); const handleViewAllPrivateChats = useCallback(() => { setIsAllPrivateChatsSidebarOpen(true); }, []); // Delete handlers const confirmDeleteChat = useCallback(async () => { if (!chatToDelete) return; setIsDeletingChat(true); try { await deleteThread(chatToDelete.id); queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); if (currentChatId === chatToDelete.id) { router.push(`/dashboard/${searchSpaceId}/new-chat`); } } catch (error) { console.error("Error deleting thread:", error); } finally { setIsDeletingChat(false); setShowDeleteChatDialog(false); setChatToDelete(null); } }, [chatToDelete, queryClient, searchSpaceId, router, currentChatId]); // Page usage const pageUsage = user ? { pagesUsed: user.pages_used, pagesLimit: user.pages_limit, } : undefined; // Detect if we're on the chat page (needs overflow-hidden for chat's own scroll) const isChatPage = pathname?.includes("/new-chat") ?? false; return ( <> {children} {/* Delete Chat Dialog */} {t("delete_chat")} {t("delete_chat_confirm")} {chatToDelete?.name}?{" "} {t("action_cannot_undone")} {/* Delete Search Space Dialog */} {t("delete_search_space")} {t("delete_space_confirm", { name: searchSpaceToDelete?.name || "" })} {/* Leave Search Space Dialog */} {t("leave_title")} {t("leave_confirm", { name: searchSpaceToLeave?.name || "" })} {/* All Shared Chats Sidebar */} {/* All Private Chats Sidebar */} {/* Inbox Sidebar */} {/* Create Search Space Dialog */} ); }