/* eslint-disable react-hooks/exhaustive-deps */ import ReactMarkdown, { Components } from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkEmoji from 'remark-emoji' import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import * as prismStyles from 'react-syntax-highlighter/dist/cjs/styles/prism' import { memo, useState, useMemo, useRef, useEffect } from 'react' import { getReadableLanguageName } from '@/lib/utils' import { cn } from '@/lib/utils' import { useCodeblock } from '@/hooks/useCodeblock' import 'katex/dist/katex.min.css' import { IconCopy, IconCopyCheck } from '@tabler/icons-react' import rehypeRaw from 'rehype-raw' import { useTranslation } from '@/i18n/react-i18next-compat' interface MarkdownProps { content: string className?: string components?: Components enableRawHtml?: boolean isUser?: boolean isWrapping?: boolean } function RenderMarkdownComponent({ content, enableRawHtml, className, isUser, components, isWrapping, }: MarkdownProps) { const { t } = useTranslation() const { codeBlockStyle, showLineNumbers } = useCodeblock() // State for tracking which code block has been copied const [copiedId, setCopiedId] = useState(null) // Map to store unique IDs for code blocks based on content and position const codeBlockIds = useRef(new Map()) // Clear ID map when content changes useEffect(() => { codeBlockIds.current.clear() }, [content]) // Function to handle copying code to clipboard const handleCopy = (code: string, id: string) => { navigator.clipboard.writeText(code) setCopiedId(id) // Reset copied state after 2 seconds setTimeout(() => { setCopiedId(null) }, 2000) } // Default components for syntax highlighting and emoji rendering const defaultComponents: Components = useMemo( () => ({ code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || '') const language = match ? match[1] : '' const isInline = !match || !language const code = String(children).replace(/\n$/, '') // Generate a unique ID based on content and language const contentKey = `${code}-${language}` let codeId = codeBlockIds.current.get(contentKey) if (!codeId) { codeId = `code-${codeBlockIds.current.size}` codeBlockIds.current.set(contentKey, codeId) } return !isInline && !isUser ? (
{getReadableLanguageName(language)}
index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1) ) .join('') as keyof typeof prismStyles ] || prismStyles.oneLight } language={language} showLineNumbers={showLineNumbers} wrapLines={true} // Temporary comment we try calculate main area width on __root lineProps={ isWrapping ? { style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' }, } : {} } customStyle={{ margin: 0, padding: '8px', borderRadius: '0 0 4px 4px', overflow: 'auto', border: 'none', }} PreTag="div" CodeTag={'code'} {...props} > {String(children).replace(/\n$/, '')}
) : ( {children} ) }, }), [codeBlockStyle, showLineNumbers, copiedId] ) // Memoize the remarkPlugins to prevent unnecessary re-renders const remarkPlugins = useMemo(() => { // Using a simpler configuration to avoid TypeScript errors return [remarkGfm, remarkMath, remarkEmoji] }, []) // Memoize the rehypePlugins to prevent unnecessary re-renders const rehypePlugins = useMemo(() => { return enableRawHtml ? [rehypeKatex, rehypeRaw] : [rehypeKatex] }, [enableRawHtml]) // Merge custom components with default components const mergedComponents = useMemo( () => ({ ...defaultComponents, ...components, }), [defaultComponents, components] ) // Render the markdown content return (
{content}
) } // Use a simple memo without custom comparison to allow re-renders when content changes // This is important for streaming content to render incrementally export const RenderMarkdown = memo(RenderMarkdownComponent)