markdown.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import ReactMarkdown from 'react-markdown'
  2. import 'katex/dist/katex.min.css'
  3. import RemarkMath from 'remark-math'
  4. import RemarkBreaks from 'remark-breaks'
  5. import RehypeKatex from 'rehype-katex'
  6. import RemarkGfm from 'remark-gfm'
  7. import SyntaxHighlighter from 'react-syntax-highlighter'
  8. import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
  9. import type { RefObject } from 'react'
  10. import { useEffect, useRef, useState } from 'react'
  11. import CopyBtn from '@/app/components/app/chat/copy-btn'
  12. // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
  13. const capitalizationLanguageNameMap: Record<string, string> = {
  14. sql: 'SQL',
  15. javascript: 'JavaScript',
  16. typescript: 'TypeScript',
  17. vbscript: 'VBScript',
  18. css: 'CSS',
  19. html: 'HTML',
  20. xml: 'XML',
  21. php: 'PHP',
  22. }
  23. const getCorrectCapitalizationLanguageName = (language: string) => {
  24. if (!language)
  25. return 'Plain'
  26. if (language in capitalizationLanguageNameMap)
  27. return capitalizationLanguageNameMap[language]
  28. return language.charAt(0).toUpperCase() + language.substring(1)
  29. }
  30. export function PreCode(props: { children: any }) {
  31. const ref = useRef<HTMLPreElement>(null)
  32. return (
  33. <pre ref={ref}>
  34. <span
  35. className="copy-code-button"
  36. onClick={() => {
  37. if (ref.current) {
  38. const code = ref.current.innerText
  39. // copyToClipboard(code);
  40. }
  41. }}
  42. ></span>
  43. {props.children}
  44. </pre>
  45. )
  46. }
  47. const useLazyLoad = (ref: RefObject<Element>): boolean => {
  48. const [isIntersecting, setIntersecting] = useState<boolean>(false)
  49. useEffect(() => {
  50. const observer = new IntersectionObserver(([entry]) => {
  51. if (entry.isIntersecting) {
  52. setIntersecting(true)
  53. observer.disconnect()
  54. }
  55. })
  56. if (ref.current)
  57. observer.observe(ref.current)
  58. return () => {
  59. observer.disconnect()
  60. }
  61. }, [ref])
  62. return isIntersecting
  63. }
  64. export function Markdown(props: { content: string }) {
  65. const [isCopied, setIsCopied] = useState(false)
  66. return (
  67. <div className="markdown-body">
  68. <ReactMarkdown
  69. remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
  70. rehypePlugins={[
  71. RehypeKatex,
  72. ]}
  73. components={{
  74. code({ node, inline, className, children, ...props }) {
  75. const match = /language-(\w+)/.exec(className || '')
  76. const language = match?.[1]
  77. const languageShowName = getCorrectCapitalizationLanguageName(language || '')
  78. return (!inline && match)
  79. ? (
  80. <div>
  81. <div
  82. className='flex justify-between h-8 items-center p-1 pl-3 border-b'
  83. style={{
  84. borderColor: 'rgba(0, 0, 0, 0.05)',
  85. }}
  86. >
  87. <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
  88. <CopyBtn
  89. value={String(children).replace(/\n$/, '')}
  90. isPlain
  91. />
  92. </div>
  93. <SyntaxHighlighter
  94. {...props}
  95. style={atelierHeathLight}
  96. customStyle={{
  97. paddingLeft: 12,
  98. backgroundColor: '#fff',
  99. }}
  100. language={match[1]}
  101. showLineNumbers
  102. PreTag="div"
  103. >
  104. {String(children).replace(/\n$/, '')}
  105. </SyntaxHighlighter>
  106. </div>
  107. )
  108. : (
  109. <code {...props} className={className}>
  110. {children}
  111. </code>
  112. )
  113. },
  114. }}
  115. linkTarget={'_blank'}
  116. >
  117. {/* Markdown detect has problem. */}
  118. {props.content}
  119. </ReactMarkdown>
  120. </div>
  121. )
  122. }