From da65cedf28942b77e8a904ec9b16b4c190645d3a Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sat, 27 Dec 2025 22:32:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(Markdown):=20=E9=9B=86=E6=88=90=20Mermaid?= =?UTF-8?q?=20=E5=9B=BE=E8=A1=A8=E5=92=8C=20KaTeX=20=E5=85=AC=E5=BC=8F?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MarkdownRenderer 集成 MermaidBlock 组件 - 添加 remark-math 和 rehype-katex 插件支持数学公式 - 添加 isStreaming 参数传递优化流式渲染 - MessageBubble 传递流式状态给渲染器 --- src/components/features/MessageBubble.tsx | 1 + src/components/markdown/MarkdownRenderer.tsx | 38 +++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/features/MessageBubble.tsx b/src/components/features/MessageBubble.tsx index 1b53224..f81c2a9 100644 --- a/src/components/features/MessageBubble.tsx +++ b/src/components/features/MessageBubble.tsx @@ -332,6 +332,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err content={message.content} onImageLinkClick={handleImageLinkClick} onLinkClick={onLinkClick} + isStreaming={isStreaming} /> ) : isStreaming ? (
diff --git a/src/components/markdown/MarkdownRenderer.tsx b/src/components/markdown/MarkdownRenderer.tsx index f5b8ec9..875263b 100644 --- a/src/components/markdown/MarkdownRenderer.tsx +++ b/src/components/markdown/MarkdownRenderer.tsx @@ -3,9 +3,15 @@ import { memo, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; import { CodeBlock } from './CodeBlock'; +import { MermaidBlock } from './MermaidBlock'; import { cn } from '@/lib/utils'; +// 导入 KaTeX 样式 +import 'katex/dist/katex.min.css'; + interface MarkdownRendererProps { content: string; className?: string; @@ -13,6 +19,8 @@ interface MarkdownRendererProps { onImageLinkClick?: (url: string) => void; /** 普通链接点击回调,用于在预览窗口中打开 */ onLinkClick?: (url: string) => void; + /** 是否正在流式输出(用于延迟 Mermaid 渲染) */ + isStreaming?: boolean; } /** @@ -30,12 +38,18 @@ function isImageUrl(url: string): boolean { * 创建 Markdown 组件配置 * 使用工厂函数以支持传入回调 */ -function createMarkdownComponents(onImageLinkClick?: (url: string) => void, onLinkClick?: (url: string) => void) { +function createMarkdownComponents( + onImageLinkClick?: (url: string) => void, + onLinkClick?: (url: string) => void, + isStreaming?: boolean +) { return { // 代码块 code({ className, children, ...props }: { className?: string; children?: React.ReactNode }) { const match = /language-(\w+)/.exec(className || ''); const isInline = !match && !className; + const language = match ? match[1] : 'text'; + const codeContent = String(children).replace(/\n$/, ''); if (isInline) { // 行内代码 - 无特殊样式,仅等宽字体 @@ -49,11 +63,16 @@ function createMarkdownComponents(onImageLinkClick?: (url: string) => void, onLi ); } - // 代码块 + // Mermaid 图表 - 使用专门的 MermaidBlock 组件 + if (language === 'mermaid') { + return ; + } + + // 普通代码块 return ( ); }, @@ -259,17 +278,18 @@ function createMarkdownComponents(onImageLinkClick?: (url: string) => void, onLi } // 使用 memo 包裹组件,避免不必要的重渲染 -export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className, onImageLinkClick, onLinkClick }: MarkdownRendererProps) { - // 使用 useMemo 缓存 components 配置,仅在回调变化时重新创建 +export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className, onImageLinkClick, onLinkClick, isStreaming }: MarkdownRendererProps) { + // 使用 useMemo 缓存 components 配置,仅在回调或流式状态变化时重新创建 const components = useMemo( - () => createMarkdownComponents(onImageLinkClick, onLinkClick), - [onImageLinkClick, onLinkClick] + () => createMarkdownComponents(onImageLinkClick, onLinkClick, isStreaming), + [onImageLinkClick, onLinkClick, isStreaming] ); return (
{content}