import type { RefObject } from 'react'; import type { HTMLPreviewHander, } from './artifacts'; import clsx from 'clsx'; import mermaid from 'mermaid'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import RehypeHighlight from 'rehype-highlight'; import RehypeKatex from 'rehype-katex'; import RemarkBreaks from 'remark-breaks'; import RemarkGfm from 'remark-gfm'; import RemarkMath from 'remark-math'; import { useDebouncedCallback } from 'use-debounce'; import ReloadButtonIcon from '../icons/reload.svg'; import LoadingIcon from '../icons/three-dots.svg'; import Locale from '../locales'; import { useChatStore } from '../store'; import { useAppConfig } from '../store/config'; import { copyToClipboard, useWindowSize } from '../utils'; import { ArtifactsShareButton, HTMLPreview, } from './artifacts'; import { IconButton } from './button'; import { FullScreen, showImageModal } from './ui-lib'; import 'katex/dist/katex.min.css'; export function Mermaid(props: { code: string }) { 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' }); showImageModal(URL.createObjectURL(blob)); } if (hasError) { return null; } return (
viewSvgInNewWindow()} > {props.code}
); } export function PreCode(props: { children: any }) { const ref = useRef(null); const previewRef = useRef(null); const [mermaidCode, setMermaidCode] = useState(''); const [htmlCode, setHtmlCode] = useState(''); const { height } = useWindowSize(); const chatStore = useChatStore(); const session = chatStore.currentSession(); const renderArtifacts = useDebouncedCallback(() => { if (!ref.current) { return; } const mermaidDom = ref.current.querySelector('code.language-mermaid'); if (mermaidDom) { setMermaidCode((mermaidDom as HTMLElement).innerText); } const htmlDom = ref.current.querySelector('code.language-html'); const refText = ref.current.querySelector('code')?.innerText; if (htmlDom) { setHtmlCode((htmlDom as HTMLElement).innerText); } else if ( refText?.startsWith(' { if (ref.current) { const codeElements = ref.current.querySelectorAll( 'code', ) as NodeListOf; const wrapLanguages = [ '', 'md', 'markdown', 'text', 'txt', 'plaintext', 'tex', 'latex', ]; codeElements.forEach((codeElement) => { const languageClass = codeElement.className.match(/language-(\w+)/); const name = languageClass ? languageClass[1] : ''; if (wrapLanguages.includes(name)) { codeElement.style.whiteSpace = 'pre-wrap'; } }); setTimeout(renderArtifacts, 1); } }, []); return ( <>
         {
            if (ref.current) {
              copyToClipboard(
                ref.current.querySelector('code')?.innerText ?? '',
              );
            }
          }}
        >
        
        {props.children}
      
{mermaidCode.length > 0 && ( )} {htmlCode.length > 0 && enableArtifacts && ( htmlCode} /> } shadow onClick={() => previewRef.current?.reload()} /> )} ); } function CustomCode(props: { children: any; className?: string }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); const config = useAppConfig(); const enableCodeFold = session.mask?.enableCodeFold !== false && config.enableCodeFold; const ref = useRef(null); const [collapsed, setCollapsed] = useState(true); const [showToggle, setShowToggle] = useState(false); 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 && enableCodeFold && collapsed) { return (
); } return null; }; return ( <> {props.children} {renderShowMoreButton()} ); } function escapeBrackets(text: string) { 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: string) { // try add wrap html code (fixed: html codeblock include 2 newline) // ignore embed codeblock 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>)(\s*)(<\/html>)([\n\r]*)(`*)([\n\r]*?)/g, (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => { return !quoteEnd ? `${bodyEnd + space + htmlEnd}\n\`\`\`\n` : match; }, ); } function _MarkDownContent(props: { content: string }) { const escapedContent = useMemo(() => { return tryWrapHtmlCode(escapeBrackets(props.content)); }, [props.content]); 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 = /^\/#/.test(href); const target = isInternal ? '_self' : aProps.target ?? '_blank'; return ; }, }} > {escapedContent}
); } export const MarkdownContent = React.memo(_MarkDownContent); export function Markdown( props: { content: string; loading?: boolean; fontSize?: number; fontFamily?: string; parentRef?: RefObject; defaultShow?: boolean; } & React.DOMAttributes, ) { const mdRef = useRef(null); return (
{props.loading ? ( ) : ( )}
); }