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 (
{greeting}
Let's build something impressive?
);
}
export default function MessageList({ messages, isProcessing }: MessageListProps) {
const scrollContainerRef = useRef(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 (
{messages.length === 0 && !isProcessing ? (
) : (
messages.map((msg) => (
))
)}
{/* Show thinking dots only when processing but no streaming message yet */}
{isProcessing && !currentTurnMessageId && }
{/* Sentinel — keeps scroll anchor at the bottom */}
);
}