markdown.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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. // import { copyToClipboard } from "../utils";
  13. // https://txtfiddle.com/~hlshwya/extract-urls-from-text
  14. // const urlRegex = /\b((https?|ftp|file):\/\/|(www|ftp)\.)[-A-Z0-9+&@#\/%?=~_|$!:,.;]*[A-Z0-9+&@#\/%=~_|$]/ig
  15. // function highlightURL(content: string) {
  16. // return content.replace(urlRegex, (url) => {
  17. // // fix http:// in [] will be parsed to link agin
  18. // const res = `[${url.replace('://', '://')}](${url})`
  19. // return res
  20. // })
  21. // }
  22. export function PreCode(props: { children: any }) {
  23. const ref = useRef<HTMLPreElement>(null)
  24. return (
  25. <pre ref={ref}>
  26. <span
  27. className="copy-code-button"
  28. onClick={() => {
  29. if (ref.current) {
  30. const code = ref.current.innerText
  31. // copyToClipboard(code);
  32. }
  33. }}
  34. ></span>
  35. {props.children}
  36. </pre>
  37. )
  38. }
  39. const useLazyLoad = (ref: RefObject<Element>): boolean => {
  40. const [isIntersecting, setIntersecting] = useState<boolean>(false)
  41. useEffect(() => {
  42. const observer = new IntersectionObserver(([entry]) => {
  43. if (entry.isIntersecting) {
  44. setIntersecting(true)
  45. observer.disconnect()
  46. }
  47. })
  48. if (ref.current)
  49. observer.observe(ref.current)
  50. return () => {
  51. observer.disconnect()
  52. }
  53. }, [ref])
  54. return isIntersecting
  55. }
  56. export function Markdown(props: { content: string }) {
  57. const [isCopied, setIsCopied] = useState(false)
  58. return (
  59. <div className="markdown-body">
  60. <ReactMarkdown
  61. remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
  62. rehypePlugins={[
  63. RehypeKatex,
  64. ]}
  65. components={{
  66. code({ node, inline, className, children, ...props }) {
  67. const match = /language-(\w+)/.exec(className || '')
  68. const language = match?.[1]
  69. const languageShowName = (() => {
  70. if (language)
  71. return language.charAt(0).toUpperCase() + language.substring(1)
  72. return 'Plain'
  73. })()
  74. return (!inline && match)
  75. ? (
  76. <div>
  77. <div
  78. className='flex justify-between h-8 items-center p-1 pl-3 border-b'
  79. style={{
  80. borderColor: 'rgba(0, 0, 0, 0.05)',
  81. }}
  82. >
  83. <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
  84. <CopyBtn
  85. value={String(children).replace(/\n$/, '')}
  86. isPlain
  87. />
  88. </div>
  89. <SyntaxHighlighter
  90. {...props}
  91. style={atelierHeathLight}
  92. customStyle={{
  93. paddingLeft: 12,
  94. backgroundColor: '#fff',
  95. }}
  96. language={match[1]}
  97. showLineNumbers
  98. PreTag="div"
  99. >
  100. {String(children).replace(/\n$/, '')}
  101. </SyntaxHighlighter>
  102. </div>
  103. )
  104. : (
  105. <code {...props} className={className}>
  106. {children}
  107. </code>
  108. )
  109. },
  110. }}
  111. linkTarget={'_blank'}
  112. >
  113. {/* Markdown detect has problem. */}
  114. {props.content}
  115. </ReactMarkdown>
  116. </div>
  117. )
  118. }