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}