index.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import React, { useCallback, useEffect, useRef, useState } from 'react'
  2. import mermaid from 'mermaid'
  3. import { usePrevious } from 'ahooks'
  4. import CryptoJS from 'crypto-js'
  5. import { useTranslation } from 'react-i18next'
  6. import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
  7. import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
  8. import cn from '@/utils/classnames'
  9. import ImagePreview from '@/app/components/base/image-uploader/image-preview'
  10. let mermaidAPI: any
  11. mermaidAPI = null
  12. if (typeof window !== 'undefined')
  13. mermaidAPI = mermaid.mermaidAPI
  14. const style = {
  15. minWidth: '480px',
  16. height: 'auto',
  17. overflow: 'auto',
  18. }
  19. const svgToBase64 = (svgGraph: string) => {
  20. const svgBytes = new TextEncoder().encode(svgGraph)
  21. const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
  22. return new Promise((resolve, reject) => {
  23. const reader = new FileReader()
  24. reader.onloadend = () => resolve(reader.result)
  25. reader.onerror = reject
  26. reader.readAsDataURL(blob)
  27. })
  28. }
  29. const Flowchart = React.forwardRef((props: {
  30. PrimitiveCode: string
  31. }, ref) => {
  32. const { t } = useTranslation()
  33. const [svgCode, setSvgCode] = useState(null)
  34. const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
  35. const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
  36. const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
  37. const [isLoading, setIsLoading] = useState(true)
  38. const timeRef = useRef<NodeJS.Timeout>()
  39. const [errMsg, setErrMsg] = useState('')
  40. const [imagePreviewUrl, setImagePreviewUrl] = useState('')
  41. const renderFlowchart = useCallback(async (PrimitiveCode: string) => {
  42. setSvgCode(null)
  43. setIsLoading(true)
  44. try {
  45. if (typeof window !== 'undefined' && mermaidAPI) {
  46. const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode)
  47. const base64Svg: any = await svgToBase64(svgGraph.svg)
  48. setSvgCode(base64Svg)
  49. setIsLoading(false)
  50. if (chartId.current && base64Svg)
  51. localStorage.setItem(chartId.current, base64Svg)
  52. }
  53. }
  54. catch (error) {
  55. if (prevPrimitiveCode === props.PrimitiveCode) {
  56. setIsLoading(false)
  57. setErrMsg((error as Error).message)
  58. }
  59. }
  60. }, [props.PrimitiveCode])
  61. useEffect(() => {
  62. if (typeof window !== 'undefined') {
  63. mermaid.initialize({
  64. startOnLoad: true,
  65. theme: 'neutral',
  66. look,
  67. flowchart: {
  68. htmlLabels: true,
  69. useMaxWidth: true,
  70. },
  71. })
  72. localStorage.removeItem(chartId.current)
  73. renderFlowchart(props.PrimitiveCode)
  74. }
  75. }, [look])
  76. useEffect(() => {
  77. const cachedSvg: any = localStorage.getItem(chartId.current)
  78. if (cachedSvg) {
  79. setSvgCode(cachedSvg)
  80. setIsLoading(false)
  81. return
  82. }
  83. if (timeRef.current)
  84. clearTimeout(timeRef.current)
  85. timeRef.current = setTimeout(() => {
  86. renderFlowchart(props.PrimitiveCode)
  87. }, 300)
  88. }, [props.PrimitiveCode])
  89. return (
  90. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  91. // @ts-expect-error
  92. <div ref={ref}>
  93. <div className="msh-segmented msh-segmented-sm css-23bs09 css-var-r1">
  94. <div className="msh-segmented-group">
  95. <label className="msh-segmented-item flex items-center space-x-1 m-2 w-[200px]">
  96. <div key='classic'
  97. className={cn('flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
  98. look === 'classic' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
  99. )}
  100. onClick={() => setLook('classic')}
  101. >
  102. <div className="msh-segmented-item-label">{t('app.mermaid.classic')}</div>
  103. </div>
  104. <div key='handDrawn'
  105. className={cn(
  106. 'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
  107. look === 'handDrawn' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
  108. )}
  109. onClick={() => setLook('handDrawn')}
  110. >
  111. <div className="msh-segmented-item-label">{t('app.mermaid.handDrawn')}</div>
  112. </div>
  113. </label>
  114. </div>
  115. </div>
  116. {
  117. svgCode
  118. && <div className="mermaid cursor-pointer" style={style} onClick={() => setImagePreviewUrl(svgCode)}>
  119. {svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="mermaid_chart" />}
  120. </div>
  121. }
  122. {isLoading
  123. && <div className='py-4 px-[26px]'>
  124. <LoadingAnim type='text'/>
  125. </div>
  126. }
  127. {
  128. errMsg
  129. && <div className='py-4 px-[26px]'>
  130. <ExclamationTriangleIcon className='w-6 h-6 text-red-500'/>
  131. &nbsp;
  132. {errMsg}
  133. </div>
  134. }
  135. {
  136. imagePreviewUrl && (<ImagePreview title='mermaid_chart' url={imagePreviewUrl} onCancel={() => setImagePreviewUrl('')} />)
  137. }
  138. </div>
  139. )
  140. })
  141. export default Flowchart