feat(Markdown): 集成 Mermaid 图表和 KaTeX 公式渲染
- MarkdownRenderer 集成 MermaidBlock 组件 - 添加 remark-math 和 rehype-katex 插件支持数学公式 - 添加 isStreaming 参数传递优化流式渲染 - MessageBubble 传递流式状态给渲染器
This commit is contained in:
parent
ead84a1921
commit
da65cedf28
@ -332,6 +332,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
|||||||
content={message.content}
|
content={message.content}
|
||||||
onImageLinkClick={handleImageLinkClick}
|
onImageLinkClick={handleImageLinkClick}
|
||||||
onLinkClick={onLinkClick}
|
onLinkClick={onLinkClick}
|
||||||
|
isStreaming={isStreaming}
|
||||||
/>
|
/>
|
||||||
) : isStreaming ? (
|
) : isStreaming ? (
|
||||||
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
|
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
|
||||||
|
|||||||
@ -3,9 +3,15 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkMath from 'remark-math';
|
||||||
|
import rehypeKatex from 'rehype-katex';
|
||||||
import { CodeBlock } from './CodeBlock';
|
import { CodeBlock } from './CodeBlock';
|
||||||
|
import { MermaidBlock } from './MermaidBlock';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
// 导入 KaTeX 样式
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
interface MarkdownRendererProps {
|
interface MarkdownRendererProps {
|
||||||
content: string;
|
content: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -13,6 +19,8 @@ interface MarkdownRendererProps {
|
|||||||
onImageLinkClick?: (url: string) => void;
|
onImageLinkClick?: (url: string) => void;
|
||||||
/** 普通链接点击回调,用于在预览窗口中打开 */
|
/** 普通链接点击回调,用于在预览窗口中打开 */
|
||||||
onLinkClick?: (url: string) => void;
|
onLinkClick?: (url: string) => void;
|
||||||
|
/** 是否正在流式输出(用于延迟 Mermaid 渲染) */
|
||||||
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,12 +38,18 @@ function isImageUrl(url: string): boolean {
|
|||||||
* 创建 Markdown 组件配置
|
* 创建 Markdown 组件配置
|
||||||
* 使用工厂函数以支持传入回调
|
* 使用工厂函数以支持传入回调
|
||||||
*/
|
*/
|
||||||
function createMarkdownComponents(onImageLinkClick?: (url: string) => void, onLinkClick?: (url: string) => void) {
|
function createMarkdownComponents(
|
||||||
|
onImageLinkClick?: (url: string) => void,
|
||||||
|
onLinkClick?: (url: string) => void,
|
||||||
|
isStreaming?: boolean
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
// 代码块
|
// 代码块
|
||||||
code({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
|
code({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
const isInline = !match && !className;
|
const isInline = !match && !className;
|
||||||
|
const language = match ? match[1] : 'text';
|
||||||
|
const codeContent = String(children).replace(/\n$/, '');
|
||||||
|
|
||||||
if (isInline) {
|
if (isInline) {
|
||||||
// 行内代码 - 无特殊样式,仅等宽字体
|
// 行内代码 - 无特殊样式,仅等宽字体
|
||||||
@ -49,11 +63,16 @@ function createMarkdownComponents(onImageLinkClick?: (url: string) => void, onLi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代码块
|
// Mermaid 图表 - 使用专门的 MermaidBlock 组件
|
||||||
|
if (language === 'mermaid') {
|
||||||
|
return <MermaidBlock code={codeContent} isStreaming={isStreaming} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通代码块
|
||||||
return (
|
return (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={String(children).replace(/\n$/, '')}
|
code={codeContent}
|
||||||
language={match ? match[1] : 'text'}
|
language={language}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -259,17 +278,18 @@ function createMarkdownComponents(onImageLinkClick?: (url: string) => void, onLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 memo 包裹组件,避免不必要的重渲染
|
// 使用 memo 包裹组件,避免不必要的重渲染
|
||||||
export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className, onImageLinkClick, onLinkClick }: MarkdownRendererProps) {
|
export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className, onImageLinkClick, onLinkClick, isStreaming }: MarkdownRendererProps) {
|
||||||
// 使用 useMemo 缓存 components 配置,仅在回调变化时重新创建
|
// 使用 useMemo 缓存 components 配置,仅在回调或流式状态变化时重新创建
|
||||||
const components = useMemo(
|
const components = useMemo(
|
||||||
() => createMarkdownComponents(onImageLinkClick, onLinkClick),
|
() => createMarkdownComponents(onImageLinkClick, onLinkClick, isStreaming),
|
||||||
[onImageLinkClick, onLinkClick]
|
[onImageLinkClick, onLinkClick, isStreaming]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('markdown-content', className)}>
|
<div className={cn('markdown-content', className)}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
components={components}
|
components={components}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user