import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { ArrowDown } from "lucide-react"; import { useTranslation } from "react-i18next"; import { ThreadMessages } from "@/components/thread/ThreadMessages"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { UIMessage } from "@/lib/types"; interface ThreadViewportProps { messages: UIMessage[]; isStreaming: boolean; composer: ReactNode; emptyState?: ReactNode; } const NEAR_BOTTOM_PX = 48; export function ThreadViewport({ messages, isStreaming, composer, emptyState, }: ThreadViewportProps) { const { t } = useTranslation(); const scrollRef = useRef(null); const [atBottom, setAtBottom] = useState(true); const hasMessages = messages.length > 0; const scrollToBottom = useCallback((smooth = false) => { const el = scrollRef.current; if (!el) return; el.scrollTo({ top: el.scrollHeight, behavior: smooth ? "smooth" : "auto", }); }, []); useEffect(() => { if (!atBottom) return; scrollToBottom(!isStreaming); }, [messages, isStreaming, atBottom, scrollToBottom]); useEffect(() => { const el = scrollRef.current; if (!el) return; const onScroll = () => { const distance = el.scrollHeight - el.scrollTop - el.clientHeight; setAtBottom(distance < NEAR_BOTTOM_PX); }; onScroll(); el.addEventListener("scroll", onScroll, { passive: true }); return () => el.removeEventListener("scroll", onScroll); }, []); return (
{hasMessages ? (
{composer}
) : (
{emptyState}
{composer}
)}
{!atBottom && ( )}
); }