"use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { format } from "date-fns"; import { ArchiveIcon, Loader2, MessageCircleMore, MoreHorizontal, RotateCcwIcon, Search, Trash2, X, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, 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 { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { deleteThread, fetchThreads, searchThreads, type ThreadListItem, updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; interface AllChatsSidebarProps { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string; onCloseMobileSidebar?: () => void; } export function AllChatsSidebar({ open, onOpenChange, searchSpaceId, onCloseMobileSidebar, }: AllChatsSidebarProps) { const t = useTranslations("sidebar"); const router = useRouter(); const params = useParams(); const queryClient = useQueryClient(); // Get the current chat ID from URL to check if user is deleting the currently open chat 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(); // Handle mounting for portal useEffect(() => { setMounted(true); }, []); // Handle escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { onOpenChange(false); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); // Lock body scroll when open useEffect(() => { if (open) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [open]); // Fetch all threads (when not searching) const { data: threadsData, error: threadsError, isLoading: isLoadingThreads, } = useQuery({ queryKey: ["all-threads", searchSpaceId], queryFn: () => fetchThreads(Number(searchSpaceId)), enabled: !!searchSpaceId && open && !isSearchMode, }); // Search threads (when searching) const { data: searchData, error: searchError, isLoading: isLoadingSearch, } = useQuery({ queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery], queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()), enabled: !!searchSpaceId && open && isSearchMode, }); // Handle thread navigation const handleThreadClick = useCallback( (threadId: number) => { router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`); onOpenChange(false); // Also close the main sidebar on mobile onCloseMobileSidebar?.(); }, [router, onOpenChange, searchSpaceId, onCloseMobileSidebar] ); // Handle thread deletion 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 the deleted chat is currently open, close sidebar first then redirect if (currentChatId === threadId) { onOpenChange(false); // Wait for sidebar close animation to complete before navigating 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] ); // Handle thread archive/unarchive 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] ); // Clear search const handleClearSearch = useCallback(() => { setSearchQuery(""); }, []); // Determine which data source to use let threads: ThreadListItem[] = []; if (isSearchMode) { threads = searchData ?? []; } else if (threadsData) { threads = showArchived ? threadsData.archived_threads : threadsData.threads; } const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads; const error = isSearchMode ? searchError : threadsError; // Get counts for tabs const activeCount = threadsData?.threads.length ?? 0; const archivedCount = threadsData?.archived_threads.length ?? 0; if (!mounted) return null; return createPortal( {open && ( <> {/* Backdrop */} onOpenChange(false)} aria-hidden="true" /> {/* Panel */} {/* Header */}

{t("all_chats") || "All Chats"}

{/* Search Input */}
setSearchQuery(e.target.value)} className="pl-9 pr-8 h-9" /> {searchQuery && ( )}
{/* Tab toggle for active/archived (only show when not searching) */} {!isSearchMode && (
)} {/* Scrollable Content */}
{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 (
{/* Main clickable area for navigation */}

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

{/* Actions dropdown */} 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 chats yet"}

{!showArchived && (

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

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