import React, { useMemo, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useTranslation } from 'react-i18next'; import { normalizeInlineCodeFences } from '../../utils/chatFormatting'; type MarkdownProps = { children: React.ReactNode; className?: string; }; type CodeBlockProps = { node?: any; inline?: boolean; className?: string; children?: React.ReactNode; }; const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockProps) => { const { t } = useTranslation('chat'); const [copied, setCopied] = useState(false); const raw = Array.isArray(children) ? children.join('') : String(children ?? ''); const looksMultiline = /[\r\n]/.test(raw); const inlineDetected = inline || (node && node.type === 'inlineCode'); const shouldInline = inlineDetected || !looksMultiline; if (shouldInline) { return ( {children} ); } const match = /language-(\w+)/.exec(className || ''); const language = match ? match[1] : 'text'; const textToCopy = raw; const handleCopy = () => { const doSet = () => { setCopied(true); setTimeout(() => setCopied(false), 1500); }; try { if (navigator && navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(textToCopy).then(doSet).catch(() => { const ta = document.createElement('textarea'); ta.value = textToCopy; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch {} document.body.removeChild(ta); doSet(); }); } else { const ta = document.createElement('textarea'); ta.value = textToCopy; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch {} document.body.removeChild(ta); doSet(); } } catch {} }; return (
{language && language !== 'text' && (
{language}
)} {raw}
); }; const markdownComponents = { code: CodeBlock, blockquote: ({ children }: { children?: React.ReactNode }) => (
{children}
), a: ({ href, children }: { href?: string; children?: React.ReactNode }) => ( {children} ), p: ({ children }: { children?: React.ReactNode }) =>
{children}
, table: ({ children }: { children?: React.ReactNode }) => (
{children}
), thead: ({ children }: { children?: React.ReactNode }) => {children}, th: ({ children }: { children?: React.ReactNode }) => ( {children} ), td: ({ children }: { children?: React.ReactNode }) => ( {children} ), }; export function Markdown({ children, className }: MarkdownProps) { const content = normalizeInlineCodeFences(String(children ?? '')); const remarkPlugins = useMemo(() => [remarkGfm, remarkMath], []); const rehypePlugins = useMemo(() => [rehypeKatex], []); return (
{content}
); }