From 5cd74031be7df4eae53bfd37a4c5d3efc51c8fb9 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Sun, 23 Jun 2024 19:07:24 -0700 Subject: [PATCH] Blocks: rename --- .../fragments-content/ContentPartImageRef.tsx | 2 +- .../fragments-content/PartImageRefDBlob.tsx | 2 +- src/modules/blocks/BlocksRenderer.tsx | 171 ++++++++++++++---- src/modules/blocks/blocks.ts | 115 ------------ src/modules/blocks/blocks.types.ts | 15 ++ src/modules/blocks/code/RenderCode.tsx | 4 +- src/modules/blocks/{ => html}/RenderHtml.tsx | 4 +- .../blocks/{ => image}/RenderImageURL.tsx | 4 +- .../blocks/markdown/RenderMarkdown.tsx | 2 +- .../blocks/{ => text}/RenderChatText.tsx | 10 +- .../blocks/{ => textdiff}/RenderTextDiff.tsx | 6 +- 11 files changed, 168 insertions(+), 167 deletions(-) delete mode 100644 src/modules/blocks/blocks.ts create mode 100644 src/modules/blocks/blocks.types.ts rename src/modules/blocks/{ => html}/RenderHtml.tsx (97%) rename src/modules/blocks/{ => image}/RenderImageURL.tsx (98%) rename src/modules/blocks/{ => text}/RenderChatText.tsx (77%) rename src/modules/blocks/{ => textdiff}/RenderTextDiff.tsx (91%) diff --git a/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx b/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx index 787ccbebc..709e41a0c 100644 --- a/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx +++ b/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import type { SxProps } from '@mui/joy/styles/types'; import { Box } from '@mui/joy'; -import { RenderImageURL } from '~/modules/blocks/RenderImageURL'; +import { RenderImageURL } from '~/modules/blocks/image/RenderImageURL'; import { blocksRendererSx } from '~/modules/blocks/BlocksRenderer'; import type { DMessageContentFragment, DMessageFragmentId, DMessageImageRefPart } from '~/common/stores/chat/chat.fragments'; diff --git a/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx b/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx index 39b2831ca..369da5fc2 100644 --- a/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx +++ b/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx @@ -6,7 +6,7 @@ import type { SxProps } from '@mui/joy/styles/types'; import { Box } from '@mui/joy'; import type { DBlobAssetId, DBlobImageAsset } from '~/modules/dblobs/dblobs.types'; -import { RenderImageURL } from '~/modules/blocks/RenderImageURL'; +import { RenderImageURL } from '~/modules/blocks/image/RenderImageURL'; import { getImageAssetAsBlobURL } from '~/modules/dblobs/dblobs.images'; import { t2iGenerateImageContentFragments } from '~/modules/t2i/t2i.client'; import { useDBAsset } from '~/modules/dblobs/dblobs.hooks'; diff --git a/src/modules/blocks/BlocksRenderer.tsx b/src/modules/blocks/BlocksRenderer.tsx index 735c9f43b..26dca3e90 100644 --- a/src/modules/blocks/BlocksRenderer.tsx +++ b/src/modules/blocks/BlocksRenderer.tsx @@ -9,13 +9,13 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import type { DMessageRole } from '~/common/stores/chat/chat.message'; import { ContentScaling, lineHeightChatTextMd, themeScalingMap } from '~/common/app.theme'; -import { RenderChatText } from './RenderChatText'; +import type { Block, CodeBlock, HtmlBlock, ImageBlock, TextBlock } from './blocks.types'; +import { RenderChatText } from './text/RenderChatText'; import { RenderCode, RenderCodeMemo } from './code/RenderCode'; -import { RenderHtml } from './RenderHtml'; -import { RenderImageURL } from './RenderImageURL'; import { RenderMarkdown, RenderMarkdownMemo } from './markdown/RenderMarkdown'; -import { RenderTextDiff } from './RenderTextDiff'; -import { areBlocksEqual, Block, parseMessageBlocks } from './blocks'; +import { RenderTextDiff } from './textdiff/RenderTextDiff'; +import { heuristicIsBlockTextHTML, RenderHtml } from './html/RenderHtml'; +import { heuristicLegacyImageBlocks, heuristicMarkdownImageReferenceBlocks, RenderImageURL } from './image/RenderImageURL'; // How long is the user collapsed message @@ -51,6 +51,109 @@ export const blocksRendererSx: SxProps = { } as const; +function areBlocksEqual(a: Block, b: Block): boolean { + if (a.type !== b.type) + return false; + + switch (a.type) { + case 'codeb': + return a.blockTitle === (b as CodeBlock).blockTitle && a.blockCode === (b as CodeBlock).blockCode && a.complete === (b as CodeBlock).complete; + case 'diffb': + return false; // diff blocks are never equal + case 'htmlb': + return a.html === (b as HtmlBlock).html; + case 'imageb': + return a.url === (b as ImageBlock).url && a.alt === (b as ImageBlock).alt; + case 'textb': + return a.content === (b as TextBlock).content; + } +} + + +function parseBlocksFromText(text: string, disableParsing: boolean, forceTextDiffs?: TextDiff[]): Block[] { + if (disableParsing) + return [{ type: 'textb', content: text }]; + + if (forceTextDiffs && forceTextDiffs.length >= 1) + return [{ type: 'diffb', textDiffs: forceTextDiffs }]; + + // special case: this could be generated by a proxy that returns an HTML page instead of the API response + if (heuristicIsBlockTextHTML(text)) + return [{ type: 'htmlb', html: text }]; + + // special case: markdown image references (e.g. ![alt text](https://example.com/image.png)) + const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text); + if (mdImageBlocks) + return mdImageBlocks; + + // special case: legacy prodia images + const legacyImageBlocks = heuristicLegacyImageBlocks(text); + if (legacyImageBlocks) + return legacyImageBlocks; + + const regexPatterns = { + // was: \w\x20\\.+-_ for tge filename, but was missing too much + // REVERTED THIS: was: (`{3,}\n?|$), but was matching backticks within blocks. so now it must end with a newline or stop + codeBlock: /`{3,}([\S\x20]+)?\n([\s\S]*?)(`{3,}\n?|$)/g, + htmlCodeBlock: /([\s\S]*?)<\/html>/gi, + svgBlock: //g, + }; + + const blocks: Block[] = []; + let lastIndex = 0; + + while (true) { + + // find the first match (if any) trying all the regexes + let match: RegExpExecArray | null = null; + let matchType: keyof typeof regexPatterns | null = null; + for (const type in regexPatterns) { + const regex = regexPatterns[type as keyof typeof regexPatterns]; + regex.lastIndex = lastIndex; + const currentMatch = regex.exec(text); + if (currentMatch && (match === null || currentMatch.index < match.index)) { + match = currentMatch; + matchType = type as keyof typeof regexPatterns; + } + } + if (match === null) + break; + + // anything leftover before the match is text + if (match.index > lastIndex) + blocks.push({ type: 'textb', content: text.slice(lastIndex, match.index) }); + + // add the block + switch (matchType) { + case 'codeBlock': + const blockTitle: string = (match[1] || '').trim(); + const blockCode: string = match[2].trim(); + const blockEnd: string = match[3]; + blocks.push({ type: 'codeb', blockTitle, blockCode, complete: blockEnd.startsWith('```') }); + break; + + case 'htmlCodeBlock': + const html: string = `${match[1]}`; + blocks.push({ type: 'codeb', blockTitle: 'html', blockCode: html, complete: true }); + break; + + case 'svgBlock': + blocks.push({ type: 'codeb', blockTitle: 'svg', blockCode: match[0], complete: true }); + break; + } + + // advance the pointer + lastIndex = match.index + match[0].length; + } + + // remainder is text + if (lastIndex < text.length) + blocks.push({ type: 'textb', content: text.slice(lastIndex) }); + + return blocks; +} + + type BlocksRendererProps = { // required text: string; @@ -115,45 +218,39 @@ export const BlocksRenderer = React.forwardRef ( - { - my: props.specialCodePlain ? 0 : themeScalingMap[props.contentScaling]?.blockCodeMarginY ?? 0, - backgroundColor: props.specialCodePlain ? 'background.surface' : fromAssistant ? 'neutral.plainHoverBg' : 'primary.plainActiveBg', - boxShadow: props.specialCodePlain ? undefined : 'inset 2px 0px 5px -4px var(--joy-palette-background-backdrop)', // was 'xs' - borderRadius: 'sm', - fontFamily: 'code', - fontSize: themeScalingMap[props.contentScaling]?.blockCodeFontSize ?? '0.875rem', - fontWeight: 'md', // JetBrains Mono has a lighter weight, so we need that extra bump - fontVariantLigatures: 'none', - lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75, - minWidth: 260, - minHeight: '2.75rem', - } - ), [fromAssistant, props.contentScaling, props.specialCodePlain]); + const scaledCodeSx: SxProps = React.useMemo(() => ({ + my: props.specialCodePlain ? 0 : themeScalingMap[props.contentScaling]?.blockCodeMarginY ?? 0, + backgroundColor: props.specialCodePlain ? 'background.surface' : fromAssistant ? 'neutral.plainHoverBg' : 'primary.plainActiveBg', + boxShadow: props.specialCodePlain ? undefined : 'inset 2px 0px 5px -4px var(--joy-palette-background-backdrop)', // was 'xs' + borderRadius: 'sm', + fontFamily: 'code', + fontSize: themeScalingMap[props.contentScaling]?.blockCodeFontSize ?? '0.875rem', + fontWeight: 'md', // JetBrains Mono has a lighter weight, so we need that extra bump + fontVariantLigatures: 'none', + lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75, + minWidth: 260, + minHeight: '2.75rem', + }), [fromAssistant, props.contentScaling, props.specialCodePlain]); - const scaledImageSx: SxProps = React.useMemo(() => ( - { - fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined, - lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75, - marginBottom: themeScalingMap[props.contentScaling]?.blockImageGap ?? 1.5, - } - ), [props.contentScaling]); + const scaledImageSx: SxProps = React.useMemo(() => ({ + fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined, + lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75, + marginBottom: themeScalingMap[props.contentScaling]?.blockImageGap ?? 1.5, + }), [props.contentScaling]); - const scaledTypographySx: SxProps = React.useMemo(() => ( - { - fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined, - lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75, - ...(props.showAsDanger ? { color: 'danger.500', fontWeight: 500 } : {}), - ...(props.showAsItalic ? { fontStyle: 'italic' } : {}), - } - ), [props.contentScaling, props.showAsDanger, props.showAsItalic]); + const scaledTypographySx: SxProps = React.useMemo(() => ({ + fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined, + lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75, + ...(props.showAsDanger ? { color: 'danger.500', fontWeight: 500 } : {}), + ...(props.showAsItalic ? { fontStyle: 'italic' } : {}), + }), [props.contentScaling, props.showAsDanger, props.showAsItalic]); // Block splitter, with memoand special recycle of former blocks, to help React minimize render work const blocks = React.useMemo(() => { // split the complete input text into blocks - const newBlocks = parseMessageBlocks(text, fromSystem, renderTextDiff); + const newBlocks = parseBlocksFromText(text, fromSystem, renderTextDiff); // recycle the previous blocks if they are the same, for stable references to React const recycledBlocks: Block[] = []; @@ -209,7 +306,7 @@ export const BlocksRenderer = React.forwardRef : block.type === 'diffb' - ? + ? : (props.renderTextAsMarkdown && !fromSystem && !isUserCommand) ? : ; diff --git a/src/modules/blocks/blocks.ts b/src/modules/blocks/blocks.ts deleted file mode 100644 index 8f8a2a668..000000000 --- a/src/modules/blocks/blocks.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Diff as TextDiff } from '@sanity/diff-match-patch'; - -import { heuristicIsBlockTextHTML } from './RenderHtml'; -import { heuristicLegacyImageBlocks, heuristicMarkdownImageReferenceBlocks } from './RenderImageURL'; - -// Block types -export type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | TextBlock; -export type CodeBlock = { type: 'codeb'; blockTitle: string; blockCode: string; complete: boolean; }; -export type DiffBlock = { type: 'diffb'; textDiffs: TextDiff[] }; -export type HtmlBlock = { type: 'htmlb'; html: string; }; -export type ImageBlock = { type: 'imageb'; url: string; alt?: string }; // Added optional alt property -export type TextBlock = { type: 'textb'; content: string; }; // for Text or Markdown - - -export function areBlocksEqual(a: Block, b: Block): boolean { - if (a.type !== b.type) - return false; - - switch (a.type) { - case 'codeb': - return a.blockTitle === (b as CodeBlock).blockTitle && a.blockCode === (b as CodeBlock).blockCode && a.complete === (b as CodeBlock).complete; - case 'diffb': - return false; // diff blocks are never equal - case 'htmlb': - return a.html === (b as HtmlBlock).html; - case 'imageb': - return a.url === (b as ImageBlock).url && a.alt === (b as ImageBlock).alt; - case 'textb': - return a.content === (b as TextBlock).content; - } -} - - -export function parseMessageBlocks(text: string, disableParsing: boolean, forceTextDiffs?: TextDiff[]): Block[] { - if (disableParsing) - return [{ type: 'textb', content: text }]; - - if (forceTextDiffs && forceTextDiffs.length >= 1) - return [{ type: 'diffb', textDiffs: forceTextDiffs }]; - - // special case: this could be generated by a proxy that returns an HTML page instead of the API response - if (heuristicIsBlockTextHTML(text)) - return [{ type: 'htmlb', html: text }]; - - // special case: markdown image references (e.g. ![alt text](https://example.com/image.png)) - const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text); - if (mdImageBlocks) - return mdImageBlocks; - - // special case: legacy prodia images - const legacyImageBlocks = heuristicLegacyImageBlocks(text); - if (legacyImageBlocks) - return legacyImageBlocks; - - const regexPatterns = { - // was: \w\x20\\.+-_ for tge filename, but was missing too much - // REVERTED THIS: was: (`{3,}\n?|$), but was matching backticks within blocks. so now it must end with a newline or stop - codeBlock: /`{3,}([\S\x20]+)?\n([\s\S]*?)(`{3,}\n?|$)/g, - htmlCodeBlock: /([\s\S]*?)<\/html>/gi, - svgBlock: //g, - }; - - const blocks: Block[] = []; - let lastIndex = 0; - - while (true) { - - // find the first match (if any) trying all the regexes - let match: RegExpExecArray | null = null; - let matchType: keyof typeof regexPatterns | null = null; - for (const type in regexPatterns) { - const regex = regexPatterns[type as keyof typeof regexPatterns]; - regex.lastIndex = lastIndex; - const currentMatch = regex.exec(text); - if (currentMatch && (match === null || currentMatch.index < match.index)) { - match = currentMatch; - matchType = type as keyof typeof regexPatterns; - } - } - if (match === null) - break; - - // anything leftover before the match is text - if (match.index > lastIndex) - blocks.push({ type: 'textb', content: text.slice(lastIndex, match.index) }); - - // add the block - switch (matchType) { - case 'codeBlock': - const blockTitle: string = (match[1] || '').trim(); - const blockCode: string = match[2].trim(); - const blockEnd: string = match[3]; - blocks.push({ type: 'codeb', blockTitle, blockCode, complete: blockEnd.startsWith('```') }); - break; - - case 'htmlCodeBlock': - const html: string = `${match[1]}`; - blocks.push({ type: 'codeb', blockTitle: 'html', blockCode: html, complete: true }); - break; - - case 'svgBlock': - blocks.push({ type: 'codeb', blockTitle: 'svg', blockCode: match[0], complete: true }); - break; - } - - // advance the pointer - lastIndex = match.index + match[0].length; - } - - // remainder is text - if (lastIndex < text.length) - blocks.push({ type: 'textb', content: text.slice(lastIndex) }); - - return blocks; -} \ No newline at end of file diff --git a/src/modules/blocks/blocks.types.ts b/src/modules/blocks/blocks.types.ts new file mode 100644 index 000000000..8750bb52b --- /dev/null +++ b/src/modules/blocks/blocks.types.ts @@ -0,0 +1,15 @@ +import type { Diff as TextDiff } from '@sanity/diff-match-patch'; + +// Block types +export type Block = + | CodeBlock + | HtmlBlock + | ImageBlock + | TextBlock + | TextDiffBlock; + +export type CodeBlock = { type: 'codeb'; blockTitle: string; blockCode: string; complete: boolean; }; +export type HtmlBlock = { type: 'htmlb'; html: string; }; +export type ImageBlock = { type: 'imageb'; url: string; alt?: string }; // Added optional alt property +export type TextBlock = { type: 'textb'; content: string; }; // for Text or Markdown +export type TextDiffBlock = { type: 'diffb'; textDiffs: TextDiff[] }; diff --git a/src/modules/blocks/code/RenderCode.tsx b/src/modules/blocks/code/RenderCode.tsx index b6d56ea61..a0b4b2ef1 100644 --- a/src/modules/blocks/code/RenderCode.tsx +++ b/src/modules/blocks/code/RenderCode.tsx @@ -16,11 +16,11 @@ import { copyToClipboard } from '~/common/util/clipboardUtils'; import { frontendSideFetch } from '~/common/util/clientFetchers'; import { useUIPreferencesStore } from '~/common/state/store-ui'; -import type { CodeBlock } from '../blocks'; +import type { CodeBlock } from '../blocks.types'; import { ButtonCodePen, isCodePenSupported } from './ButtonCodePen'; import { ButtonJsFiddle, isJSFiddleSupported } from './ButtonJSFiddle'; import { ButtonStackBlitz, isStackBlitzSupported } from './ButtonStackBlitz'; -import { heuristicIsBlockTextHTML, IFrameComponent } from '../RenderHtml'; +import { heuristicIsBlockTextHTML, IFrameComponent } from '../html/RenderHtml'; import { patchSvgString, RenderCodeMermaid } from './RenderCodeMermaid'; diff --git a/src/modules/blocks/RenderHtml.tsx b/src/modules/blocks/html/RenderHtml.tsx similarity index 97% rename from src/modules/blocks/RenderHtml.tsx rename to src/modules/blocks/html/RenderHtml.tsx index 3638dbd66..727c5a3ef 100644 --- a/src/modules/blocks/RenderHtml.tsx +++ b/src/modules/blocks/html/RenderHtml.tsx @@ -7,8 +7,8 @@ import WebIcon from '@mui/icons-material/Web'; import { copyToClipboard } from '~/common/util/clipboardUtils'; -import type { HtmlBlock } from './blocks'; -import { OverlayButton, overlayButtonsSx } from './code/RenderCode'; +import type { HtmlBlock } from '../blocks.types'; +import { OverlayButton, overlayButtonsSx } from '../code/RenderCode'; // this is used by the blocks parser (for full text detection) and by the Code component (for inline rendering) diff --git a/src/modules/blocks/RenderImageURL.tsx b/src/modules/blocks/image/RenderImageURL.tsx similarity index 98% rename from src/modules/blocks/RenderImageURL.tsx rename to src/modules/blocks/image/RenderImageURL.tsx index 721ce17c3..46e23474f 100644 --- a/src/modules/blocks/RenderImageURL.tsx +++ b/src/modules/blocks/image/RenderImageURL.tsx @@ -13,8 +13,8 @@ import WarningRoundedIcon from '@mui/icons-material/WarningRounded'; import { GoodTooltip } from '~/common/components/GoodTooltip'; import { Link } from '~/common/components/Link'; -import type { ImageBlock } from './blocks'; -import { OverlayButton, overlayButtonsSx } from './code/RenderCode'; +import type { ImageBlock } from '../blocks.types'; +import { OverlayButton, overlayButtonsSx } from '../code/RenderCode'; const mdImageReferenceRegex = /^!\[([^\]]*)]\(([^)]+)\)$/; diff --git a/src/modules/blocks/markdown/RenderMarkdown.tsx b/src/modules/blocks/markdown/RenderMarkdown.tsx index 9125a3521..bc175c330 100644 --- a/src/modules/blocks/markdown/RenderMarkdown.tsx +++ b/src/modules/blocks/markdown/RenderMarkdown.tsx @@ -5,7 +5,7 @@ import { Box, styled } from '@mui/joy'; import { lineHeightChatTextMd } from '~/common/app.theme'; -import type { TextBlock } from '../blocks'; +import type { TextBlock } from '../blocks.types'; /* diff --git a/src/modules/blocks/RenderChatText.tsx b/src/modules/blocks/text/RenderChatText.tsx similarity index 77% rename from src/modules/blocks/RenderChatText.tsx rename to src/modules/blocks/text/RenderChatText.tsx index 9cf451a0b..f05893d43 100644 --- a/src/modules/blocks/RenderChatText.tsx +++ b/src/modules/blocks/text/RenderChatText.tsx @@ -1,13 +1,17 @@ import * as React from 'react'; +import type { SxProps } from '@mui/joy/styles/types'; import { Chip, Typography } from '@mui/joy'; -import { SxProps } from '@mui/joy/styles/types'; -import { extractChatCommand } from '../../apps/chat/commands/commands.registry'; +import { extractChatCommand } from '../../../apps/chat/commands/commands.registry'; -import type { TextBlock } from './blocks'; +import type { TextBlock } from '../blocks.types'; +/** + * Renders a text block with chat commands. + * NOTE: should remove the commands parsing dependency. + */ export const RenderChatText = (props: { textBlock: TextBlock; sx?: SxProps; }) => { const elements = extractChatCommand(props.textBlock.content); diff --git a/src/modules/blocks/RenderTextDiff.tsx b/src/modules/blocks/textdiff/RenderTextDiff.tsx similarity index 91% rename from src/modules/blocks/RenderTextDiff.tsx rename to src/modules/blocks/textdiff/RenderTextDiff.tsx index ba42aea50..b7e98a2b2 100644 --- a/src/modules/blocks/RenderTextDiff.tsx +++ b/src/modules/blocks/textdiff/RenderTextDiff.tsx @@ -4,7 +4,7 @@ import { cleanupEfficiency, Diff as TextDiff, DIFF_DELETE, DIFF_INSERT, makeDiff import type { SxProps } from '@mui/joy/styles/types'; import { Box, Typography, useTheme } from '@mui/joy'; -import type { DiffBlock } from './blocks'; +import type { TextDiffBlock } from '../blocks.types'; export function useSanityTextDiffs(_text: string, _diffText: string | undefined, enabled: boolean) { @@ -37,13 +37,13 @@ export function useSanityTextDiffs(_text: string, _diffText: string | undefined, } -export const RenderTextDiff = (props: { diffBlock: DiffBlock; sx?: SxProps; }) => { +export const RenderTextDiff = (props: { textDiffBlock: TextDiffBlock; sx?: SxProps; }) => { // external state const theme = useTheme(); // derived state - const textDiffs: TextDiff[] = props.diffBlock.textDiffs; + const textDiffs: TextDiff[] = props.textDiffBlock.textDiffs; // text added const styleAdd = {