mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-27 16:33:49 +00:00
support mermaid
This commit is contained in:
1
client/src/components/Icon/icons/export.svg
Normal file
1
client/src/components/Icon/icons/export.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686468581713" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2951" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M512 640.64a42.666667 42.666667 0 0 0 42.666667-42.666667v-341.333333h130.986666a21.333333 21.333333 0 0 0 14.250667-5.461333l2.688-2.901334a21.333333 21.333333 0 0 0-4.010667-29.909333l-165.717333-126.464a32 32 0 0 0-38.912 0.042667L329.472 218.453333a21.333333 21.333333 0 0 0 12.970667 38.229334H469.333333v341.333333a42.666667 42.666667 0 0 0 42.666667 42.666667z m229.674667-298.368a42.666667 42.666667 0 0 0 4.992 85.034667H853.333333v426.666666H170.666667v-426.666666h106.666666a42.666667 42.666667 0 0 0 0-85.333334H170.666667a85.333333 85.333333 0 0 0-85.333334 85.333334v426.666666a85.333333 85.333333 0 0 0 85.333334 85.333334h682.666666a85.333333 85.333333 0 0 0 85.333334-85.333334v-426.666666a85.333333 85.333333 0 0 0-85.333334-85.333334h-106.666666z" fill="#000000" p-id="2952"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -29,7 +29,8 @@ const map = {
|
||||
appStore: require('./icons/appStore.svg').default,
|
||||
menu: require('./icons/menu.svg').default,
|
||||
edit: require('./icons/edit.svg').default,
|
||||
inform: require('./icons/inform.svg').default
|
||||
inform: require('./icons/inform.svg').default,
|
||||
export: require('./icons/export.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
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"
|
||||
>
|
||||
|
19
client/src/types/mermaid.d.ts
vendored
19
client/src/types/mermaid.d.ts
vendored
@@ -1,19 +0,0 @@
|
||||
declare module "mermaid" {
|
||||
import mermaidAPI from "mermaid";
|
||||
const mermaid: any;
|
||||
export default mermaid;
|
||||
|
||||
// 扩展 mermaidAPI
|
||||
interface MermaidAPI extends mermaidAPI.mermaidAPI {
|
||||
contentLoaded: (
|
||||
targetEl: Element,
|
||||
options?: mermaidAPI.mermaidAPI.Config
|
||||
) => void;
|
||||
}
|
||||
|
||||
const mermaidAPIInstance: MermaidAPI;
|
||||
export default mermaidAPIInstance;
|
||||
}
|
||||
type Dispatch = (action: Action) => void;
|
||||
|
||||
|
Reference in New Issue
Block a user