MarkdownRenderer.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import React, { memo, useMemo } from 'react';
  2. import ReactMarkdown from 'react-markdown';
  3. import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
  4. import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
  5. import remarkGfm from 'remark-gfm';
  6. import remarkMath from 'remark-math';
  7. import rehypeKatex from 'rehype-katex';
  8. import 'katex/dist/katex.min.css'; // KaTeX 样式
  9. interface MarkdownRendererProps {
  10. content: string;
  11. className?: string;
  12. isStreaming?: boolean; // 是否正在流式输出
  13. }
  14. const MarkdownRenderer: React.FC<MarkdownRendererProps> = memo(({ content, className = '', isStreaming = false }) => {
  15. // 流式输出时使用简化渲染,完成后才做完整 Markdown 渲染
  16. const remarkPlugins = useMemo(() => isStreaming ? [] : [remarkGfm, remarkMath], [isStreaming]);
  17. const rehypePlugins = useMemo(() => isStreaming ? [] : [rehypeKatex], [isStreaming]);
  18. // 缓存 components 配置,避免每次渲染都创建新对象
  19. const components = useMemo(() => ({
  20. // 代码块渲染 - 流式时简化处理
  21. code({ inline, className, children, ...props }: any) {
  22. const match = /language-(\w+)/.exec(className || '');
  23. const language = match ? match[1] : '';
  24. // 流式输出时使用简单代码块,避免频繁高亮计算
  25. if (isStreaming && !inline && language) {
  26. return (
  27. <pre className="bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto my-2">
  28. <code className={`language-${language}`}>{children}</code>
  29. </pre>
  30. );
  31. }
  32. return !inline && language ? (
  33. <div className="relative group">
  34. <SyntaxHighlighter
  35. style={oneDark as any}
  36. language={language}
  37. PreTag="div"
  38. className="rounded-lg !mt-2 !mb-4"
  39. showLineNumbers={true}
  40. wrapLines={true}
  41. {...props}
  42. >
  43. {String(children).replace(/\n$/, '')}
  44. </SyntaxHighlighter>
  45. <button
  46. onClick={() => {
  47. navigator.clipboard.writeText(String(children));
  48. }}
  49. className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-gray-700 hover:bg-gray-600 text-white text-xs px-2 py-1 rounded"
  50. >
  51. 复制
  52. </button>
  53. </div>
  54. ) : (
  55. <code
  56. className="bg-gray-100 text-red-600 px-1.5 py-0.5 rounded text-sm font-mono"
  57. {...props}
  58. >
  59. {children}
  60. </code>
  61. );
  62. },
  63. // 表格样式
  64. table({ children }: any) {
  65. return (
  66. <div className="overflow-x-auto my-4">
  67. <table className="min-w-full border-collapse border border-gray-300 rounded-lg overflow-hidden">
  68. {children}
  69. </table>
  70. </div>
  71. );
  72. },
  73. thead({ children }: any) {
  74. return (
  75. <thead className="bg-gray-50">
  76. {children}
  77. </thead>
  78. );
  79. },
  80. th({ children }: any) {
  81. return (
  82. <th className="border border-gray-300 px-4 py-2 text-left font-semibold text-gray-700">
  83. {children}
  84. </th>
  85. );
  86. },
  87. td({ children }: any) {
  88. return (
  89. <td className="border border-gray-300 px-4 py-2 text-gray-600">
  90. {children}
  91. </td>
  92. );
  93. },
  94. // 标题样式
  95. h1({ children }: any) {
  96. return (
  97. <h1 className="text-2xl font-bold text-gray-900 mt-6 mb-4 pb-2 border-b border-gray-200">
  98. {children}
  99. </h1>
  100. );
  101. },
  102. h2({ children }: any) {
  103. return (
  104. <h2 className="text-xl font-bold text-gray-900 mt-5 mb-3">
  105. {children}
  106. </h2>
  107. );
  108. },
  109. h3({ children }: any) {
  110. return (
  111. <h3 className="text-lg font-semibold text-gray-900 mt-4 mb-2">
  112. {children}
  113. </h3>
  114. );
  115. },
  116. h4({ children }: any) {
  117. return (
  118. <h4 className="text-base font-semibold text-gray-900 mt-3 mb-2">
  119. {children}
  120. </h4>
  121. );
  122. },
  123. // 段落样式
  124. p({ children }: any) {
  125. return (
  126. <p className="mb-4 leading-relaxed text-gray-700">
  127. {children}
  128. </p>
  129. );
  130. },
  131. // 列表样式
  132. ul({ children }: any) {
  133. return (
  134. <ul className="mb-4 ml-6 space-y-1 list-disc">
  135. {children}
  136. </ul>
  137. );
  138. },
  139. ol({ children }: any) {
  140. return (
  141. <ol className="mb-4 ml-6 space-y-1 list-decimal">
  142. {children}
  143. </ol>
  144. );
  145. },
  146. li({ children }: any) {
  147. return (
  148. <li className="text-gray-700 leading-relaxed">
  149. {children}
  150. </li>
  151. );
  152. },
  153. // 引用样式
  154. blockquote({ children }: any) {
  155. return (
  156. <blockquote className="border-l-4 border-blue-500 pl-4 py-2 my-4 bg-blue-50 text-gray-700 italic">
  157. {children}
  158. </blockquote>
  159. );
  160. },
  161. // 链接样式
  162. a({ href, children }: any) {
  163. return (
  164. <a
  165. href={href}
  166. target="_blank"
  167. rel="noopener noreferrer"
  168. className="text-blue-600 hover:text-blue-800 underline"
  169. >
  170. {children}
  171. </a>
  172. );
  173. },
  174. // 强调样式
  175. strong({ children }: any) {
  176. return (
  177. <strong className="font-semibold text-gray-900">
  178. {children}
  179. </strong>
  180. );
  181. },
  182. em({ children }: any) {
  183. return (
  184. <em className="italic text-gray-700">
  185. {children}
  186. </em>
  187. );
  188. },
  189. // 分割线
  190. hr() {
  191. return (
  192. <hr className="my-6 border-t border-gray-300" />
  193. );
  194. },
  195. }), [isStreaming]);
  196. return (
  197. <div className={`markdown-content ${className}`}>
  198. <ReactMarkdown
  199. remarkPlugins={remarkPlugins}
  200. rehypePlugins={rehypePlugins}
  201. components={components}
  202. >
  203. {content}
  204. </ReactMarkdown>
  205. </div>
  206. );
  207. });
  208. export default MarkdownRenderer;