import React, { useCallback, useLayoutEffect, useMemo, useRef } from 'react'; import MessageWrapper from './MessageWrapper'; import QuickActions from './QuickActions'; import WorkflowsHome from './WorkflowsHome'; import InputSection from './InputSection'; import StreamingAssistantMessage from './Messages/StreamingAssistantMessage'; import StaticAssistantMessage from './Messages/StaticAssistantMessage'; import MessagesSkeleton from './MessagesSkeleton'; import { BrandMark } from '../ui'; import { useCurrentConversationId, useMessages, useMessagesLoading, useSendMessage, useRetryError, useDismissError, useCurrentConversationState, useEditAndResubmitLatestUserMessage, useCurrentStoppedWorkingSummary, } from '../../contexts/ChatContext'; import useRenderTracker from '../../hooks/useRenderTracker'; import { usePopulateComposerDraft } from '../../contexts/ComposerContext'; import { useSetIsWorkflowsModalOpen } from '../../contexts/UIContext'; import { useFeatureFlag } from '../../featureFlags'; import { IS_WORDPRESS } from '../../constants'; interface ChatAreaProps { scrollRef: React.RefObject; scrollToBottom: (behavior: ScrollBehavior) => void; } const ChatArea = React.memo(({ scrollRef, scrollToBottom }) => { const currentConversationId = useCurrentConversationId(); const messages = useMessages(); const messagesLoading = useMessagesLoading(); const sendMessage = useSendMessage(); const populateComposerDraft = usePopulateComposerDraft(); const retryError = useRetryError(); const dismissError = useDismissError(); const editAndResubmitLatestUserMessage = useEditAndResubmitLatestUserMessage(); const currentConversationState = useCurrentConversationState(); const stoppedWorkingSummary = useCurrentStoppedWorkingSummary(); // Workflows are plugin-only: the web build isn't connected to a site, so it // keeps the old example quick-actions instead. const workflowsEnabled = useFeatureFlag('workflows') && IS_WORDPRESS; const openWorkflowsModal = useSetIsWorkflowsModalOpen(); // So we can refer to the container if needed const chatContainerRef = useRef(null); useRenderTracker('ChatArea', { currentConversationId, messages, sendMessage, populateComposerDraft, retryError, dismissError, currentConversationState, }); // Retry & Dismiss const handleRetry = useCallback( (id: string) => { retryError(id); }, [retryError] ); const handleDismiss = useCallback( (id: string) => { dismissError(id); }, [dismissError] ); const handleWorkflowClick = useCallback( async (text: string) => { populateComposerDraft(text); }, [populateComposerDraft] ); // Completed messages const completedMessages = useMemo(() => { if (!currentConversationId) return null; let userTurnIndex = 0; let currentTurnKey: string | undefined; const latestUserMessageId = [...messages].reverse().find((message) => message.role === 'user')?.message_id; const canEditLatestUser = currentConversationState?.phase === 'IDLE'; return messages.map((message) => { if (message.role === 'user') { userTurnIndex += 1; currentTurnKey = `${currentConversationId}:turn:${userTurnIndex}`; } const turnKey = message.role === 'assistant' ? currentTurnKey : undefined; return ( ); }); }, [ messages, handleRetry, handleDismiss, currentConversationId, currentConversationState?.phase, editAndResubmitLatestUserMessage, ]); const pendingAssistantMessage = useMemo(() => { if (!currentConversationId) return null; if (currentConversationState?.phase !== 'THINKING' && currentConversationState?.phase !== 'STREAMING') { return null; } return ; }, [currentConversationState, currentConversationId]); const stoppedWorkingMessage = useMemo(() => { if (!currentConversationId || !stoppedWorkingSummary || pendingAssistantMessage) { return null; } return ( ); }, [currentConversationId, pendingAssistantMessage, stoppedWorkingSummary]); // Rendered messages const renderedMessages = useMemo(() => { return [...(completedMessages || []), pendingAssistantMessage, stoppedWorkingMessage].filter(Boolean); }, [completedMessages, pendingAssistantMessage, stoppedWorkingMessage]); // When entering or switching a conversation, land at the bottom INSTANTLY and // before paint (useLayoutEffect) — so a reload is already scrolled to the latest // message rather than visibly scrolling there. We wait until that conversation's // messages are present, then pin once. const prevConversationIdRef = useRef(null); const pendingInitialScrollRef = useRef(false); useLayoutEffect(() => { const conversationId = currentConversationId || null; if (conversationId !== prevConversationIdRef.current) { prevConversationIdRef.current = conversationId; pendingInitialScrollRef.current = Boolean(conversationId); } if (!pendingInitialScrollRef.current || !conversationId || messages.length === 0) { return; } const el = scrollRef.current; if (!el) return; el.scrollTop = el.scrollHeight; pendingInitialScrollRef.current = false; }, [currentConversationId, messages, scrollRef]); // Render return (
{!currentConversationId ? (

How can I help with your WordPress site?

Ask a question or pick a starting point below.

{workflowsEnabled ? ( openWorkflowsModal(true)} /> ) : ( <>
{IS_WORDPRESS ? 'Workflows' : 'Examples'}
)}
) : messagesLoading ? (
) : (
{renderedMessages}
)}
); }); export default ChatArea;