import { useRef, useEffect, useState, createContext, useContext, useMemo, type ReactNode } from 'react'; // --------------------------------------------------------------------------- // ConversationContext — shared state for child ai-elements components // --------------------------------------------------------------------------- interface ConversationContextValue { isStreaming: boolean; setIsStreaming: (v: boolean) => void; messages: unknown[]; setMessages: (msgs: unknown[]) => void; } const ConversationContext = createContext(null); export function useConversationContext() { const ctx = useContext(ConversationContext); if (!ctx) { throw new Error('useConversationContext must be used within ConversationProvider'); } return ctx; } export function ConversationProvider({ children }: { children: ReactNode }) { const [isStreaming, setIsStreaming] = useState(false); const [messages, setMessages] = useState([]); const value = useMemo( () => ({ isStreaming, setIsStreaming, messages, setMessages }), [isStreaming, messages], ); return ( {children} ); } // --------------------------------------------------------------------------- // Conversation container with auto-stick-to-bottom scroll behavior // --------------------------------------------------------------------------- /** * Conversation container with auto-stick-to-bottom scroll behavior. * * Inspired by DeerFlow's use-stick-to-bottom pattern: * - Stays pinned to bottom during streaming * - Remembers user's scroll position when they scroll up * - Auto-scrolls back to bottom on new content when near the bottom */ interface ConversationProps { children: ReactNode; className?: string; } const SCROLL_THRESHOLD = 80; // px from bottom to consider "at bottom" export function Conversation({ children, className = '' }: ConversationProps) { const containerRef = useRef(null); const isAtBottomRef = useRef(true); const observerRef = useRef(null); // Track whether user is near the bottom const handleScroll = () => { const el = containerRef.current; if (!el) return; const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; isAtBottomRef.current = distanceFromBottom < SCROLL_THRESHOLD; }; // Auto-scroll to bottom when content changes and user is at bottom useEffect(() => { const el = containerRef.current; if (!el) return; observerRef.current = new ResizeObserver(() => { if (isAtBottomRef.current) { el.scrollTop = el.scrollHeight; } }); observerRef.current.observe(el); return () => { observerRef.current?.disconnect(); }; }, []); // Also observe child list changes (new messages) useEffect(() => { const el = containerRef.current; if (!el) return; const mutationObserver = new MutationObserver(() => { if (isAtBottomRef.current) { el.scrollTop = el.scrollHeight; } }); mutationObserver.observe(el, { childList: true, subtree: true }); return () => { mutationObserver.disconnect(); }; }, []); return (
{children}
); }