import React, { useCallback, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import 'katex/dist/katex.min.css'; import RemarkMath from 'remark-math'; // Math syntax import RemarkBreaks from 'remark-breaks'; // Line break import RehypeKatex from 'rehype-katex'; // Math render import RemarkGfm from 'remark-gfm'; // Special markdown syntax import RehypeExternalLinks from 'rehype-external-links'; import styles from './index.module.scss'; import dynamic from 'next/dynamic'; import { Link, Button, Box } from '@chakra-ui/react'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { useTranslation } from 'next-i18next'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { MARKDOWN_QUOTE_SIGN } from '@fastgpt/global/core/chat/constants'; import { CodeClassNameEnum } from './utils'; const CodeLight = dynamic(() => import('./CodeLight'), { ssr: false }); const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false }); const MdImage = dynamic(() => import('./img/Image'), { ssr: false }); const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false }); const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false }); const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { ssr: false }); const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false }); const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false }); type Props = { source?: string; showAnimation?: boolean; isDisabled?: boolean; forbidZhFormat?: boolean; }; const Markdown = (props: Props) => { const source = props.source || ''; if (source.length < 200000) { return ; } return {source}; }; const MarkdownRender = ({ source = '', showAnimation, isDisabled, forbidZhFormat }: Props) => { const components = useMemo( () => ({ img: Image, pre: RewritePre, code: Code, a: A }), [] ); const formatSource = useMemo(() => { if (showAnimation || forbidZhFormat) return source; // 保护 URL 格式:https://, http://, /api/xxx const urlPlaceholders: string[] = []; const textWithProtectedUrls = source.replace( /(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|\/api\/[^\s]+)(?=\s|$)/g, (match) => { urlPlaceholders.push(match); return `__URL_${urlPlaceholders.length - 1}__`; } ); // 处理中文与英文数字之间的分词 const textWithSpaces = textWithProtectedUrls .replace( /([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g, '$1$3 $2$4' ) // 处理引用标记 .replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1'); // 还原 URL const finalText = textWithSpaces.replace( /__URL_(\d+)__/g, (_, index) => urlPlaceholders[parseInt(index)] ); return finalText; }, [forbidZhFormat, showAnimation, source]); const urlTransform = useCallback((val: string) => { return val; }, []); return ( {formatSource} {isDisabled && } ); }; export default React.memo(Markdown); /* Custom dom */ function Code(e: any) { const { className, codeBlock, children } = e; const match = /language-(\w+)/.exec(className || ''); const codeType = match?.[1]; const strChildren = String(children); const Component = useMemo(() => { if (codeType === CodeClassNameEnum.mermaid) { return ; } if (codeType === CodeClassNameEnum.guide) { return ; } if (codeType === CodeClassNameEnum.questionGuide) { return ; } if (codeType === CodeClassNameEnum.echarts) { return ; } if (codeType === CodeClassNameEnum.iframe) { return ; } if (codeType && codeType.toLowerCase() === CodeClassNameEnum.html) { return ( {children} ); } return ( {children} ); }, [codeType, className, codeBlock, match, children, strChildren]); return Component; } function Image({ src }: { src?: string }) { return ; } function A({ children, ...props }: any) { const { t } = useTranslation(); // empty href link if (!props.href && typeof children?.[0] === 'string') { const text = useMemo(() => String(children), [children]); return ( ); } // quote link(未使用) if (children?.length === 1 && typeof children?.[0] === 'string') { const text = String(children); if (text === MARKDOWN_QUOTE_SIGN && props.href) { return ( getCollectionSourceAndOpen(props.href)} /> ); } } return {children}; } function RewritePre({ children }: any) { const modifiedChildren = React.Children.map(children, (child) => { if (React.isValidElement(child)) { // @ts-ignore return React.cloneElement(child, { codeBlock: true }); } return child; }); return <>{modifiedChildren}; }