"use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { format } from "date-fns"; import { ArchiveIcon, MessageCircleMore, MoreHorizontal, RotateCcwIcon, Search, Trash2, User, X, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { deleteThread, fetchThreads, searchThreads, updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; interface AllPrivateChatsSidebarProps { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string; onCloseMobileSidebar?: () => void; } export function AllPrivateChatsSidebar({ open, onOpenChange, searchSpaceId, onCloseMobileSidebar, }: AllPrivateChatsSidebarProps) { const t = useTranslations("sidebar"); const router = useRouter(); const params = useParams(); const queryClient = useQueryClient(); const currentChatId = Array.isArray(params.chat_id) ? Number(params.chat_id[0]) : params.chat_id ? Number(params.chat_id) : null; const [deletingThreadId, setDeletingThreadId] = useState(null); const [archivingThreadId, setArchivingThreadId] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [showArchived, setShowArchived] = useState(false); const [mounted, setMounted] = useState(false); const [openDropdownId, setOpenDropdownId] = useState(null); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const isSearchMode = !!debouncedSearchQuery.trim(); useEffect(() => { setMounted(true); }, []); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { onOpenChange(false); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); useEffect(() => { if (open) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [open]); const { data: threadsData, error: threadsError, isLoading: isLoadingThreads, } = useQuery({ queryKey: ["all-threads", searchSpaceId], queryFn: () => fetchThreads(Number(searchSpaceId)), enabled: !!searchSpaceId && open && !isSearchMode, }); const { data: searchData, error: searchError, isLoading: isLoadingSearch, } = useQuery({ queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery], queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()), enabled: !!searchSpaceId && open && isSearchMode, }); // Filter to only private chats (PRIVATE visibility or no visibility set) const { activeChats, archivedChats } = useMemo(() => { if (isSearchMode) { const privateSearchResults = (searchData ?? []).filter( (thread) => thread.visibility !== "SEARCH_SPACE" ); return { activeChats: privateSearchResults.filter((t) => !t.archived), archivedChats: privateSearchResults.filter((t) => t.archived), }; } if (!threadsData) return { activeChats: [], archivedChats: [] }; const activePrivate = threadsData.threads.filter( (thread) => thread.visibility !== "SEARCH_SPACE" ); const archivedPrivate = threadsData.archived_threads.filter( (thread) => thread.visibility !== "SEARCH_SPACE" ); return { activeChats: activePrivate, archivedChats: archivedPrivate }; }, [threadsData, searchData, isSearchMode]); const threads = showArchived ? archivedChats : activeChats; const handleThreadClick = useCallback( (threadId: number) => { router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`); onOpenChange(false); onCloseMobileSidebar?.(); }, [router, onOpenChange, searchSpaceId, onCloseMobileSidebar] ); const handleDeleteThread = useCallback( async (threadId: number) => { setDeletingThreadId(threadId); try { await deleteThread(threadId); toast.success(t("chat_deleted") || "Chat deleted successfully"); queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); if (currentChatId === threadId) { onOpenChange(false); setTimeout(() => { router.push(`/dashboard/${searchSpaceId}/new-chat`); }, 250); } } catch (error) { console.error("Error deleting thread:", error); toast.error(t("error_deleting_chat") || "Failed to delete chat"); } finally { setDeletingThreadId(null); } }, [queryClient, searchSpaceId, t, currentChatId, router, onOpenChange] ); const handleToggleArchive = useCallback( async (threadId: number, currentlyArchived: boolean) => { setArchivingThreadId(threadId); try { await updateThread(threadId, { archived: !currentlyArchived }); toast.success( currentlyArchived ? t("chat_unarchived") || "Chat restored" : t("chat_archived") || "Chat archived" ); queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); } catch (error) { console.error("Error archiving thread:", error); toast.error(t("error_archiving_chat") || "Failed to archive chat"); } finally { setArchivingThreadId(null); } }, [queryClient, searchSpaceId, t] ); const handleClearSearch = useCallback(() => { setSearchQuery(""); }, []); const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads; const error = isSearchMode ? searchError : threadsError; const activeCount = activeChats.length; const archivedCount = archivedChats.length; if (!mounted) return null; return createPortal( {open && ( <> onOpenChange(false)} aria-hidden="true" />

{t("chats") || "Private Chats"}

setSearchQuery(e.target.value)} className="pl-9 pr-8 h-9" /> {searchQuery && ( )}
{!isSearchMode && ( setShowArchived(value === "archived")} className="shrink-0 mx-4" > Active {activeCount} Archived {archivedCount} )}
{isLoading ? (
) : error ? (
{t("error_loading_chats") || "Error loading chats"}
) : threads.length > 0 ? (
{threads.map((thread) => { const isDeleting = deletingThreadId === thread.id; const isArchiving = archivingThreadId === thread.id; const isBusy = isDeleting || isArchiving; const isActive = currentChatId === thread.id; return (

{t("updated") || "Updated"}:{" "} {format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}

setOpenDropdownId(isOpen ? thread.id : null)} > handleToggleArchive(thread.id, thread.archived)} disabled={isArchiving} > {thread.archived ? ( <> {t("unarchive") || "Restore"} ) : ( <> {t("archive") || "Archive"} )} handleDeleteThread(thread.id)} className="text-destructive focus:text-destructive" > {t("delete") || "Delete"}
); })}
) : isSearchMode ? (

{t("no_chats_found") || "No chats found"}

{t("try_different_search") || "Try a different search term"}

) : (

{showArchived ? t("no_archived_chats") || "No archived chats" : t("no_chats") || "No private chats"}

{!showArchived && (

{t("start_new_chat_hint") || "Start a new chat from the chat page"}

)}
)}
)}
, document.body ); }