Blocks: disaggro

This commit is contained in:
Enrico Ros
2024-08-03 18:52:11 -07:00
parent 1e5bb5aa7e
commit b18a3f0fd9
3 changed files with 120 additions and 111 deletions
+5 -111
View File
@@ -1,23 +1,20 @@
import * as React from 'react';
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
import type { SxProps } from '@mui/joy/styles/types';
import { Button, ColorPaletteProp } from '@mui/joy';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import type { DMessageRole } from '~/common/stores/chat/chat.message';
import { ContentScaling } from '~/common/app.theme';
import type { Block, CodeBlock, HtmlBlock, ImageBlock, TextBlock } from './blocks.types';
import { BlocksContainer } from './BlocksContainers';
import { RenderHtmlResponse } from './html/RenderHtmlResponse';
import { RenderImageURL } from './image/RenderImageURL';
import { RenderMarkdown, RenderMarkdownMemo } from './markdown/RenderMarkdown';
import { RenderPlainChatText } from './plaintext/RenderPlainChatText';
import { RenderTextDiff } from './textdiff/RenderTextDiff';
import { heuristicIsBlockPureHTML, RenderHtmlResponse } from './html/RenderHtmlResponse';
import { heuristicLegacyImageBlocks, heuristicMarkdownImageReferenceBlocks, RenderImageURL } from './image/RenderImageURL';
import { ToggleExpansionButton } from './ToggleExpansionButton';
import { parseBlocksFromText } from './blocks.textparser';
import { renderCodeMemoOrNot } from './code/RenderCode';
import { useScaledCodeSx, useScaledImageSx, useScaledTypographySx, useToggleExpansionButtonSx } from '~/modules/blocks/blocks.styles';
import { useScaledCodeSx, useScaledImageSx, useScaledTypographySx, useToggleExpansionButtonSx } from './blocks.styles';
// How long is the user collapsed message
@@ -43,86 +40,6 @@ function areBlocksEqual(a: Block, b: Block): boolean {
}
function parseBlocksFromText(text: string): Block[] {
// special case: this could be generated by a proxy that returns an HTML page instead of the API response
if (heuristicIsBlockPureHTML(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();
// note: we don't trim blockCode to preserve leading spaces, however if the last line is only made of spaces, we trim that
const blockCode: string = match[2].replace(/\s+$/, '');
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;
@@ -151,29 +68,6 @@ type BlocksRendererProps = {
};
function ToggleExpansionButton(props: {
color?: ColorPaletteProp;
isCollapsed: boolean;
onToggle: () => void;
sx: SxProps;
}) {
return (
<div style={{ lineHeight: 1 /* Absorbs some weird height issue since the parent has an extended line height (lineHeightChatTextMd) */ }}>
<Button
variant='soft'
color={props.color}
size='sm'
onClick={props.onToggle}
startDecorator={props.isCollapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />}
sx={props.sx}
>
{props.isCollapsed ? 'Show more' : 'Show less'}
</Button>
</div>
);
}
/**
* Features: collpase/expand, auto-detects HTML, SVG, Code, etc..
* Used by (and more):
@@ -0,0 +1,32 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Button, ColorPaletteProp } from '@mui/joy';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
/**
* Simple button to 'Show more' or 'Show less' content
*/
export function ToggleExpansionButton(props: {
color?: ColorPaletteProp;
isCollapsed: boolean;
onToggle: () => void;
sx: SxProps;
}) {
return (
<div style={{ lineHeight: 1 /* Absorbs some weird height issue since the parent has an extended line height (lineHeightChatTextMd) */ }}>
<Button
variant='soft'
color={props.color}
size='sm'
onClick={props.onToggle}
startDecorator={props.isCollapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />}
sx={props.sx}
>
{props.isCollapsed ? 'Show more' : 'Show less'}
</Button>
</div>
);
}
+83
View File
@@ -0,0 +1,83 @@
import type { Block } from './blocks.types';
import { heuristicIsBlockPureHTML } from './html/RenderHtmlResponse';
import { heuristicLegacyImageBlocks, heuristicMarkdownImageReferenceBlocks } from './image/RenderImageURL';
export function parseBlocksFromText(text: string): Block[] {
// special case: this could be generated by a proxy that returns an HTML page instead of the API response
if (heuristicIsBlockPureHTML(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();
// note: we don't trim blockCode to preserve leading spaces, however if the last line is only made of spaces, we trim that
const blockCode: string = match[2].replace(/\s+$/, '');
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;
}