react-virtual in messages list
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user