akseljoonas's picture
akseljoonas HF Staff
font update
92ce855
import { useEffect, useRef, useMemo, useCallback } from 'react';
import { Box, Stack, Typography } from '@mui/material';
import MessageBubble from './MessageBubble';
import ThinkingIndicator from './ThinkingIndicator';
import { useAgentStore } from '@/store/agentStore';
import { useSessionStore } from '@/store/sessionStore';
import { apiFetch } from '@/utils/api';
import { logger } from '@/utils/logger';
import type { Message } from '@/types/agent';
interface MessageListProps {
messages: Message[];
isProcessing: boolean;
}
function getGreeting(): string {
const h = new Date().getHours();
if (h < 12) return 'Morning';
if (h < 17) return 'Afternoon';
return 'Evening';
}
/** Minimal greeting shown when the conversation is empty. */
function WelcomeGreeting() {
const { user } = useAgentStore();
const firstName = user?.name?.split(' ')[0] || user?.username;
const greeting = firstName ? `${getGreeting()}, ${firstName}` : getGreeting();
return (
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
py: 8,
gap: 1.5,
}}
>
<Typography
sx={{
fontFamily: 'monospace',
fontSize: '1.6rem',
color: 'var(--text)',
fontWeight: 600,
}}
>
{greeting}
</Typography>
<Typography
color="text.secondary"
sx={{ fontFamily: 'monospace', fontSize: '0.9rem' }}
>
Let's build something impressive?
</Typography>
</Box>
);
}
export default function MessageList({ messages, isProcessing }: MessageListProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const stickToBottom = useRef(true);
const { activeSessionId } = useSessionStore();
const { removeLastTurn, currentTurnMessageId } = useAgentStore();
// ── Scroll-to-bottom helper ─────────────────────────────────────
const scrollToBottom = useCallback(() => {
const el = scrollContainerRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, []);
// ── Track user scroll intent ────────────────────────────────────
useEffect(() => {
const el = scrollContainerRef.current;
if (!el) return;
const onScroll = () => {
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
stickToBottom.current = distFromBottom < 80;
};
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
}, []);
// ── Auto-scroll on new messages / state changes ─────────────────
useEffect(() => {
if (stickToBottom.current) scrollToBottom();
}, [messages, isProcessing, scrollToBottom]);
// ── Auto-scroll on DOM mutations (streaming content growth) ─────
useEffect(() => {
const el = scrollContainerRef.current;
if (!el) return;
const observer = new MutationObserver(() => {
if (stickToBottom.current) {
el.scrollTop = el.scrollHeight;
}
});
observer.observe(el, {
childList: true,
subtree: true,
characterData: true,
});
return () => observer.disconnect();
}, []);
// Find the index of the last user message (start of the last turn)
const lastUserMsgId = useMemo(() => {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === 'user') return messages[i].id;
}
return null;
}, [messages]);
const handleUndoLastTurn = useCallback(async () => {
if (!activeSessionId) return;
try {
await apiFetch(`/api/undo/${activeSessionId}`, { method: 'POST' });
removeLastTurn(activeSessionId);
} catch (e) {
logger.error('Undo failed:', e);
}
}, [activeSessionId, removeLastTurn]);
return (
<Box
ref={scrollContainerRef}
sx={{
flex: 1,
overflow: 'auto',
px: { xs: 0.5, sm: 1, md: 2 },
py: { xs: 2, md: 3 },
display: 'flex',
flexDirection: 'column',
}}
>
<Stack
spacing={3}
sx={{
maxWidth: 880,
mx: 'auto',
width: '100%',
flex: messages.length === 0 && !isProcessing ? 1 : undefined,
}}
>
{messages.length === 0 && !isProcessing ? (
<WelcomeGreeting />
) : (
messages.map((msg) => (
<MessageBubble
key={msg.id}
message={msg}
isLastTurn={msg.id === lastUserMsgId}
onUndoTurn={handleUndoLastTurn}
isProcessing={isProcessing}
isStreaming={isProcessing && msg.id === currentTurnMessageId}
/>
))
)}
{/* Show thinking dots only when processing but no streaming message yet */}
{isProcessing && !currentTurnMessageId && <ThinkingIndicator />}
{/* Sentinel β€” keeps scroll anchor at the bottom */}
<div />
</Stack>
</Box>
);
}