diff --git a/src/modules/blocks/AutoBlocksRenderer.tsx b/src/modules/blocks/AutoBlocksRenderer.tsx index 70601ba80..0ae18f4c3 100644 --- a/src/modules/blocks/AutoBlocksRenderer.tsx +++ b/src/modules/blocks/AutoBlocksRenderer.tsx @@ -3,25 +3,19 @@ import type { Diff as SanityTextDiff } from '@sanity/diff-match-patch'; import type { ContentScaling } from '~/common/app.theme'; import type { DMessageRole } from '~/common/stores/chat/chat.message'; -import { shallowEquals } from '~/common/util/hooks/useShallowObject'; import { BlocksContainer } from './BlocksContainers'; -import { RenderBlockInputs } from './blocks.types'; import { RenderDangerousHtml } from './danger-html/RenderDangerousHtml'; import { RenderImageURL } from './image/RenderImageURL'; import { RenderMarkdown, RenderMarkdownMemo } from './markdown/RenderMarkdown'; import { RenderPlainChatText } from './plaintext/RenderPlainChatText'; import { RenderTextDiff } from './textdiff/RenderTextDiff'; import { ToggleExpansionButton } from './ToggleExpansionButton'; -import { parseBlocksFromText } from './blocks.textparser'; import { renderCodeMemoOrNot } from './code/RenderCode'; +import { useAutoBlocksMemo, useTextCollapser } from './blocks.hooks'; import { useScaledCodeSx, useScaledImageSx, useScaledTypographySx, useToggleExpansionButtonSx } from './blocks.styles'; -// How long is the user collapsed message -const USER_COLLAPSED_LINES: number = 8; - - type BlocksRendererProps = { // required text: string; @@ -58,77 +52,23 @@ type BlocksRendererProps = { */ export const AutoBlocksRenderer = React.forwardRef((props, ref) => { - // state - const [forceUserExpanded, setForceUserExpanded] = React.useState(false); - const prevBlocksRef = React.useRef([]); - - // derived state - const { text: _text, renderSanityTextDiffs } = props; + // props-derived state const fromAssistant = props.fromRole === 'assistant'; const fromSystem = props.fromRole === 'system'; const fromUser = props.fromRole === 'user'; - const isUserCommand = fromUser && _text.startsWith('/'); - - - // Memo text, which could be 'collapsed' to a few lines in case of user messages - - const { text, isTextCollapsed } = React.useMemo(() => { - if (fromUser && !forceUserExpanded) { - const textLines = _text.split('\n'); - if (textLines.length > USER_COLLAPSED_LINES) - return { text: textLines.slice(0, USER_COLLAPSED_LINES).join('\n'), isTextCollapsed: true }; - } - return { text: _text, isTextCollapsed: false }; - }, [forceUserExpanded, fromUser, _text]); - - const handleToggleExpansion = React.useCallback(() => { - setForceUserExpanded(on => !on); - }, []); - - - // Block splitter, with memo and special recycle of former blocks, to help React minimize render work - - const autoBlocksMemo = React.useMemo(() => { - - // follow outside direction, or activate the auto-splitter based on content - const newBlocks: RenderBlockInputs = []; - if (props.renderAsCodeWithTitle) - newBlocks.push({ bkt: 'code-bk', title: props.renderAsCodeWithTitle, code: text, isPartial: false }); - else if (fromSystem) - newBlocks.push({ bkt: 'md-bk', content: text }); - else if (renderSanityTextDiffs && renderSanityTextDiffs.length >= 1) - newBlocks.push({ bkt: 'txt-diffs-bk', sanityTextDiffs: renderSanityTextDiffs }); - else - newBlocks.push(...parseBlocksFromText(text)); - - // recycle the previous blocks if they are the same, for stable references to React - const recycledBlocks: RenderBlockInputs = []; - for (let i = 0; i < newBlocks.length; i++) { - const newBlock = newBlocks[i]; - const prevBlock = prevBlocksRef.current[i] ?? undefined; - - // Check if the new block can be replaced by the previous block to maintain reference stability - if (prevBlock && shallowEquals(prevBlock, newBlock)) { - recycledBlocks.push(prevBlock); - } else { - // Once a block doesn't match, we use the new blocks from this point forward. - recycledBlocks.push(...newBlocks.slice(i)); - break; - } - } - - // Update prevBlocksRef with the current blocks for the next render - prevBlocksRef.current = recycledBlocks; - - // Apply specialDiagramMode filter if applicable - return props.specialDiagramMode - ? recycledBlocks.filter(({ bkt }) => bkt === 'code-bk' || recycledBlocks.length === 1) - : recycledBlocks; - }, [fromSystem, props.renderAsCodeWithTitle, props.specialDiagramMode, renderSanityTextDiffs, text]); + const isUserCommand = fromUser && props.text.startsWith('/'); + // state + const { text, isTextCollapsed, forceTextExpanded, handleToggleExpansion } = + useTextCollapser(props.text, fromUser); + let autoBlocksStable = + useAutoBlocksMemo(text, props.renderAsCodeWithTitle, fromSystem, props.renderSanityTextDiffs); +console.log(autoBlocksStable); + // apply specialDiagramMode filter if applicable + if (props.specialDiagramMode) + autoBlocksStable = autoBlocksStable.filter(({ bkt }) => bkt === 'code-bk' || autoBlocksStable.length === 1); // Memo the styles, to minimize re-renders - const scaledCodeSx = useScaledCodeSx(fromAssistant, props.contentScaling, !!props.specialCodePlain); const scaledImageSx = useScaledImageSx(props.contentScaling); const scaledTypographySx = useScaledTypographySx(props.contentScaling, !!props.showAsDanger, !!props.showAsItalic); @@ -143,10 +83,10 @@ export const AutoBlocksRenderer = React.forwardRef {/* sequence of render components, for each Block */} - {autoBlocksMemo.map((bkInput, index) => { + {autoBlocksStable.map((bkInput, index) => { // Optimization: only memo the non-currently-rendered components, if the message is still in flux - const optimizeMemoBeforeLastBlock = props.optiAllowSubBlocksMemo === true && index < (autoBlocksMemo.length - 1); + const optimizeMemoBeforeLastBlock = props.optiAllowSubBlocksMemo === true && index < (autoBlocksStable.length - 1); switch (bkInput.bkt) { @@ -212,7 +152,7 @@ export const AutoBlocksRenderer = React.forwardRef { + // nothing to do + if (!enable || forceTextExpanded) + return { text: origText, isTextCollapsed: false }; + + // count lines + const textLines = origText.split('\n'); + if (textLines.length <= USER_COLLAPSED_LINES) + return { text: origText, isTextCollapsed: false }; + + // chop to the first few lines + return { text: textLines.slice(0, USER_COLLAPSED_LINES).join('\n'), isTextCollapsed: true }; + }, [enable, forceTextExpanded, origText]); + + // memo handlers + const handleToggleExpansion = React.useCallback(() => setForceTextExpanded(on => !on), []); + + return { + text, + isTextCollapsed, + forceTextExpanded, + handleToggleExpansion, + }; +} + + +export function useAutoBlocksMemo(text: string, forceCodeWithTitle: string | undefined, forceMarkdown: boolean, forceSanityTextDiffs: SanityTextDiff[] | undefined): RenderBlockInputs { + + // state - previous blocks, to stabilize objects + const prevBlocksRef = React.useRef([]); + + return React.useMemo(() => { + + // follow outside direction, or activate the auto-splitter based on content + const newBlocks: RenderBlockInputs = []; + if (forceCodeWithTitle !== undefined) + newBlocks.push({ bkt: 'code-bk', title: forceCodeWithTitle, code: text, isPartial: false }); + else if (forceMarkdown) + newBlocks.push({ bkt: 'md-bk', content: text }); + else if (forceSanityTextDiffs && forceSanityTextDiffs.length >= 1) + newBlocks.push({ bkt: 'txt-diffs-bk', sanityTextDiffs: forceSanityTextDiffs }); + else + newBlocks.push(...parseBlocksFromText(text)); + + // recycle the previous blocks if they are the same, for stable references to React + const recycledBlocks: RenderBlockInputs = []; + for (let i = 0; i < newBlocks.length; i++) { + const newBlock = newBlocks[i]; + const prevBlock = prevBlocksRef.current[i] ?? undefined; + + // Check if the new block can be replaced by the previous block to maintain reference stability + if (prevBlock && shallowEquals(prevBlock, newBlock)) { + recycledBlocks.push(prevBlock); + } else { + // Once a block doesn't match, we use the new blocks from this point forward. + recycledBlocks.push(...newBlocks.slice(i)); + break; + } + } + + // Update prevBlocksRef with the current blocks for the next render + prevBlocksRef.current = recycledBlocks; + + return recycledBlocks; + }, [forceCodeWithTitle, forceMarkdown, forceSanityTextDiffs, text]); +} \ No newline at end of file