index.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import React, { useEffect, useRef, useState } from 'react'
  2. import mermaid from 'mermaid'
  3. import { usePrevious } from 'ahooks'
  4. import CryptoJS from 'crypto-js'
  5. import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
  6. import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
  7. let mermaidAPI: any
  8. mermaidAPI = null
  9. if (typeof window !== 'undefined') {
  10. mermaid.initialize({
  11. startOnLoad: true,
  12. theme: 'default',
  13. flowchart: {
  14. htmlLabels: true,
  15. useMaxWidth: true,
  16. },
  17. })
  18. mermaidAPI = mermaid.mermaidAPI
  19. }
  20. const style = {
  21. minWidth: '480px',
  22. height: 'auto',
  23. overflow: 'auto',
  24. }
  25. const svgToBase64 = (svgGraph: string) => {
  26. const svgBytes = new TextEncoder().encode(svgGraph)
  27. const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
  28. return new Promise((resolve, reject) => {
  29. const reader = new FileReader()
  30. reader.onloadend = () => resolve(reader.result)
  31. reader.onerror = reject
  32. reader.readAsDataURL(blob)
  33. })
  34. }
  35. const Flowchart = React.forwardRef((props: {
  36. PrimitiveCode: string
  37. }, ref) => {
  38. const [svgCode, setSvgCode] = useState(null)
  39. const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
  40. const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
  41. const [isLoading, setIsLoading] = useState(true)
  42. const timeRef = useRef<NodeJS.Timeout>()
  43. const [errMsg, setErrMsg] = useState('')
  44. const renderFlowchart = async (PrimitiveCode: string) => {
  45. try {
  46. if (typeof window !== 'undefined' && mermaidAPI) {
  47. const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode)
  48. const base64Svg: any = await svgToBase64(svgGraph.svg)
  49. setSvgCode(base64Svg)
  50. setIsLoading(false)
  51. if (chartId.current && base64Svg)
  52. localStorage.setItem(chartId.current, base64Svg)
  53. }
  54. }
  55. catch (error) {
  56. if (prevPrimitiveCode === props.PrimitiveCode) {
  57. setIsLoading(false)
  58. setErrMsg((error as Error).message)
  59. }
  60. }
  61. }
  62. useEffect(() => {
  63. const cachedSvg: any = localStorage.getItem(chartId.current)
  64. if (cachedSvg) {
  65. setSvgCode(cachedSvg)
  66. setIsLoading(false)
  67. return
  68. }
  69. if (timeRef.current)
  70. clearTimeout(timeRef.current)
  71. timeRef.current = setTimeout(() => {
  72. renderFlowchart(props.PrimitiveCode)
  73. }, 300)
  74. }, [props.PrimitiveCode])
  75. return (
  76. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  77. // @ts-expect-error
  78. <div ref={ref}>
  79. {
  80. svgCode
  81. && <div className="mermaid" style={style}>
  82. {svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="Mermaid chart" />}
  83. </div>
  84. }
  85. {isLoading
  86. && <div className='py-4 px-[26px]'>
  87. <LoadingAnim type='text' />
  88. </div>
  89. }
  90. {
  91. errMsg
  92. && <div className='py-4 px-[26px]'>
  93. <ExclamationTriangleIcon className='w-6 h-6 text-red-500' />
  94. &nbsp;
  95. {errMsg}
  96. </div>
  97. }
  98. </div>
  99. )
  100. })
  101. export default Flowchart