"use client"; import Link from "next/link"; import { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { WorkspaceBody, WorkspaceContainer, WorkspaceHeader, } from "@/components/workspace/workspace-container"; import { useI18n } from "@/core/i18n/hooks"; import { useInfiniteThreads } from "@/core/threads/hooks"; import { pathOfThread, titleOfThread } from "@/core/threads/utils"; import { formatTimeAgo } from "@/core/utils/datetime"; export default function ChatsPage() { const { t } = useI18n(); const { data: infiniteThreads, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteThreads(); const threads = useMemo( () => infiniteThreads?.pages.flat() ?? [], [infiniteThreads], ); const [search, setSearch] = useState(""); const isSearching = search.trim().length > 0; useEffect(() => { document.title = `${t.pages.chats} - ${t.pages.appName}`; }, [t.pages.chats, t.pages.appName]); const filteredThreads = useMemo(() => { return threads.filter((thread) => { return titleOfThread(thread).toLowerCase().includes(search.toLowerCase()); }); }, [threads, search]); // Sentinel-based auto load-more for the unfiltered list (issue #3482). // In search mode we deliberately do NOT auto-paginate, otherwise an empty // filtered view would keep the sentinel in the viewport and drain the // entire backend list one page at a time. Searching falls back to an // explicit button so users can still reach older conversations on demand. const sentinelRef = useRef(null); useEffect(() => { const element = sentinelRef.current; if (!element || !hasNextPage || isSearching) { return; } const observer = new IntersectionObserver( ([entry]) => { if (entry?.isIntersecting && hasNextPage && !isFetchingNextPage) { void fetchNextPage(); } }, { rootMargin: "200px 0px 200px 0px" }, ); observer.observe(element); return () => observer.disconnect(); }, [fetchNextPage, hasNextPage, isFetchingNextPage, isSearching]); return (
setSearch(e.target.value)} />
{filteredThreads?.map((thread) => (
{titleOfThread(thread)}
{thread.updated_at && (
{formatTimeAgo(thread.updated_at)}
)}
))} {hasNextPage && !isSearching && (
); }