import React, { Suspense, useState, useEffect, useRef, useCallback } from 'react'; import { classNames } from '~/utils/classNames'; import WithTooltip from '~/components/ui/Tooltip'; import { type Message, USER_RESPONSE_CATEGORY } from '~/lib/persistence/message'; import { MessageContents } from './components/MessageContents'; import { JumpToBottom } from './components/JumpToBottom'; import { APP_SUMMARY_CATEGORY, parseAppSummaryMessage } from '~/lib/persistence/messageAppSummary'; interface MessagesProps { id?: string; className?: string; hasPendingMessage?: boolean; pendingMessageStatus?: string; messages?: Message[]; } function renderAppSummary(message: Message, index: number) { const appSummary = parseAppSummaryMessage(message); if (!appSummary) { return null; } return (
Development Plan
{appSummary.description}
Features
{appSummary.features.map((feature) => (
{feature.description}
))} {appSummary.tests.length > 0 &&
Test Results
} {appSummary.tests.map((test) => (
{test.recordingId ? ( {test.title} ) : (
{test.title}
)}
))}
); } export const Messages = React.forwardRef( ({ messages = [], hasPendingMessage = false, pendingMessageStatus = '' }, ref) => { const [showDetailMessageIds, setShowDetailMessageIds] = useState([]); const [showJumpToBottom, setShowJumpToBottom] = useState(false); const containerRef = useRef(null); const setRefs = useCallback( (element: HTMLDivElement | null) => { containerRef.current = element; if (typeof ref === 'function') { ref(element); } else if (ref) { ref.current = element; } }, [ref], ); const handleScroll = () => { if (!containerRef.current) { return; } const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; setShowJumpToBottom(distanceFromBottom > 50); }; const scrollToBottom = () => { if (!containerRef.current) { return; } containerRef.current.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth', }); }; useEffect(() => { const container = containerRef.current; if (container) { container.addEventListener('scroll', handleScroll); return () => container.removeEventListener('scroll', handleScroll); } return undefined; }, []); useEffect(() => { if (!showJumpToBottom) { scrollToBottom(); } }, [messages, showJumpToBottom]); // Get the last user response before a given message, or null if there is // no user response between this and the last user message. const getLastUserResponse = (index: number) => { for (let i = index - 1; i >= 0; i--) { if (messages[i].category === USER_RESPONSE_CATEGORY) { return messages[i]; } if (messages[i].role === 'user') { return null; } } return null; }; // Return whether the app summary at index is the last for the associated user response. const isLastAppSummary = (index: number) => { let lastIndex = -1; for (let i = index; i < messages.length; i++) { const { category } = messages[i]; if (category === USER_RESPONSE_CATEGORY) { return lastIndex === index; } if (category === APP_SUMMARY_CATEGORY) { lastIndex = i; } } return lastIndex === index; }; const renderMessage = (message: Message, index: number) => { const { role, repositoryId } = message; const isUserMessage = role === 'user'; const isFirst = index === 0; const isLast = index === messages.length - 1; if (!isUserMessage && message.category && message.category !== USER_RESPONSE_CATEGORY) { const lastUserResponse = getLastUserResponse(index); const showDetails = !lastUserResponse || showDetailMessageIds.includes(lastUserResponse.id); if (message.category === APP_SUMMARY_CATEGORY) { // The default view only shows the last app summary for each user response. if (!isLastAppSummary(index) && !showDetails) { return null; } return renderAppSummary(message, index); } if (!showDetails) { return null; } } return (
} > {isUserMessage && (
)}
{!isUserMessage && message.category === 'UserResponse' && showDetailMessageIds.includes(message.id) && (
)} {!isUserMessage && message.category === 'UserResponse' && !showDetailMessageIds.includes(message.id) && (
)} {repositoryId && (
)}
); }; return (
{messages.length > 0 ? messages.map(renderMessage) : null} {hasPendingMessage && (
{pendingMessageStatus ? `${pendingMessageStatus}...` : ''}
)}
); }, );