'use client'; import { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; import ReactMarkdown from 'react-markdown'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import { Anchor, Blockquote, Checkbox, Code, Divider, List, Table, Text, Title, } from '@mantine/core'; import classes from './MarkdownMessage.module.css'; // Import KaTeX CSS for LaTeX rendering import 'katex/dist/katex.min.css'; interface MarkdownMessageProps { content: string; isStreaming?: boolean; } /** * Hook that tracks content changes and returns content with fade-in markers for new text */ function useStreamingContent(content: string, isStreaming: boolean) { const [processedContent, setProcessedContent] = useState(content); const prevContentRef = useRef(''); const fadeIdRef = useRef(0); useEffect(() => { if (!isStreaming) { // When not streaming, just use the content directly (no animation spans) setProcessedContent(content); prevContentRef.current = content; return; } const prevContent = prevContentRef.current; const prevLength = prevContent.length; const currentLength = content.length; if (currentLength > prevLength) { // New content arrived - wrap it in a fade-in span const existingContent = content.slice(0, prevLength); const newContent = content.slice(prevLength); const fadeId = fadeIdRef.current++; // Wrap new content in a span with fade-in class // Use a unique key to force re-render of the animation const wrappedNew = `${escapeHtml(newContent)}`; setProcessedContent(existingContent + wrappedNew); prevContentRef.current = content; } else if (currentLength < prevLength) { // Content was reset setProcessedContent(content); prevContentRef.current = content; fadeIdRef.current = 0; } }, [content, isStreaming]); // When streaming ends, clean up the spans and show plain content useEffect(() => { if (!isStreaming && content) { setProcessedContent(content); prevContentRef.current = content; } }, [isStreaming, content]); return processedContent; } /** * Escape HTML special characters to prevent XSS */ function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessageProps) { const processedContent = useStreamingContent(content, isStreaming); return (
{children};
}
return (
{children}
);
},
pre: ({ children }) => {children}, hr: () =>