index.tsx 5.0 KB

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