Commit 1d236f87 by archer

perf: markdown redraw

parent 3b515c3c
......@@ -35,7 +35,7 @@
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"mammoth": "^1.5.1",
"mermaid": "^8.13.5",
"mermaid": "^10.2.3",
"mongoose": "^6.10.0",
"nanoid": "^4.0.1",
"next": "13.1.6",
......@@ -49,9 +49,10 @@
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-breaks": "^3.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",
......
......@@ -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);
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 (
......
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);
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'}
......
......@@ -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;
}
}
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;
......@@ -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 && (
......
......@@ -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>
......
......@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment