mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Blocks: disaggro
This commit is contained in:
@@ -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. )
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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. )
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user