react-virtual in messages list

This commit is contained in:
2026-02-20 23:36:56 +00:00
parent 68c7973281
commit f089a89eae
7 changed files with 1836 additions and 31 deletions
+54 -29
View File
@@ -1,18 +1,10 @@
import type { RefObject } from 'react';
import { ArrowDown } from 'lucide-react';
import { useVirtualizer } from '@tanstack/react-virtual';
import type { ChatMessage } from './types';
import { MessageBubble, StreamingBubble } from './MessageBubble';
type MessageListProps = {
messages: ChatMessage[];
streamingText: string;
isGenerating: boolean;
showJumpToBottom: boolean;
onJumpToBottom: () => void;
onQuestionAnswer?: (text: string) => void;
scrollViewportRef: RefObject<HTMLDivElement | null>;
bottomRef: RefObject<HTMLDivElement | null>;
};
const MESSAGE_HEIGHT = 100; // Estimated height per message bubble
const OVERSCAN = 5;
export const MessageList = ({
messages,
@@ -23,30 +15,63 @@ export const MessageList = ({
onQuestionAnswer,
scrollViewportRef,
bottomRef,
}: MessageListProps) => (
<div className="flex-1 min-h-0 relative">
<div ref={scrollViewportRef} className="h-full overflow-y-auto">
<div className="p-4 space-y-3">
}: {
messages: ChatMessage[];
streamingText: string;
isGenerating: boolean;
showJumpToBottom: boolean;
onJumpToBottom: () => void;
onQuestionAnswer?: (text: string) => void;
scrollViewportRef: React.RefObject<HTMLDivElement | null>;
bottomRef: React.RefObject<HTMLDivElement | null>;
}) => {
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => scrollViewportRef.current,
estimateSize: () => MESSAGE_HEIGHT,
overscan: OVERSCAN,
});
return (
<div className="flex-1 min-h-0 relative">
<div ref={scrollViewportRef} className="h-full overflow-y-auto">
{messages.length === 0 && !isGenerating && (
<div className="flex items-center justify-center h-full min-h-[200px] text-duck-dark/30 text-sm">
Send a message to start
</div>
)}
{messages.map((msg, i) => (
<MessageBubble key={i} message={msg} onAnswer={onQuestionAnswer} />
))}
<div
className="p-4 space-y-3"
style={{ height: virtualizer.getTotalSize() }}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const msg = messages[virtualRow.index]!;
return (
<div
key={virtualRow.key}
className="absolute left-0 w-full"
style={{
top: virtualRow.start,
height: virtualRow.size,
}}
>
<MessageBubble message={msg} onAnswer={onQuestionAnswer} />
</div>
);
})}
</div>
{isGenerating && <StreamingBubble text={streamingText} />}
<div ref={bottomRef} />
</div>
</div>
{showJumpToBottom && (
<button
onClick={onJumpToBottom}
className="absolute bottom-2 left-1/2 -translate-x-1/2 bg-duck-teal text-white rounded-full p-1.5 shadow-lg hover:bg-duck-teal/90 transition-colors cursor-pointer"
>
<ArrowDown className="h-4 w-4" />
</button>
)}
</div>
);
{showJumpToBottom && (
<button
onClick={onJumpToBottom}
className="absolute bottom-2 left-1/2 -translate-x-1/2 bg-duck-teal text-white rounded-full p-1.5 shadow-lg hover:bg-duck-teal/90 transition-colors cursor-pointer"
>
<ArrowDown className="h-4 w-4" />
</button>
)}
</div>
);
};