mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-28 00:56:26 +00:00
perf: markdown redraw
This commit is contained in:
@@ -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);
|
@@ -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 (
|
||||
|
17
client/src/components/Markdown/Link.tsx
Normal file
17
client/src/components/Markdown/Link.tsx
Normal 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);
|
@@ -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'}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/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);
|
||||
|
Reference in New Issue
Block a user