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 */}
); }