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
@@ -1,4 +1,49 @@
import { MessageSquare } from 'lucide-react';
import { useChatSessions } from '@/state/useChatSessions';
export const ChatList = () => {
return <h1>ChatList</h1>;
const { sessions } = useChatSessions();
if (sessions.length === 0) {
return (
<div className="h-full flex items-center justify-center text-duck-dark/30 dark:text-foreground/30 text-sm">
No sessions yet. Start a new chat!
</div>
);
}
return (
<div className="space-y-1.5 p-3">
{sessions.map((session) => (
<a
key={session.id}
href={`/chat/${session.id}`}
className="block p-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 hover:bg-duck-teal/5 dark:hover:bg-duck-teal/10 transition-colors group"
>
<div className="flex items-start gap-2 min-w-0">
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60 mt-0.5" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-duck-dark/80 dark:text-foreground/80 truncate">
{session.title}
</div>
<div className="text-xs text-duck-dark/40 dark:text-foreground/40">
{new Date(session.createdAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
{session.model && (
<div className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate">
{session.model.includes('/') ? session.model.replace('/', ' - ') : session.model}
</div>
)}
</div>
</div>
</a>
))}
</div>
);
};
@@ -1 +1,6 @@
export {};
export { ChatPanel } from './ChatPanel';
export { EmbeddableChat, type Attachment } from './EmbeddableChat';
export { InputArea } from './InputArea';
export { Settings } from './Settings';
export { usePi } from './usePi';
export { ChatList } from './ChatList';
+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>
);
};