Commit 1d236f87 by archer

perf: markdown redraw

parent 3b515c3c
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mammoth": "^1.5.1", "mammoth": "^1.5.1",
"mermaid": "^8.13.5", "mermaid": "^10.2.3",
"mongoose": "^6.10.0", "mongoose": "^6.10.0",
"nanoid": "^4.0.1", "nanoid": "^4.0.1",
"next": "13.1.6", "next": "13.1.6",
...@@ -49,9 +49,10 @@ ...@@ -49,9 +49,10 @@
"react-day-picker": "^8.7.1", "react-day-picker": "^8.7.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.1", "react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2", "rehype-katex": "^6.0.2",
"remark-breaks": "^3.0.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
......
...@@ -287,8 +287,7 @@ const CodeLight = ({ ...@@ -287,8 +287,7 @@ const CodeLight = ({
children, children,
className, className,
inline, inline,
match, match
...props
}: { }: {
children: React.ReactNode & React.ReactNode[]; children: React.ReactNode & React.ReactNode[];
className?: string; className?: string;
...@@ -315,18 +314,14 @@ const CodeLight = ({ ...@@ -315,18 +314,14 @@ const CodeLight = ({
<Box ml={1}>复制</Box> <Box ml={1}>复制</Box>
</Flex> </Flex>
</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)} {String(children)}
</SyntaxHighlighter> </SyntaxHighlighter>
</Box> </Box>
); );
} }
return ( return <code className={className}>{children}</code>;
<code className={className} {...props}>
{children}
</code>
);
}; };
export default React.memo(CodeLight); export default React.memo(CodeLight);
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Image, Skeleton } from '@chakra-ui/react'; import { Image, Skeleton } from '@chakra-ui/react';
const MdImage = ({ src }: { src: string }) => { const MdImage = ({ src }: { src?: string }) => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [succeed, setSucceed] = useState(false); const [succeed, setSucceed] = useState(false);
return ( 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'; import { Box } from '@chakra-ui/react';
// @ts-ignore // @ts-ignore
import mermaid from 'mermaid'; import mermaid from 'mermaid';
...@@ -8,8 +8,11 @@ import styles from './index.module.scss'; ...@@ -8,8 +8,11 @@ import styles from './index.module.scss';
const mermaidAPI = mermaid.mermaidAPI; const mermaidAPI = mermaid.mermaidAPI;
mermaidAPI.initialize({ mermaidAPI.initialize({
startOnLoad: false, startOnLoad: true,
theme: 'base', theme: 'base',
flowchart: {
useMaxWidth: false
},
themeVariables: { themeVariables: {
fontSize: '14px', fontSize: '14px',
primaryColor: '#d6e8ff', primaryColor: '#d6e8ff',
...@@ -21,52 +24,53 @@ mermaidAPI.initialize({ ...@@ -21,52 +24,53 @@ mermaidAPI.initialize({
} }
}); });
const punctuationMap: Record<string, string> = {
',': ',',
';': ';',
'。': '.',
':': ':',
'!': '!',
'?': '?',
'“': '"',
'”': '"',
'‘': "'",
'’': "'",
'【': '[',
'】': ']',
'(': '(',
')': ')',
'《': '<',
'》': '>',
'、': ','
};
const MermaidBlock = ({ code }: { code: string }) => { const MermaidBlock = ({ code }: { code: string }) => {
const dom = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [svg, setSvg] = useState(''); const [svg, setSvg] = useState('');
const [errorSvgCode, setErrorSvgCode] = useState('');
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const punctuationMap: Record<string, string> = { if (!code || !ref.current) return;
',': ',',
';': ';',
'。': '.',
':': ':',
'!': '!',
'?': '?',
'“': '"',
'”': '"',
'‘': "'",
'’': "'",
'【': '[',
'】': ']',
'(': '(',
')': ')',
'《': '<',
'》': '>',
'、': ','
};
const formatCode = code.replace(
/([,;。:!?“”‘’【】()《》、])/g,
(match) => punctuationMap[match]
);
try { try {
const svgCode = await mermaidAPI.render(`mermaid-${Date.now()}`, formatCode); const formatCode = code.replace(
setSvg(svgCode); new RegExp(`[${Object.keys(punctuationMap).join('')}]`, 'g'),
} catch (error) { (match) => punctuationMap[match]
setErrorSvgCode(formatCode); );
console.log(error); const { svg } = await mermaidAPI.render(`mermaid-${Date.now()}`, formatCode);
setSvg(svg);
} catch (e: any) {
console.log('[Mermaid] ', e?.message);
} }
})(); })();
}, [code]); }, [code]);
const onclickExport = useCallback(() => { const onclickExport = useCallback(() => {
const svg = dom.current?.children[0]; const svg = ref.current?.children[0];
if (!svg) return; if (!svg) return;
const w = svg.clientWidth * 4; const rate = svg.clientHeight / svg.clientWidth;
const h = svg.clientHeight * 4; const w = 3000;
const h = rate * w;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = w; canvas.width = w;
...@@ -78,7 +82,7 @@ const MermaidBlock = ({ code }: { code: string }) => { ...@@ -78,7 +82,7 @@ const MermaidBlock = ({ code }: { code: string }) => {
ctx.fillRect(0, 0, w, h); ctx.fillRect(0, 0, w, h);
const img = new Image(); 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 = () => { img.onload = () => {
ctx.drawImage(img, 0, 0, w, h); ctx.drawImage(img, 0, 0, w, h);
...@@ -99,14 +103,14 @@ const MermaidBlock = ({ code }: { code: string }) => { ...@@ -99,14 +103,14 @@ const MermaidBlock = ({ code }: { code: string }) => {
return ( return (
<Box position={'relative'}> <Box position={'relative'}>
<Box <Box
ref={dom} ref={ref}
as={'p'}
className={styles.mermaid} className={styles.mermaid}
minW={'100px'} minW={'100px'}
minH={'50px'} minH={'50px'}
py={4} py={4}
dangerouslySetInnerHTML={{ __html: svg }} dangerouslySetInnerHTML={{ __html: svg }}
/> />
<MyIcon <MyIcon
name={'export'} name={'export'}
w={'20px'} w={'20px'}
......
...@@ -319,7 +319,6 @@ ...@@ -319,7 +319,6 @@
border: medium none; border: medium none;
margin: 0; margin: 0;
padding: 0; padding: 0;
white-space: pre;
} }
.markdown .highlight pre, .markdown .highlight pre,
.markdown pre { .markdown pre {
...@@ -345,10 +344,6 @@ ...@@ -345,10 +344,6 @@
word-break: break-all; word-break: break-all;
} }
p {
white-space: pre-line;
}
pre { pre {
display: block; display: block;
width: 100%; width: 100%;
...@@ -419,9 +414,4 @@ ...@@ -419,9 +414,4 @@
.mermaid { .mermaid {
overflow-x: auto; 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 ReactMarkdown from 'react-markdown';
import { formatLinkText } from '@/utils/tools'; import RemarkGfm from 'remark-gfm';
import remarkGfm from 'remark-gfm'; import RemarkMath from 'remark-math';
import remarkMath from 'remark-math'; import RehypeKatex from 'rehype-katex';
import rehypeKatex from 'rehype-katex'; import RemarkBreaks from 'remark-breaks';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import styles from './index.module.scss'; 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 MermaidCodeBlock from './MermaidCodeBlock';
import MdImage from './Image'; import MdImage from './Image';
const Markdown = ({ function Code({ inline, className, children }: any) {
source, const match = /language-(\w+)/.exec(className || '');
isChatting = false,
formatLink if (match?.[1] === 'mermaid') {
}: { return <MermaidCodeBlock code={String(children)} />;
source: string; }
formatLink?: boolean;
isChatting?: boolean; return (
}) => { <CodeLight className={className} inline={inline} match={match}>
const formatSource = useMemo(() => { {children}
return formatLink ? formatLinkText(source) : source; </CodeLight>
}, [source, formatLink]); );
}
function Image({ src }: { src?: string }) {
return <MdImage src={src} />;
}
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
return ( return (
<ReactMarkdown <ReactMarkdown
className={`markdown ${styles.markdown} className={`markdown ${styles.markdown}
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''} ${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
`} `}
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
rehypePlugins={[rehypeKatex]} rehypePlugins={[RehypeKatex]}
components={{ components={{
a: Link,
img: Image,
pre: 'div', pre: 'div',
img({ src = '' }) { code: Code
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>
);
}
}} }}
linkTarget="_blank"
> >
{formatSource} {source}
</ReactMarkdown> </ReactMarkdown>
); );
}; };
export default memo(Markdown); export default Markdown;
...@@ -43,13 +43,13 @@ import { fileDownload } from '@/utils/file'; ...@@ -43,13 +43,13 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common'; import { htmlTemplate } from '@/constants/common';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import Loading from '@/components/Loading'; import Loading from '@/components/Loading';
import Markdown from '@/components/Markdown';
import SideBar from '@/components/SideBar'; import SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
import Empty from './components/Empty'; import Empty from './components/Empty';
import QuoteModal from './components/QuoteModal'; import QuoteModal from './components/QuoteModal';
import { HUMAN_ICON } from '@/constants/chat'; import { HUMAN_ICON } from '@/constants/chat';
const Markdown = dynamic(async () => await import('@/components/Markdown'));
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), { const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
ssr: false ssr: false
}); });
...@@ -736,7 +736,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { ...@@ -736,7 +736,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
<Markdown <Markdown
source={item.value} source={item.value}
isChatting={isChatting && index === chatData.history.length - 1} isChatting={isChatting && index === chatData.history.length - 1}
formatLink
/> />
<Flex> <Flex>
{!!item.systemPrompt && ( {!!item.systemPrompt && (
......
...@@ -659,7 +659,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) => ...@@ -659,7 +659,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
<Markdown <Markdown
source={item.value} source={item.value}
isChatting={isChatting && index === shareChatData.history.length - 1} isChatting={isChatting && index === shareChatData.history.length - 1}
formatLink
/> />
</Card> </Card>
</Box> </Box>
......
...@@ -115,12 +115,6 @@ export const voiceBroadcast = ({ text }: { text: string }) => { ...@@ -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 = '') => { export const getErrText = (err: any, def = '') => {
const msg = typeof err === 'string' ? err : err?.message || def || ''; const msg = typeof err === 'string' ? err : err?.message || def || '';
msg && console.log('error =>', msg); 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