index.tsx 4.9 KB

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