'use client' import * as React from 'react' import { cn } from '../../lib/utils' import { ChatMessage } from './ChatMessage' import type { ChatMessageData } from './types' export interface ChatThreadProps { messages: ChatMessageData[] /** Rendered when there are no messages. */ emptyState?: React.ReactNode /** Custom message renderer; defaults to . */ renderMessage?: (message: ChatMessageData) => React.ReactNode /** Auto-scroll to the newest message on change. Default true. */ autoScroll?: boolean renderContent?: (content: string, message: ChatMessageData) => React.ReactNode /** Show an animated "working" indicator at the end (the agent is processing). */ busy?: boolean /** Label shown next to the working indicator. Default "Working…". */ busyLabel?: string className?: string } /** Animated three-dot "agent is working" row, aligned like an assistant message. */ function WorkingIndicator({ label = 'Working…' }: { label?: string }) { return (
) } /** * Scrollable, auto-scrolling list of chat messages. App-agnostic. */ export function ChatThread({ messages, emptyState, renderMessage, autoScroll = true, renderContent, busy = false, busyLabel, className, }: ChatThreadProps) { const anchorRef = React.useRef(null) React.useEffect(() => { if (autoScroll) anchorRef.current?.scrollIntoView({ block: 'end' }) }, [messages, busy, autoScroll]) // Empty thread + not working → the caller's empty state. if (messages.length === 0 && !busy && emptyState) { return (
{emptyState}
) } return (
{messages.map((m) => renderMessage ? ( {renderMessage(m)} ) : ( ) )} {busy && }
) }