({ messages, isStreaming }: VirtualMessageListProps)
| 37 | } |
| 38 | |
| 39 | export function VirtualMessageList({ messages, isStreaming }: VirtualMessageListProps) { |
| 40 | const scrollRef = useRef<HTMLDivElement>(null); |
| 41 | const isAtBottomRef = useRef(true); |
| 42 | |
| 43 | const virtualizer = useVirtualizer({ |
| 44 | count: messages.length, |
| 45 | getScrollElement: () => scrollRef.current, |
| 46 | estimateSize: (index) => estimateMessageHeight(messages[index]), |
| 47 | overscan: 5, |
| 48 | }); |
| 49 | |
| 50 | // Track whether the user has scrolled away from the bottom |
| 51 | const handleScroll = useCallback(() => { |
| 52 | const el = scrollRef.current; |
| 53 | if (!el) return; |
| 54 | const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; |
| 55 | isAtBottomRef.current = distanceFromBottom < 80; |
| 56 | }, []); |
| 57 | |
| 58 | // Auto-scroll to bottom when new messages arrive (if already at bottom) |
| 59 | useEffect(() => { |
| 60 | if (!isAtBottomRef.current) return; |
| 61 | const el = scrollRef.current; |
| 62 | if (!el) return; |
| 63 | if (isStreaming) { |
| 64 | // Instant scroll during streaming to keep up with tokens |
| 65 | el.scrollTop = el.scrollHeight; |
| 66 | } else { |
| 67 | el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); |
| 68 | } |
| 69 | }, [messages.length, isStreaming]); |
| 70 | |
| 71 | // Also scroll when the last streaming message content changes |
| 72 | useEffect(() => { |
| 73 | if (!isStreaming || !isAtBottomRef.current) return; |
| 74 | const el = scrollRef.current; |
| 75 | if (el) el.scrollTop = el.scrollHeight; |
| 76 | }); |
| 77 | |
| 78 | const items = virtualizer.getVirtualItems(); |
| 79 | |
| 80 | return ( |
| 81 | <div |
| 82 | ref={scrollRef} |
| 83 | className="flex-1 overflow-y-auto" |
| 84 | onScroll={handleScroll} |
| 85 | > |
| 86 | {/* Spacer that gives the virtualizer its total height */} |
| 87 | <div |
| 88 | style={{ height: virtualizer.getTotalSize(), position: "relative" }} |
| 89 | className="max-w-3xl mx-auto px-4 py-6" |
| 90 | > |
| 91 | {items.map((virtualItem) => { |
| 92 | const message = messages[virtualItem.index]; |
| 93 | return ( |
| 94 | <div |
| 95 | key={virtualItem.key} |
| 96 | data-index={virtualItem.index} |
nothing calls this directly
no test coverage detected