"use client"; import { SidebarGroupLabel, SidebarMenuSub } from "ui/sidebar"; import Link from "next/link"; import { SidebarMenuAction, SidebarMenuButton, SidebarMenuSkeleton, SidebarMenuSubItem, } from "ui/sidebar"; import { SidebarGroupContent, SidebarMenu, SidebarMenuItem } from "ui/sidebar"; import { SidebarGroup } from "ui/sidebar"; import { ThreadDropdown } from "../thread-dropdown"; import { ChevronDown, ChevronUp, MoreHorizontal, Trash } from "lucide-react"; import { useMounted } from "@/hooks/use-mounted"; import { appStore } from "@/app/store"; import { Button } from "ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "ui/dropdown-menu"; import { deleteThreadsAction, deleteUnarchivedThreadsAction, } from "@/app/api/chat/actions"; import { fetcher } from "lib/utils"; import { toast } from "sonner"; import { useShallow } from "zustand/shallow"; import { useRouter } from "next/navigation"; import useSWR, { mutate } from "swr"; import { handleErrorWithToast } from "ui/shared-toast"; import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { TextShimmer } from "ui/text-shimmer"; import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip"; import { deduplicateByKey, groupBy } from "lib/utils"; import { ChatThread } from "app-types/chat"; type ThreadGroup = { label: string; threads: any[]; }; const MAX_THREADS_COUNT = 40; export function AppSidebarThreads() { const mounted = useMounted(); const router = useRouter(); const t = useTranslations("Layout"); const [storeMutate, currentThreadId, generatingTitleThreadIds] = appStore( useShallow((state) => [ state.mutate, state.currentThreadId, state.generatingTitleThreadIds, ]), ); // State to track if expanded view is active const [isExpanded, setIsExpanded] = useState(false); const { data: threadList, isLoading } = useSWR("/api/thread", fetcher, { onError: handleErrorWithToast, fallbackData: [], onSuccess: (data) => { storeMutate((prev) => { const groupById = groupBy(prev.threadList, "id"); const generatingTitleThreads = prev.generatingTitleThreadIds .map((id) => { return groupById[id]?.[0]; }) .filter(Boolean) as ChatThread[]; const list = deduplicateByKey( generatingTitleThreads.concat(data), "id", ); return { threadList: list.map((v) => { const target = groupById[v.id]?.[0]; if (!target) return v; if (target.title && !v.title) return { ...v, title: target.title, }; return v; }), }; }); }, }); // Check if we have 40 or more threads to display "View All" button const hasExcessThreads = threadList && threadList.length >= MAX_THREADS_COUNT; // Use either limited or full thread list based on expanded state const displayThreadList = useMemo(() => { if (!threadList) return []; return !isExpanded && hasExcessThreads ? threadList.slice(0, MAX_THREADS_COUNT) : threadList; }, [threadList, hasExcessThreads, isExpanded]); const threadGroupByDate = useMemo(() => { if (!displayThreadList || displayThreadList.length === 0) { return []; } const today = new Date(); today.setHours(0, 0, 0, 0); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const lastWeek = new Date(today); lastWeek.setDate(lastWeek.getDate() - 7); const groups: ThreadGroup[] = [ { label: t("today"), threads: [] }, { label: t("yesterday"), threads: [] }, { label: t("lastWeek"), threads: [] }, { label: t("older"), threads: [] }, ]; displayThreadList.forEach((thread) => { const threadDate = (thread.lastMessageAt ? new Date(thread.lastMessageAt) : new Date(thread.createdAt)) || new Date(); threadDate.setHours(0, 0, 0, 0); if (threadDate.getTime() === today.getTime()) { groups[0].threads.push(thread); } else if (threadDate.getTime() === yesterday.getTime()) { groups[1].threads.push(thread); } else if (threadDate.getTime() >= lastWeek.getTime()) { groups[2].threads.push(thread); } else { groups[3].threads.push(thread); } }); // Filter out empty groups return groups.filter((group) => group.threads.length > 0); }, [displayThreadList]); const handleDeleteAllThreads = async () => { await toast.promise(deleteThreadsAction(), { loading: t("deletingAllChats"), success: () => { mutate("/api/thread"); router.push("/"); return t("allChatsDeleted"); }, error: t("failedToDeleteAllChats"), }); }; const handleDeleteUnarchivedThreads = async () => { await toast.promise(deleteUnarchivedThreadsAction(), { loading: t("deletingUnarchivedChats"), success: () => { mutate("/api/thread"); router.push("/"); return t("unarchivedChatsDeleted"); }, error: t("failedToDeleteUnarchivedChats"), }); }; if (isLoading || threadList?.length === 0) return (

{t("recentChats")}

{isLoading ? ( Array.from({ length: 12 }).map( (_, index) => mounted && , ) ) : (

{t("noConversationsYet")}

)}
); return ( <> {threadGroupByDate.map((group, index) => { const isFirst = index === 0; return (

{group.label}

{isFirst && ( {t("deleteAllChats")} {t("deleteUnarchivedChats")} )} {group.threads.map((thread) => (
{generatingTitleThreadIds.includes( thread.id, ) ? ( {thread.title || "New Chat"} ) : (

{thread.title || "New Chat"}

)}
{thread.title || "New Chat"}
))} ); })} {hasExcessThreads && ( {/* TODO: Later implement a dedicated search/all chats page instead of this expand functionality */}
)} ); }