MarkdownRenderer.tsx 6.0 KB

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