/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import ReactMarkdown from 'react-markdown'; import 'katex/dist/katex.min.css'; import 'highlight.js/styles/github.css'; import './markdown.css'; import RemarkMath from 'remark-math'; import RemarkBreaks from 'remark-breaks'; import RehypeKatex from 'rehype-katex'; import RemarkGfm from 'remark-gfm'; import RehypeHighlight from 'rehype-highlight'; import { useRef, useState, useEffect, useMemo } from 'react'; import mermaid from 'mermaid'; import React from 'react'; import { useDebouncedCallback } from 'use-debounce'; import clsx from 'clsx'; import { Button, Tooltip, Toast } from '@douyinfe/semi-ui'; import { copy, rehypeSplitWordsIntoSpans } from '../../../helpers'; import { IconCopy } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose', }); export function Mermaid(props) { const ref = useRef(null); const [hasError, setHasError] = useState(false); useEffect(() => { if (props.code && ref.current) { mermaid .run({ nodes: [ref.current], suppressErrors: true, }) .catch((e) => { setHasError(true); console.error('[Mermaid] ', e.message); }); } }, [props.code]); function viewSvgInNewWindow() { const svg = ref.current?.querySelector('svg'); if (!svg) return; const text = new XMLSerializer().serializeToString(svg); const blob = new Blob([text], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); } if (hasError) { return null; } return (
viewSvgInNewWindow()} > {props.code}
); } export function PreCode(props) { const ref = useRef(null); const [mermaidCode, setMermaidCode] = useState(''); const [htmlCode, setHtmlCode] = useState(''); const { t } = useTranslation(); const renderArtifacts = useDebouncedCallback(() => { if (!ref.current) return; const mermaidDom = ref.current.querySelector('code.language-mermaid'); if (mermaidDom) { setMermaidCode(mermaidDom.innerText); } const htmlDom = ref.current.querySelector('code.language-html'); const refText = ref.current.querySelector('code')?.innerText; if (htmlDom) { setHtmlCode(htmlDom.innerText); } else if ( refText?.startsWith(' { if (ref.current) { const codeElements = ref.current.querySelectorAll('code'); const wrapLanguages = [ '', 'md', 'markdown', 'text', 'txt', 'plaintext', 'tex', 'latex', ]; codeElements.forEach((codeElement) => { let languageClass = codeElement.className.match(/language-(\w+)/); let name = languageClass ? languageClass[1] : ''; if (wrapLanguages.includes(name)) { codeElement.style.whiteSpace = 'pre-wrap'; } }); setTimeout(renderArtifacts, 1); } }, []); return ( <>
        
{props.children}
{mermaidCode.length > 0 && ( )} {htmlCode.length > 0 && (
HTML预览:
)} ); } function CustomCode(props) { const ref = useRef(null); const [collapsed, setCollapsed] = useState(true); const [showToggle, setShowToggle] = useState(false); const { t } = useTranslation(); useEffect(() => { if (ref.current) { const codeHeight = ref.current.scrollHeight; setShowToggle(codeHeight > 400); ref.current.scrollTop = ref.current.scrollHeight; } }, [props.children]); const toggleCollapsed = () => { setCollapsed((collapsed) => !collapsed); }; const renderShowMoreButton = () => { if (showToggle && collapsed) { return (
); } return null; }; return (
{props.children} {renderShowMoreButton()}
); } function escapeBrackets(text) { const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g; return text.replace( pattern, (match, codeBlock, squareBracket, roundBracket) => { if (codeBlock) { return codeBlock; } else if (squareBracket) { return `$$${squareBracket}$$`; } else if (roundBracket) { return `$${roundBracket}$`; } return match; }, ); } function tryWrapHtmlCode(text) { // 尝试包装HTML代码 if (text.includes('```')) { return text; } return text .replace( /([`]*?)(\w*?)([\n\r]*?)()/g, (match, quoteStart, lang, newLine, doctype) => { return !quoteStart ? '\n```html\n' + doctype : match; }, ) .replace( /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g, (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => { return !quoteEnd ? bodyEnd + space + htmlEnd + '\n```\n' : match; }, ); } function _MarkdownContent(props) { const { content, className, animated = false, previousContentLength = 0, } = props; const escapedContent = useMemo(() => { return tryWrapHtmlCode(escapeBrackets(content)); }, [content]); // 判断是否为用户消息 const isUserMessage = className && className.includes('user-message'); const rehypePluginsBase = useMemo(() => { const base = [ RehypeKatex, [ RehypeHighlight, { detect: false, ignoreMissing: true, }, ], ]; if (animated) { base.push([rehypeSplitWordsIntoSpans, { previousContentLength }]); } return base; }, [animated, previousContentLength]); return ( (

), a: (aProps) => { const href = aProps.href || ''; if (/\.(aac|mp3|opus|wav)$/.test(href)) { return (

); } if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) { return ( ); } const isInternal = /^\/#/i.test(href); const target = isInternal ? '_self' : (aProps.target ?? '_blank'); return ( { e.target.style.textDecoration = 'underline'; }} onMouseLeave={(e) => { e.target.style.textDecoration = 'none'; }} /> ); }, h1: (props) => (

), h2: (props) => (

), h3: (props) => (

), h4: (props) => (

), h5: (props) => (

), h6: (props) => (
), blockquote: (props) => (
), ul: (props) => (
    ), ol: (props) => (
      ), li: (props) => (
    1. ), table: (props) => (
      ), th: (props) => (
      ), td: (props) => ( ), }} > {escapedContent} ); } export const MarkdownContent = React.memo(_MarkdownContent); export function MarkdownRenderer(props) { const { content, loading, fontSize = 14, fontFamily = 'inherit', className, style, animated = false, previousContentLength = 0, ...otherProps } = props; return (
      {loading ? (
      正在渲染...
      ) : ( )}
      ); } export default MarkdownRenderer;