mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 01:40:51 +00:00
support mermaid
This commit is contained in:
17
client/src/components/Markdown/Loading.tsx
Normal file
17
client/src/components/Markdown/Loading.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<Box
|
||||
w={'100%'}
|
||||
h={'80px'}
|
||||
backgroundImage={'url("/imgs/loading.gif")'}
|
||||
backgroundSize={'contain'}
|
||||
backgroundRepeat={'no-repeat'}
|
||||
backgroundPosition={'center'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Loading);
|
@@ -1,63 +1,99 @@
|
||||
import React, { FC, useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useRef, memo, useCallback, useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
// @ts-ignore
|
||||
import mermaid from 'mermaid';
|
||||
import { Spinner } from '@chakra-ui/react';
|
||||
import MyIcon from '../Icon';
|
||||
|
||||
interface MermaidCodeBlockProps {
|
||||
code: string;
|
||||
}
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const MermaidCodeBlock: FC<MermaidCodeBlockProps> = ({ code }) => {
|
||||
const [svg, setSvg] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const codeTimeoutIdRef = useRef<number | null>(null);
|
||||
const mermaidAPI = mermaid.mermaidAPI;
|
||||
mermaidAPI.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
fontSize: '14px',
|
||||
primaryColor: '#d6e8ff',
|
||||
primaryTextColor: '#485058',
|
||||
primaryBorderColor: '#fff',
|
||||
lineColor: '#5A646E',
|
||||
secondaryColor: '#B5E9E5',
|
||||
tertiaryColor: '#485058'
|
||||
}
|
||||
});
|
||||
|
||||
const MermaidBlock = ({ code }: { code: string }) => {
|
||||
const dom = useRef<HTMLDivElement>(null);
|
||||
const [svg, setSvg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (codeTimeoutIdRef.current) {
|
||||
clearTimeout(codeTimeoutIdRef.current);
|
||||
try {
|
||||
mermaidAPI.render('mermaid-svg', code, (svgCode: string) => {
|
||||
setSvg(svgCode);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
codeTimeoutIdRef.current = window.setTimeout(() => {
|
||||
setLoading(true);
|
||||
|
||||
const mermaidAPI = (mermaid as any).mermaidAPI as any;
|
||||
mermaidAPI.initialize({ startOnLoad: false, theme: 'forest' });
|
||||
|
||||
try {
|
||||
mermaidAPI.parse(code);
|
||||
mermaidAPI.render('mermaid-svg', code, (svgCode: string) => {
|
||||
setSvg(svgCode);
|
||||
setLoading(false);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing Mermaid code:', '\n', error, '\n', 'Code:', code);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}, 1000);
|
||||
}, [code]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (codeTimeoutIdRef.current) {
|
||||
clearTimeout(codeTimeoutIdRef.current);
|
||||
}
|
||||
const onclickExport = useCallback(() => {
|
||||
const svg = dom.current?.children[0];
|
||||
if (!svg) return;
|
||||
|
||||
const w = svg.clientWidth * 4;
|
||||
const h = svg.clientHeight * 4;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
// 绘制白色背景
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
const img = new Image();
|
||||
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(dom.current.innerHTML)}`;
|
||||
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
const jpgDataUrl = canvas.toDataURL('image/jpeg', 1);
|
||||
const a = document.createElement('a');
|
||||
a.href = jpgDataUrl;
|
||||
a.download = 'mermaid.jpg';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
img.onerror = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<div className="loading">
|
||||
<img src="/imgs/loading.gif" alt="Loading..." />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="mermaid-wrapper"
|
||||
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<Box position={'relative'}>
|
||||
<Box
|
||||
ref={dom}
|
||||
className={styles.mermaid}
|
||||
minH={'50px'}
|
||||
py={4}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'export'}
|
||||
w={'20px'}
|
||||
position={'absolute'}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
color: 'myBlue.700'
|
||||
}}
|
||||
right={0}
|
||||
top={0}
|
||||
cursor={'pointer'}
|
||||
onClick={onclickExport}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MermaidCodeBlock;
|
||||
export default memo(MermaidBlock);
|
||||
|
@@ -1,5 +1,10 @@
|
||||
import React from 'react';
|
||||
export const codeLight: { [key: string]: React.CSSProperties } = {
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import Icon from '@/components/Icon';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
|
||||
const codeLight: { [key: string]: React.CSSProperties } = {
|
||||
'code[class*=language-]': {
|
||||
color: '#d4d4d4',
|
||||
textShadow: 'none',
|
||||
@@ -277,3 +282,51 @@ export const codeLight: { [key: string]: React.CSSProperties } = {
|
||||
zIndex: '0'
|
||||
}
|
||||
};
|
||||
|
||||
const CodeLight = ({
|
||||
children,
|
||||
className,
|
||||
inline,
|
||||
match,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode & React.ReactNode[];
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
match: RegExpExecArray | null;
|
||||
}) => {
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<Box my={3} borderRadius={'md'} overflow={'overlay'} backgroundColor={'#222'}>
|
||||
<Flex
|
||||
className="code-header"
|
||||
py={2}
|
||||
px={5}
|
||||
backgroundColor={useColorModeValue('#323641', 'gray.600')}
|
||||
color={'#fff'}
|
||||
fontSize={'sm'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box flex={1}>{match?.[1]}</Box>
|
||||
<Flex cursor={'pointer'} onClick={() => copyData(String(children))} alignItems={'center'}>
|
||||
<Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
|
||||
<Box ml={1}>复制</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre" {...props}>
|
||||
{String(children)}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeLight);
|
@@ -416,3 +416,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
overflow-x: auto;
|
||||
|
||||
svg {
|
||||
height: auto !important;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,16 @@
|
||||
import React, { memo, useMemo, useEffect } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useCopyData, formatLinkText } from '@/utils/tools';
|
||||
import Icon from '@/components/Icon';
|
||||
import { formatLinkText } from '@/utils/tools';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import MermaidCodeBlock from './MermaidCodeBlock';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from './index.module.scss';
|
||||
import { codeLight } from './codeLight';
|
||||
import CodeLight from './codeLight';
|
||||
import Loading from './Loading';
|
||||
import MermaidCodeBlock from './MermaidCodeBlock';
|
||||
|
||||
const Markdown = ({
|
||||
source,
|
||||
@@ -22,8 +21,6 @@ const Markdown = ({
|
||||
formatLink?: boolean;
|
||||
isChatting?: boolean;
|
||||
}) => {
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const formatSource = useMemo(() => {
|
||||
return formatLink ? formatLinkText(source) : source;
|
||||
}, [source, formatLink]);
|
||||
@@ -31,53 +28,25 @@ const Markdown = ({
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}
|
||||
${
|
||||
isChatting
|
||||
? source === ""
|
||||
? styles.waitingAnimation
|
||||
: styles.animation
|
||||
: ""
|
||||
}
|
||||
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
|
||||
`}
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[remarkGfm, rehypeKatex]}
|
||||
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
pre: "div",
|
||||
pre: 'div',
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className ||'');
|
||||
const code = String(children);
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
|
||||
if (match && match[1] === "mermaid") {
|
||||
return <MermaidCodeBlock code={code} />;
|
||||
if (match?.[1] === 'mermaid') {
|
||||
return isChatting ? <Loading /> : <MermaidCodeBlock code={String(children)} />;
|
||||
}
|
||||
|
||||
return !inline && match ? (
|
||||
<Box my={3} borderRadius={"md"} overflow={"overlay"} backgroundColor={"#222"}>
|
||||
<Flex
|
||||
className="code-header"
|
||||
py={2}
|
||||
px={5}
|
||||
backgroundColor={useColorModeValue("#323641", "gray.600")}
|
||||
color={"#fff"}
|
||||
fontSize={"sm"}
|
||||
userSelect={"none"}
|
||||
>
|
||||
<Box flex={1}>{match?.[1]}</Box>
|
||||
<Flex cursor={"pointer"} onClick={() => copyData(code)} alignItems={"center"}>
|
||||
<Icon name={"copy"} width={15} height={15} fill={"#fff"}></Icon>
|
||||
<Box ml={1}>复制代码</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre" {...props}>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
return (
|
||||
<CodeLight className={className} inline={inline} match={match} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
</CodeLight>
|
||||
);
|
||||
},
|
||||
}
|
||||
}}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
|
Reference in New Issue
Block a user