markdown.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import ReactMarkdown from 'react-markdown'
  2. import ReactEcharts from 'echarts-for-react'
  3. import 'katex/dist/katex.min.css'
  4. import RemarkMath from 'remark-math'
  5. import RemarkBreaks from 'remark-breaks'
  6. import RehypeKatex from 'rehype-katex'
  7. import RemarkGfm from 'remark-gfm'
  8. import SyntaxHighlighter from 'react-syntax-highlighter'
  9. import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
  10. import type { RefObject } from 'react'
  11. import { memo, useEffect, useMemo, useRef, useState } from 'react'
  12. import type { CodeComponent } from 'react-markdown/lib/ast-to-react'
  13. import cn from '@/utils/classnames'
  14. import CopyBtn from '@/app/components/base/copy-btn'
  15. import SVGBtn from '@/app/components/base/svg'
  16. import Flowchart from '@/app/components/base/mermaid'
  17. import ImageGallery from '@/app/components/base/image-gallery'
  18. // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
  19. const capitalizationLanguageNameMap: Record<string, string> = {
  20. sql: 'SQL',
  21. javascript: 'JavaScript',
  22. java: 'Java',
  23. typescript: 'TypeScript',
  24. vbscript: 'VBScript',
  25. css: 'CSS',
  26. html: 'HTML',
  27. xml: 'XML',
  28. php: 'PHP',
  29. python: 'Python',
  30. yaml: 'Yaml',
  31. mermaid: 'Mermaid',
  32. markdown: 'MarkDown',
  33. makefile: 'MakeFile',
  34. echarts: 'ECharts',
  35. }
  36. const getCorrectCapitalizationLanguageName = (language: string) => {
  37. if (!language)
  38. return 'Plain'
  39. if (language in capitalizationLanguageNameMap)
  40. return capitalizationLanguageNameMap[language]
  41. return language.charAt(0).toUpperCase() + language.substring(1)
  42. }
  43. const preprocessLaTeX = (content: string) => {
  44. if (typeof content !== 'string')
  45. return content
  46. return content.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`)
  47. .replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`)
  48. .replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`)
  49. }
  50. export function PreCode(props: { children: any }) {
  51. const ref = useRef<HTMLPreElement>(null)
  52. return (
  53. <pre ref={ref}>
  54. <span
  55. className="copy-code-button"
  56. ></span>
  57. {props.children}
  58. </pre>
  59. )
  60. }
  61. const useLazyLoad = (ref: RefObject<Element>): boolean => {
  62. const [isIntersecting, setIntersecting] = useState<boolean>(false)
  63. useEffect(() => {
  64. const observer = new IntersectionObserver(([entry]) => {
  65. if (entry.isIntersecting) {
  66. setIntersecting(true)
  67. observer.disconnect()
  68. }
  69. })
  70. if (ref.current)
  71. observer.observe(ref.current)
  72. return () => {
  73. observer.disconnect()
  74. }
  75. }, [ref])
  76. return isIntersecting
  77. }
  78. // **Add code block
  79. // Avoid error #185 (Maximum update depth exceeded.
  80. // This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
  81. // React limits the number of nested updates to prevent infinite loops.)
  82. // Reference A: https://reactjs.org/docs/error-decoder.html?invariant=185
  83. // Reference B1: https://react.dev/reference/react/memo
  84. // Reference B2: https://react.dev/reference/react/useMemo
  85. // ****
  86. // The original error that occurred in the streaming response during the conversation:
  87. // Error: Minified React error 185;
  88. // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
  89. // or use the non-minified dev environment for full errors and additional helpful warnings.
  90. const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }) => {
  91. const [isSVG, setIsSVG] = useState(true)
  92. const match = /language-(\w+)/.exec(className || '')
  93. const language = match?.[1]
  94. const languageShowName = getCorrectCapitalizationLanguageName(language || '')
  95. let chartData = JSON.parse(String('{"title":{"text":"Something went wrong."}}').replace(/\n$/, ''))
  96. if (language === 'echarts') {
  97. try {
  98. chartData = JSON.parse(String(children).replace(/\n$/, ''))
  99. }
  100. catch (error) {
  101. }
  102. }
  103. // Use `useMemo` to ensure that `SyntaxHighlighter` only re-renders when necessary
  104. return useMemo(() => {
  105. return (!inline && match)
  106. ? (
  107. <div>
  108. <div
  109. className='flex justify-between h-8 items-center p-1 pl-3 border-b'
  110. style={{
  111. borderColor: 'rgba(0, 0, 0, 0.05)',
  112. }}
  113. >
  114. <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
  115. <div style={{ display: 'flex' }}>
  116. {language === 'mermaid'
  117. && <SVGBtn
  118. isSVG={isSVG}
  119. setIsSVG={setIsSVG}
  120. />
  121. }
  122. <CopyBtn
  123. className='mr-1'
  124. value={String(children).replace(/\n$/, '')}
  125. isPlain
  126. />
  127. </div>
  128. </div>
  129. {(language === 'mermaid' && isSVG)
  130. ? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />)
  131. : (
  132. (language === 'echarts')
  133. ? (<div style={{ minHeight: '250px', minWidth: '250px' }}><ReactEcharts
  134. option={chartData}
  135. >
  136. </ReactEcharts></div>)
  137. : (<SyntaxHighlighter
  138. {...props}
  139. style={atelierHeathLight}
  140. customStyle={{
  141. paddingLeft: 12,
  142. backgroundColor: '#fff',
  143. }}
  144. language={match[1]}
  145. showLineNumbers
  146. PreTag="div"
  147. >
  148. {String(children).replace(/\n$/, '')}
  149. </SyntaxHighlighter>))}
  150. </div>
  151. )
  152. : (
  153. <code {...props} className={className}>
  154. {children}
  155. </code>
  156. )
  157. }, [chartData, children, className, inline, isSVG, language, languageShowName, match, props])
  158. })
  159. CodeBlock.displayName = 'CodeBlock'
  160. export function Markdown(props: { content: string; className?: string }) {
  161. const latexContent = preprocessLaTeX(props.content)
  162. return (
  163. <div className={cn(props.className, 'markdown-body')}>
  164. <ReactMarkdown
  165. remarkPlugins={[[RemarkMath, { singleDollarTextMath: false }], RemarkGfm, RemarkBreaks]}
  166. rehypePlugins={[
  167. RehypeKatex as any,
  168. ]}
  169. components={{
  170. code: CodeBlock,
  171. img({ src }) {
  172. return (
  173. <ImageGallery srcs={[src || '']} />
  174. )
  175. },
  176. p: (paragraph) => {
  177. const { node }: any = paragraph
  178. if (node.children[0].tagName === 'img') {
  179. const image = node.children[0]
  180. return (
  181. <>
  182. <ImageGallery srcs={[image.properties.src]} />
  183. <p>{paragraph.children.slice(1)}</p>
  184. </>
  185. )
  186. }
  187. return <p>{paragraph.children}</p>
  188. },
  189. }}
  190. linkTarget='_blank'
  191. >
  192. {/* Markdown detect has problem. */}
  193. {latexContent}
  194. </ReactMarkdown>
  195. </div>
  196. )
  197. }