添加mermaid图表接口 (#85)
* 添加mermaid图表接口 * 添加类型文件 * Update package.json * Create next-env.d.ts
This commit is contained in:
parent
9a831b5b8b
commit
d057d20c17
5
client/next-env.d.ts
vendored
Normal file
5
client/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
@ -55,7 +55,8 @@
|
|||||||
"sass": "^1.58.3",
|
"sass": "^1.58.3",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"wxpay-v3": "^3.0.2",
|
"wxpay-v3": "^3.0.2",
|
||||||
"zustand": "^4.3.5"
|
"zustand": "^4.3.5",
|
||||||
|
"mermaid": "^8.13.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
|
|||||||
63
client/src/components/Markdown/MermaidCodeBlock.tsx
Normal file
63
client/src/components/Markdown/MermaidCodeBlock.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React, { FC, useEffect, useState, useRef } from 'react';
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
import { Spinner } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
interface MermaidCodeBlockProps {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MermaidCodeBlock: FC<MermaidCodeBlockProps> = ({ code }) => {
|
||||||
|
const [svg, setSvg] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const codeTimeoutIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (codeTimeoutIdRef.current) {
|
||||||
|
clearTimeout(codeTimeoutIdRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading ? (
|
||||||
|
<div className="loading">
|
||||||
|
<img src="/imgs/loading.gif" alt="Loading..." />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="mermaid-wrapper"
|
||||||
|
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MermaidCodeBlock;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useMemo, useEffect } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||||
@ -7,6 +7,7 @@ import Icon from '@/components/Icon';
|
|||||||
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 MermaidCodeBlock from './MermaidCodeBlock';
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
@ -29,49 +30,54 @@ const Markdown = ({
|
|||||||
|
|
||||||
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={[remarkMath]}
|
remarkPlugins={[remarkMath]}
|
||||||
rehypePlugins={[remarkGfm, rehypeKatex]}
|
rehypePlugins={[remarkGfm, rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
pre: 'div',
|
pre: "div",
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className ||'');
|
||||||
const code = String(children);
|
const code = String(children);
|
||||||
|
|
||||||
return !inline || match ? (
|
if (match && match[1] === "mermaid") {
|
||||||
<Box my={3} borderRadius={'md'} overflow={'overlay'} backgroundColor={'#222'}>
|
return <MermaidCodeBlock code={code} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !inline && match ? (
|
||||||
|
<Box my={3} borderRadius={"md"} overflow={"overlay"} backgroundColor={"#222"}>
|
||||||
<Flex
|
<Flex
|
||||||
className="code-header"
|
className="code-header"
|
||||||
py={2}
|
py={2}
|
||||||
px={5}
|
px={5}
|
||||||
backgroundColor={useColorModeValue('#323641', 'gray.600')}
|
backgroundColor={useColorModeValue("#323641", "gray.600")}
|
||||||
color={'#fff'}
|
color={"#fff"}
|
||||||
fontSize={'sm'}
|
fontSize={"sm"}
|
||||||
userSelect={'none'}
|
userSelect={"none"}
|
||||||
>
|
>
|
||||||
<Box flex={1}>{match?.[1]}</Box>
|
<Box flex={1}>{match?.[1]}</Box>
|
||||||
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
<Flex cursor={"pointer"} onClick={() => copyData(code)} alignItems={"center"}>
|
||||||
<Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
|
<Icon name={"copy"} width={15} height={15} fill={"#fff"}></Icon>
|
||||||
<Box ml={1}>复制代码</Box>
|
<Box ml={1}>复制代码</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre" {...props}>
|
||||||
style={codeLight as any}
|
|
||||||
language={match?.[1]}
|
|
||||||
PreTag="pre"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{code}
|
{code}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
{code}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
>
|
>
|
||||||
|
|||||||
19
client/src/types/mermaid.d.ts
vendored
Normal file
19
client/src/types/mermaid.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user