import React, { useEffect, useMemo, useRef, useState } from "react"; import { useCurrentConversationState, useCurrentPartialContent, } from "../../../contexts/ChatContext"; import { ParsedBlock, parseMarkdownAndCode } from "../../../utils/parseMarkdownAndCode"; import MarkdownBlock from "../Blocks/MarkdownBlock"; import CodeBlock from "../Blocks/CodeBlock"; import AssistantThinkingSummary from "./AssistantThinkingSummary"; // Component to render a list of completed blocks. const CompletedBlocks: React.FC<{ blocks: ParsedBlock[] }> = React.memo(({ blocks }) => { if (blocks.length === 0) return null; return ( <> {blocks.map((block) => { if (block.type === "markdown") { return ( ); } if (block.type === "code") { return ( ); } return null; })} ); }); const StreamingAssistantMessage: React.FC = () => { const conversationState = useCurrentConversationState(); // All content received from the server so far const fullContent = useCurrentPartialContent(); // How many characters make up completed blocks const finalizedLengthRef = useRef(0); // The completed blocks (don't need to be re-parsed or rendered) const [completedBlocks, setCompletedBlocks] = useState([]); // The latest block (is re-parsed until completed) const [latestBlock, setLatestBlock] = useState(null); // Each time the fullContent changes, we update our incremental parser. useEffect(() => { if (!fullContent) { finalizedLengthRef.current = 0; setCompletedBlocks([]); setLatestBlock(null); return; } // Compute the string making up the latest block const textToParse = fullContent.slice(finalizedLengthRef.current); // Parse the text const parsed = parseMarkdownAndCode(textToParse); // If nothing is parsed, do nothing. if (parsed.length === 0) return; // If parser returns >1 block, mark all but the last block as complete if (parsed.length > 1) { const completeBlocks = parsed.slice(0, parsed.length - 1); const lastCompleteBlock = completeBlocks[completeBlocks.length - 1]; // Update completedBlocks with these new complete blocks. setCompletedBlocks((prev) => [...prev, ...completeBlocks]); // Update the finalized length: add the length (endIndex) of the last complete block // Note: since parseMarkdownAndCode works on the newSegment, its indices are relative to newSegment. // To get the absolute index, add the current finalizedLength. const newFinalized = finalizedLengthRef.current + lastCompleteBlock.endIndex; finalizedLengthRef.current = newFinalized; // The current (latest) block is the last block returned. setLatestBlock(parsed[parsed.length - 1]); } else { // Only one block was parsed: update latestBlock. setLatestBlock(parsed[0]); } }, [fullContent]); const timeline = conversationState?.timeline ?? []; const hasVisibleTimeline = timeline.some(entry => entry.kind !== 'reasoning' || entry.text.trim()); const showExpandedStatus = !conversationState?.hasStartedStreaming; const thoughtDurationMs = conversationState?.thoughtDurationMs ?? ( conversationState?.startedAt ? Math.max(1, Date.now() - conversationState.startedAt) : 0 ); // Memoize the rendering of completed blocks. const renderedCompletedBlocks = useMemo(() => { return ; }, [completedBlocks]); // Memoize rendering for the current (partial) block. const renderedLatestBlock = useMemo(() => { if (!latestBlock) return null; if (latestBlock.type === "markdown") { return ( ); } if (latestBlock.type === "code") { return ( ); } return null; }, [latestBlock]); return (
{(showExpandedStatus || hasVisibleTimeline) && ( )}
{renderedCompletedBlocks} {renderedLatestBlock}
); }; export default React.memo(StreamingAssistantMessage);