"use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { format } from "date-fns"; import { useSetAtom } from "jotai"; import { ArchiveIcon, ChevronLeft, MessageCircleMore, MoreHorizontal, PenLine, RotateCcwIcon, Search, Trash2, User, X, } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useLongPress } from "@/hooks/use-long-press"; import { useIsMobile } from "@/hooks/use-mobile"; import { deleteThread, fetchThreads, searchThreads, updateThread, } from "@/lib/chat/thread-persistence"; import { cn } from "@/lib/utils"; import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; export interface AllPrivateChatsSidebarContentProps { onOpenChange: (open: boolean) => void; searchSpaceId: string; onCloseMobileSidebar?: () => void; } interface AllPrivateChatsSidebarProps extends AllPrivateChatsSidebarContentProps { open: boolean; } export function AllPrivateChatsSidebarContent({ onOpenChange, searchSpaceId, onCloseMobileSidebar, }: AllPrivateChatsSidebarContentProps) { const t = useTranslations("sidebar"); const router = useRouter(); const params = useParams(); const queryClient = useQueryClient(); const isMobile = useIsMobile(); const removeChatTab = useSetAtom(removeChatTabAtom); 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 [openDropdownId, setOpenDropdownId] = useState(null); const [showRenameDialog, setShowRenameDialog] = useState(false); const [renamingThread, setRenamingThread] = useState<{ id: number; title: string } | null>(null); const [newTitle, setNewTitle] = useState(""); const [isRenaming, setIsRenaming] = useState(false); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const pendingThreadIdRef = useRef(null); const { handlers: longPressHandlers, wasLongPress } = useLongPress( useCallback(() => { if (pendingThreadIdRef.current !== null) { setOpenDropdownId(pendingThreadIdRef.current); } }, []) ); const isSearchMode = !!debouncedSearchQuery.trim(); const { data: threadsData, error: threadsError, isLoading: isLoadingThreads, } = useQuery({ queryKey: ["all-threads", searchSpaceId], queryFn: () => fetchThreads(Number(searchSpaceId)), enabled: !!searchSpaceId && !isSearchMode, placeholderData: () => queryClient.getQueryData(["threads", searchSpaceId, { limit: 40 }]), }); const { data: searchData, error: searchError, isLoading: isLoadingSearch, } = useQuery({ queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery], queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()), enabled: !!searchSpaceId && 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); const fallbackTab = removeChatTab(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(() => { if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) { router.push(fallbackTab.chatUrl); return; } 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, removeChatTab] ); 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 handleStartRename = useCallback((threadId: number, title: string) => { setRenamingThread({ id: threadId, title }); setNewTitle(title); setShowRenameDialog(true); }, []); const handleConfirmRename = useCallback(async () => { if (!renamingThread || !newTitle.trim()) return; setIsRenaming(true); try { await updateThread(renamingThread.id, { title: newTitle.trim() }); toast.success(t("chat_renamed") || "Chat renamed"); queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId, "detail", String(renamingThread.id)], }); } catch (error) { console.error("Error renaming thread:", error); toast.error(t("error_renaming_chat") || "Failed to rename chat"); } finally { setIsRenaming(false); setShowRenameDialog(false); setRenamingThread(null); setNewTitle(""); } }, [renamingThread, newTitle, 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; return ( <>
{isMobile && ( )}

{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 mt-2" > Active {activeCount} Archived {archivedCount} )}
{isLoading ? (
{[75, 90, 55, 80, 65, 85].map((titleWidth) => (
))}
) : 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 (
{isMobile ? ( ) : (

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

)} setOpenDropdownId(isOpen ? thread.id : null)} > {!thread.archived && ( handleStartRename(thread.id, thread.title || "New Chat")} > {t("rename") || "Rename"} )} handleToggleArchive(thread.id, thread.archived)} disabled={isArchiving} > {thread.archived ? ( <> {t("unarchive") || "Restore"} ) : ( <> {t("archive") || "Archive"} )} handleDeleteThread(thread.id)}> {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"}

)}
)}
{t("rename_chat") || "Rename Chat"} {t("rename_chat_description") || "Enter a new name for this conversation."} setNewTitle(e.target.value)} placeholder={t("chat_title_placeholder") || "Chat title"} onKeyDown={(e) => { if (e.key === "Enter" && !isRenaming && newTitle.trim()) { handleConfirmRename(); } }} /> ); } export function AllPrivateChatsSidebar({ open, onOpenChange, searchSpaceId, onCloseMobileSidebar, }: AllPrivateChatsSidebarProps) { const t = useTranslations("sidebar"); return ( ); }