import React from 'react'; import { RiArrowDownLine } from '@remixicon/react'; import { ChatInput } from './ChatInput'; import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; import { Skeleton } from '@/components/ui/skeleton'; import ChatEmptyState from './ChatEmptyState'; import MessageList from './MessageList'; import { ScrollShadow } from '@/components/ui/ScrollShadow'; import { useChatScrollManager } from '@/hooks/useChatScrollManager'; import { useDeviceInfo } from '@/lib/device'; import { Button } from '@/components/ui/button'; import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar'; import { TimelineDialog } from './TimelineDialog'; export const ChatContainer: React.FC = () => { const { currentSessionId, messages, permissions, questions, streamingMessageIds, isLoading, loadMessages, loadMoreMessages, updateViewportAnchor, sessionMemoryState, openNewSessionDraft, isSyncing, messageStreamStates, trimToViewportWindow, sessionActivityPhase, newSessionDraft, } = useSessionStore(); const { isTimelineDialogOpen, setTimelineDialogOpen, } = useUIStore(); const streamingMessageId = React.useMemo(() => { if (!currentSessionId) return null; return streamingMessageIds.get(currentSessionId) ?? null; }, [currentSessionId, streamingMessageIds]); const { isMobile } = useDeviceInfo(); const draftOpen = Boolean(newSessionDraft?.open); React.useEffect(() => { if (!currentSessionId && !draftOpen) { openNewSessionDraft(); } }, [currentSessionId, draftOpen, openNewSessionDraft]); const sessionMessages = React.useMemo(() => { return currentSessionId ? messages.get(currentSessionId) || [] : []; }, [currentSessionId, messages]); const sessionPermissions = React.useMemo(() => { return currentSessionId ? permissions.get(currentSessionId) || [] : []; }, [currentSessionId, permissions]); const sessionQuestions = React.useMemo(() => { return currentSessionId ? questions.get(currentSessionId) || [] : []; }, [currentSessionId, questions]); const sessionBlockingCards = React.useMemo(() => { return [...sessionPermissions, ...sessionQuestions]; }, [sessionPermissions, sessionQuestions]); const { scrollRef, handleMessageContentChange, getAnimationHandlers, showScrollButton, scrollToBottom, scrollToPosition, isPinned, } = useChatScrollManager({ currentSessionId, sessionMessages, streamingMessageId, sessionMemoryState, updateViewportAnchor, isSyncing, isMobile, messageStreamStates, sessionPermissions: sessionBlockingCards, trimToViewportWindow, }); const memoryState = React.useMemo(() => { if (!currentSessionId) { return null; } return sessionMemoryState.get(currentSessionId) ?? null; }, [currentSessionId, sessionMemoryState]); const hasMoreAbove = Boolean(memoryState?.hasMoreAbove); const [isLoadingOlder, setIsLoadingOlder] = React.useState(false); React.useEffect(() => { setIsLoadingOlder(false); }, [currentSessionId]); const handleLoadOlder = React.useCallback(async () => { if (!currentSessionId || isLoadingOlder) { return; } const container = scrollRef.current; const prevHeight = container?.scrollHeight ?? null; const prevTop = container?.scrollTop ?? null; setIsLoadingOlder(true); try { await loadMoreMessages(currentSessionId, 'up'); if (container && prevHeight !== null && prevTop !== null) { const heightDiff = container.scrollHeight - prevHeight; scrollToPosition(prevTop + heightDiff, { instant: true }); } } finally { setIsLoadingOlder(false); } }, [currentSessionId, isLoadingOlder, loadMoreMessages, scrollRef, scrollToPosition]); // Scroll to a specific message by ID (for timeline dialog) const scrollToMessage = React.useCallback((messageId: string) => { const container = scrollRef.current; if (!container) return; // Find the message element by looking for data-message-id attribute const messageElement = container.querySelector(`[data-message-id="${messageId}"]`) as HTMLElement; if (messageElement) { // Scroll to the message with some padding (50px from top) const containerRect = container.getBoundingClientRect(); const messageRect = messageElement.getBoundingClientRect(); const offset = 50; const scrollTop = messageRect.top - containerRect.top + container.scrollTop - offset; container.scrollTo({ top: scrollTop, behavior: 'smooth' }); } }, [scrollRef]); React.useEffect(() => { if (!currentSessionId) { return; } const hasSessionMessages = messages.has(currentSessionId); const existingMessages = hasSessionMessages ? messages.get(currentSessionId) ?? [] : []; if (existingMessages.length > 0) { return; } const load = async () => { try { await loadMessages(currentSessionId); } finally { const currentPhase = sessionActivityPhase?.get(currentSessionId) ?? 'idle'; const isActivePhase = currentPhase === 'busy' || currentPhase === 'cooldown'; // When pinned and active, scroll is already maintained automatically const shouldSkipScroll = isActivePhase && isPinned; if (!shouldSkipScroll) { if (typeof window === 'undefined') { scrollToBottom(); } else { window.requestAnimationFrame(() => { scrollToBottom(); }); } } } }; void load(); }, [currentSessionId, isPinned, loadMessages, messages, scrollToBottom, sessionActivityPhase]); if (!currentSessionId && !draftOpen) { return (