"use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useSetAtom } from "jotai"; import { ArchiveIcon, Check, ChevronDown, MoreHorizontal, Pencil, RotateCcwIcon, Search, Trash2, 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 { Badge } from "@/components/ui/badge"; 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 { useActivateChatThread } from "@/hooks/use-activate-chat-thread"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useLongPress } from "@/hooks/use-long-press"; import { useIsMobile } from "@/hooks/use-mobile"; import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-thread-mutations"; import { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence"; import { formatThreadTimestamp } from "@/lib/format-date"; import { cn } from "@/lib/utils"; interface AllChatsContentProps { workspaceId: string; className?: string; } function AllChatsContent({ workspaceId, className }: AllChatsContentProps) { const t = useTranslations("sidebar"); const router = useRouter(); const params = useParams(); const queryClient = useQueryClient(); const isMobile = useIsMobile(); const removeChatTab = useSetAtom(removeChatTabAtom); const { activateChatThread, prefetchChatThread } = useActivateChatThread(); const { mutateAsync: deleteThread } = useDeleteThread(workspaceId); const { mutateAsync: archiveThread } = useArchiveThread(workspaceId); const { mutateAsync: renameThread } = useRenameThread(workspaceId); 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", workspaceId], queryFn: () => fetchThreads(Number(workspaceId)), enabled: !!workspaceId && !isSearchMode, placeholderData: () => queryClient.getQueryData(["threads", workspaceId, { limit: 40 }]), }); const { data: searchData, error: searchError, isLoading: isLoadingSearch, } = useQuery({ queryKey: ["search-threads", workspaceId, debouncedSearchQuery], queryFn: () => searchThreads(Number(workspaceId), debouncedSearchQuery.trim()), enabled: !!workspaceId && isSearchMode, }); const threads = useMemo(() => { if (isSearchMode) { return (searchData ?? []).filter((thread) => thread.archived === showArchived); } if (!threadsData) return []; return showArchived ? threadsData.archived_threads : threadsData.threads; }, [threadsData, searchData, isSearchMode, showArchived]); const handleThreadClick = useCallback( (thread: ThreadListItem) => { activateChatThread({ id: thread.id, title: thread.title || "New Chat", workspaceId, visibility: thread.visibility, }); }, [activateChatThread, workspaceId] ); const handleDeleteThread = useCallback( async (threadId: number) => { setDeletingThreadId(threadId); try { await deleteThread({ threadId }); const fallbackTab = removeChatTab(threadId); toast.success(t("chat_deleted") || "Chat deleted successfully"); if (currentChatId === threadId) { setTimeout(() => { if ( fallbackTab?.type === "chat" && fallbackTab.chatUrl && fallbackTab.chatId !== undefined ) { activateChatThread({ id: fallbackTab.chatId ?? null, title: fallbackTab.title, url: fallbackTab.chatUrl, workspaceId: fallbackTab.workspaceId ?? workspaceId, ...(fallbackTab.visibility !== undefined ? { visibility: fallbackTab.visibility } : {}), ...(fallbackTab.hasComments !== undefined ? { hasComments: fallbackTab.hasComments } : {}), }); return; } router.push(`/dashboard/${workspaceId}/new-chat`); }, 250); } } catch (error) { console.error("Error deleting thread:", error); toast.error(t("error_deleting_chat") || "Failed to delete chat"); } finally { setDeletingThreadId(null); } }, [activateChatThread, deleteThread, t, currentChatId, router, removeChatTab, workspaceId] ); const handleToggleArchive = useCallback( async (threadId: number, currentlyArchived: boolean) => { setArchivingThreadId(threadId); try { await archiveThread({ threadId, archived: !currentlyArchived }); toast.success( currentlyArchived ? t("chat_unarchived") || "Chat restored" : t("chat_archived") || "Chat archived" ); } catch (error) { console.error("Error archiving thread:", error); toast.error(t("error_archiving_chat") || "Failed to archive chat"); } finally { setArchivingThreadId(null); } }, [archiveThread, 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 renameThread({ threadId: renamingThread.id, title: newTitle.trim(), previousTitle: renamingThread.title, }); toast.success(t("chat_renamed") || "Chat renamed"); } 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, renameThread, t]); const handleClearSearch = useCallback(() => { setSearchQuery(""); }, []); const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads; const error = isSearchMode ? searchError : threadsError; const selectedFilterLabel = showArchived ? "Archived" : "Active"; return (

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

{!isSearchMode && ( setShowArchived(false)}> Active setShowArchived(true)}> Archived )}
setSearchQuery(e.target.value)} className="h-12 border-0 bg-muted pl-10 pr-9 text-base shadow-none" /> {searchQuery && ( )}
{isLoading ? (
{[75, 90, 55, 80, 65, 85].map((titleWidth) => (
))}
) : error ? (
{t("error_loading_chats") || "Error loading chats"}
) : threads.length > 0 ? (
{threads.map((thread, index) => { const isDeleting = deletingThreadId === thread.id; const isArchiving = archivingThreadId === thread.id; const isBusy = isDeleting || isArchiving; const isActive = currentChatId === thread.id; return (
0 && "border-t border-border/60" )} > {isMobile ? ( ) : (

{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}

)}
{thread.visibility === "SEARCH_SPACE" ? ( Shared ) : null} 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 chats"}

)}
{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 AllChatsWorkspaceContent({ workspaceId }: { workspaceId: string }) { return (
); }