import { useEffect, useRef, type CSSProperties, type ReactNode } from 'react'; import { MessageBubble, type MessageBubbleProps } from './MessageBubble.js'; import { PromptComposer } from './PromptComposer.js'; import { useChat } from './useChat.js'; import type { Message, UseChatOptions } from './types.js'; export interface ChatStreamProps extends UseChatOptions { /** Render slot above the messages list (e.g. a header or model picker). */ header?: ReactNode; /** Render slot below the composer (e.g. a disclaimer). */ footer?: ReactNode; /** Slot to render when there are no messages yet. */ emptyState?: ReactNode; /** Per-message custom renderer. Defaults to . */ renderMessage?: (message: Message) => ReactNode; /** Forwarded to every when default renderer is used. */ messageProps?: Omit; /** Placeholder shown inside . */ placeholder?: string; /** Tailwind escape hatch. */ className?: string; /** Inline style escape hatch. */ style?: CSSProperties; } /** * `` — opinionated composition of `useChat` + * `` + ``. The 80% path for any chat UI * in a ByteLyst product. Drop it in, point it at an endpoint, done. * * Behavior: * - Auto-scrolls to bottom on new message (sticky-bottom pattern). * - Shows Stop affordance while streaming. * - Surfaces transport errors inline below the messages list. * - Renders `emptyState` when there are zero messages. * * For more control, compose the primitives yourself — `useChat` returns * everything you need. */ export function ChatStream({ header, footer, emptyState, renderMessage, messageProps, placeholder, className, style, ...chatOptions }: ChatStreamProps) { const chat = useChat(chatOptions); const scrollRef = useRef(null); // Sticky-bottom scroll — only auto-scroll if the user was already at // (or near) the bottom. Otherwise their reading position is preserved. useEffect(() => { const el = scrollRef.current; if (!el) return; const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; if (distanceFromBottom < 80) { el.scrollTop = el.scrollHeight; } }, [chat.messages]); const messages = chat.messages; const showEmpty = messages.length === 0 && !chat.isLoading; return (
{header}
{showEmpty ? emptyState : messages.map(message => renderMessage ? ( {renderMessage(message)} ) : ( ), )} {chat.error && (
{chat.error.message}
)}
{footer}
); }