perf: markdown redraw

This commit is contained in:
archer
2023-06-19 16:50:14 +08:00
parent 3b515c3c2d
commit 1d236f87ae
11 changed files with 271 additions and 458 deletions

View File

@@ -287,8 +287,7 @@ const CodeLight = ({
children,
className,
inline,
match,
...props
match
}: {
children: React.ReactNode & React.ReactNode[];
className?: string;
@@ -315,18 +314,14 @@ const CodeLight = ({
<Box ml={1}></Box>
</Flex>
</Flex>
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre" {...props}>
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre">
{String(children)}
</SyntaxHighlighter>
</Box>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
return <code className={className}>{children}</code>;
};
export default React.memo(CodeLight);

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Image, Skeleton } from '@chakra-ui/react';
const MdImage = ({ src }: { src: string }) => {
const MdImage = ({ src }: { src?: string }) => {
const [isLoading, setIsLoading] = useState(true);
const [succeed, setSucceed] = useState(false);
return (

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
const regex = /((http|https|ftp):\/\/[^\s\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]+)/gi;
const Link = ({ href }: { href?: string }) => {
const decText = decodeURIComponent(href || '');
const replaceText = decText.replace(regex, (match, p1) => {
const isInternal = /^\/#/i.test(p1);
const target = isInternal ? '_self' : '_blank';
return `<a href="${p1}" target=${target}>${p1}</a>`;
});
return <Box as={'span'} dangerouslySetInnerHTML={{ __html: replaceText }} />;
};
export default React.memo(Link);

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, memo, useCallback, useState } from 'react';
import React, { useEffect, useRef, memo, useCallback, useState, useMemo } from 'react';
import { Box } from '@chakra-ui/react';
// @ts-ignore
import mermaid from 'mermaid';
@@ -8,8 +8,11 @@ import styles from './index.module.scss';
const mermaidAPI = mermaid.mermaidAPI;
mermaidAPI.initialize({
startOnLoad: false,
startOnLoad: true,
theme: 'base',
flowchart: {
useMaxWidth: false
},
themeVariables: {
fontSize: '14px',
primaryColor: '#d6e8ff',
@@ -21,52 +24,53 @@ mermaidAPI.initialize({
}
});
const punctuationMap: Record<string, string> = {
'': ',',
'': ';',
'。': '.',
'': ':',
'': '!',
'': '?',
'“': '"',
'”': '"',
'': "'",
'': "'",
'【': '[',
'】': ']',
'': '(',
'': ')',
'《': '<',
'》': '>',
'、': ','
};
const MermaidBlock = ({ code }: { code: string }) => {
const dom = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLDivElement>(null);
const [svg, setSvg] = useState('');
const [errorSvgCode, setErrorSvgCode] = useState('');
useEffect(() => {
(async () => {
const punctuationMap: Record<string, string> = {
'': ',',
'': ';',
'。': '.',
'': ':',
'': '!',
'': '?',
'“': '"',
'”': '"',
'': "'",
'': "'",
'【': '[',
'】': ']',
'': '(',
'': ')',
'《': '<',
'》': '>',
'、': ','
};
const formatCode = code.replace(
/([,;。:!?“”‘’【】()《》、])/g,
(match) => punctuationMap[match]
);
if (!code || !ref.current) return;
try {
const svgCode = await mermaidAPI.render(`mermaid-${Date.now()}`, formatCode);
setSvg(svgCode);
} catch (error) {
setErrorSvgCode(formatCode);
console.log(error);
const formatCode = code.replace(
new RegExp(`[${Object.keys(punctuationMap).join('')}]`, 'g'),
(match) => punctuationMap[match]
);
const { svg } = await mermaidAPI.render(`mermaid-${Date.now()}`, formatCode);
setSvg(svg);
} catch (e: any) {
console.log('[Mermaid] ', e?.message);
}
})();
}, [code]);
const onclickExport = useCallback(() => {
const svg = dom.current?.children[0];
const svg = ref.current?.children[0];
if (!svg) return;
const w = svg.clientWidth * 4;
const h = svg.clientHeight * 4;
const rate = svg.clientHeight / svg.clientWidth;
const w = 3000;
const h = rate * w;
const canvas = document.createElement('canvas');
canvas.width = w;
@@ -78,7 +82,7 @@ const MermaidBlock = ({ code }: { code: string }) => {
ctx.fillRect(0, 0, w, h);
const img = new Image();
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(dom.current.innerHTML)}`;
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(ref.current.innerHTML)}`;
img.onload = () => {
ctx.drawImage(img, 0, 0, w, h);
@@ -99,14 +103,14 @@ const MermaidBlock = ({ code }: { code: string }) => {
return (
<Box position={'relative'}>
<Box
ref={dom}
as={'p'}
ref={ref}
className={styles.mermaid}
minW={'100px'}
minH={'50px'}
py={4}
dangerouslySetInnerHTML={{ __html: svg }}
/>
<MyIcon
name={'export'}
w={'20px'}

View File

@@ -319,7 +319,6 @@
border: medium none;
margin: 0;
padding: 0;
white-space: pre;
}
.markdown .highlight pre,
.markdown pre {
@@ -345,10 +344,6 @@
word-break: break-all;
}
p {
white-space: pre-line;
}
pre {
display: block;
width: 100%;
@@ -419,9 +414,4 @@
.mermaid {
overflow-x: auto;
svg {
height: auto !important;
width: auto;
}
}

View File

@@ -1,65 +1,54 @@
import React, { memo, useMemo } from 'react';
import React from 'react';
import ReactMarkdown from 'react-markdown';
import { formatLinkText } from '@/utils/tools';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import RemarkGfm from 'remark-gfm';
import RemarkMath from 'remark-math';
import RehypeKatex from 'rehype-katex';
import RemarkBreaks from 'remark-breaks';
import 'katex/dist/katex.min.css';
import styles from './index.module.scss';
import CodeLight from './codeLight';
import Loading from './Loading';
import Link from './Link';
import CodeLight from './CodeLight';
import MermaidCodeBlock from './MermaidCodeBlock';
import MdImage from './Image';
const Markdown = ({
source,
isChatting = false,
formatLink
}: {
source: string;
formatLink?: boolean;
isChatting?: boolean;
}) => {
const formatSource = useMemo(() => {
return formatLink ? formatLinkText(source) : source;
}, [source, formatLink]);
function Code({ inline, className, children }: any) {
const match = /language-(\w+)/.exec(className || '');
if (match?.[1] === 'mermaid') {
return <MermaidCodeBlock code={String(children)} />;
}
return (
<CodeLight className={className} inline={inline} match={match}>
{children}
</CodeLight>
);
}
function Image({ src }: { src?: string }) {
return <MdImage src={src} />;
}
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
return (
<ReactMarkdown
className={`markdown ${styles.markdown}
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
`}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
`}
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
rehypePlugins={[RehypeKatex]}
components={{
a: Link,
img: Image,
pre: 'div',
img({ src = '' }) {
return isChatting ? <Loading text="图片加载中..." /> : <MdImage src={src} />;
},
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
if (match?.[1] === 'mermaid') {
return isChatting ? (
<Loading text="导图加载中..." />
) : (
<MermaidCodeBlock code={String(children)} />
);
}
return (
<CodeLight className={className} inline={inline} match={match} {...props}>
{children}
</CodeLight>
);
}
code: Code
}}
linkTarget="_blank"
>
{formatSource}
{source}
</ReactMarkdown>
);
};
export default memo(Markdown);
export default Markdown;

View File

@@ -43,13 +43,13 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
import { useUserStore } from '@/store/user';
import Loading from '@/components/Loading';
import Markdown from '@/components/Markdown';
import SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import QuoteModal from './components/QuoteModal';
import { HUMAN_ICON } from '@/constants/chat';
const Markdown = dynamic(async () => await import('@/components/Markdown'));
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
ssr: false
});
@@ -736,7 +736,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
formatLink
/>
<Flex>
{!!item.systemPrompt && (

View File

@@ -659,7 +659,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
<Markdown
source={item.value}
isChatting={isChatting && index === shareChatData.history.length - 1}
formatLink
/>
</Card>
</Box>

View File

@@ -115,12 +115,6 @@ export const voiceBroadcast = ({ text }: { text: string }) => {
};
};
export const formatLinkText = (text: string) => {
const httpReg =
/(http|https|ftp):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?/gi;
return text.replace(httpReg, ` $& `);
};
export const getErrText = (err: any, def = '') => {
const msg = typeof err === 'string' ? err : err?.message || def || '';
msg && console.log('error =>', msg);