Blocks: rename

This commit is contained in:
Enrico Ros
2024-06-23 19:07:24 -07:00
parent facb85b5da
commit 5cd74031be
11 changed files with 168 additions and 167 deletions
@@ -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';
@@ -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';
+134 -37
View File
@@ -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: /<!DOCTYPE html>([\s\S]*?)<\/html>/gi,
svgBlock: /<svg (xmlns|width|viewBox)=([\s\S]*?)<\/svg>/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 = `<!DOCTYPE html>${match[1]}</html>`;
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<HTMLDivElement, BlocksRendererPro
// Memo the styles, to minimize re-renders
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 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<HTMLDivElement, BlocksRendererPro
onImageRegenerate={undefined /* because there could be many of these URL images in a fragment, and we miss the whole partial-edit logic in a text fragment */}
scaledImageSx={scaledImageSx} variant='content-part' />
: block.type === 'diffb'
? <RenderTextDiff key={'text-diff-' + index} diffBlock={block} sx={scaledTypographySx} />
? <RenderTextDiff key={'text-diff-' + index} textDiffBlock={block} sx={scaledTypographySx} />
: (props.renderTextAsMarkdown && !fromSystem && !isUserCommand)
? <RenderMarkdownMemoOrNot key={'text-md-' + index} textBlock={block} sx={scaledTypographySx} />
: <RenderChatText key={'text-' + index} textBlock={block} sx={scaledTypographySx} />;
-115
View File
@@ -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: /<!DOCTYPE html>([\s\S]*?)<\/html>/gi,
svgBlock: /<svg (xmlns|width|viewBox)=([\s\S]*?)<\/svg>/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 = `<!DOCTYPE html>${match[1]}</html>`;
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;
}
+15
View File
@@ -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[] };
+2 -2
View File
@@ -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';
@@ -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)
@@ -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 = /^!\[([^\]]*)]\(([^)]+)\)$/;
@@ -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';
/*
@@ -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);
@@ -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 = {