index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import type { FC, SVGProps } from 'react'
  2. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
  3. import useSWR from 'swr'
  4. import { useRouter } from 'next/navigation'
  5. import { useContext } from 'use-context-selector'
  6. import { useTranslation } from 'react-i18next'
  7. import { omit } from 'lodash-es'
  8. import { ArrowRightIcon } from '@heroicons/react/24/solid'
  9. import cn from 'classnames'
  10. import SegmentCard from '../completed/SegmentCard'
  11. import { FieldInfo } from '../metadata'
  12. import style from '../completed/style.module.css'
  13. import { DocumentContext } from '../index'
  14. import s from './style.module.css'
  15. import Button from '@/app/components/base/button'
  16. import Divider from '@/app/components/base/divider'
  17. import { ToastContext } from '@/app/components/base/toast'
  18. import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
  19. import type { CommonResponse } from '@/models/common'
  20. import { asyncRunSafe, sleep } from '@/utils'
  21. import { formatNumber } from '@/utils/format'
  22. import { fetchIndexingStatus as doFetchIndexingStatus, fetchIndexingEstimate, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
  23. import DatasetDetailContext from '@/context/dataset-detail'
  24. import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'
  25. type Props = {
  26. detail?: FullDocumentDetail
  27. stopPosition?: 'top' | 'bottom'
  28. datasetId?: string
  29. documentId?: string
  30. indexingType?: string
  31. detailUpdate: VoidFunction
  32. }
  33. const StopIcon = ({ className }: SVGProps<SVGElement>) => {
  34. return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
  35. <g clipPath="url(#clip0_2328_2798)">
  36. <path d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V8.1C10.5 8.94008 10.5 9.36012 10.3365 9.68099C10.1927 9.96323 9.96323 10.1927 9.68099 10.3365C9.36012 10.5 8.94008 10.5 8.1 10.5H3.9C3.05992 10.5 2.63988 10.5 2.31901 10.3365C2.03677 10.1927 1.8073 9.96323 1.66349 9.68099C1.5 9.36012 1.5 8.94008 1.5 8.1V3.9Z" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
  37. </g>
  38. <defs>
  39. <clipPath id="clip0_2328_2798">
  40. <rect width="12" height="12" fill="white" />
  41. </clipPath>
  42. </defs>
  43. </svg>
  44. }
  45. const ResumeIcon = ({ className }: SVGProps<SVGElement>) => {
  46. return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
  47. <path d="M10 3.5H5C3.34315 3.5 2 4.84315 2 6.5C2 8.15685 3.34315 9.5 5 9.5H10M10 3.5L8 1.5M10 3.5L8 5.5" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
  48. </svg>
  49. }
  50. const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ({ sourceData, docName }) => {
  51. const { t } = useTranslation()
  52. const segmentationRuleMap = {
  53. docName: t('datasetDocuments.embedding.docName'),
  54. mode: t('datasetDocuments.embedding.mode'),
  55. segmentLength: t('datasetDocuments.embedding.segmentLength'),
  56. textCleaning: t('datasetDocuments.embedding.textCleaning'),
  57. }
  58. const getRuleName = (key: string) => {
  59. if (key === 'remove_extra_spaces')
  60. return t('datasetCreation.stepTwo.removeExtraSpaces')
  61. if (key === 'remove_urls_emails')
  62. return t('datasetCreation.stepTwo.removeUrlEmails')
  63. if (key === 'remove_stopwords')
  64. return t('datasetCreation.stepTwo.removeStopwords')
  65. }
  66. const getValue = useCallback((field: string) => {
  67. let value: string | number | undefined = '-'
  68. switch (field) {
  69. case 'docName':
  70. value = docName
  71. break
  72. case 'mode':
  73. value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)
  74. break
  75. case 'segmentLength':
  76. value = sourceData?.rules?.segmentation?.max_tokens
  77. break
  78. default:
  79. value = sourceData?.mode === 'automatic'
  80. ? (t('datasetDocuments.embedding.automatic') as string)
  81. // eslint-disable-next-line array-callback-return
  82. : sourceData?.rules?.pre_processing_rules?.map((rule) => {
  83. if (rule.enabled)
  84. return getRuleName(rule.id)
  85. }).filter(Boolean).join(';')
  86. break
  87. }
  88. return value
  89. }, [sourceData, docName])
  90. return <div className='flex flex-col pt-8 pb-10 first:mt-0'>
  91. {Object.keys(segmentationRuleMap).map((field) => {
  92. return <FieldInfo
  93. key={field}
  94. label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}
  95. displayedValue={String(getValue(field))}
  96. />
  97. })}
  98. </div>
  99. }
  100. const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: dstId, documentId: docId, indexingType, detailUpdate }) => {
  101. const onTop = stopPosition === 'top'
  102. const { t } = useTranslation()
  103. const { notify } = useContext(ToastContext)
  104. const { datasetId = '', documentId = '' } = useContext(DocumentContext)
  105. const { indexingTechnique } = useContext(DatasetDetailContext)
  106. const localDatasetId = dstId ?? datasetId
  107. const localDocumentId = docId ?? documentId
  108. const localIndexingTechnique = indexingType ?? indexingTechnique
  109. const [indexingStatusDetail, setIndexingStatusDetail] = useState<any>(null)
  110. const fetchIndexingStatus = async () => {
  111. const status = await doFetchIndexingStatus({ datasetId: localDatasetId, documentId: localDocumentId })
  112. setIndexingStatusDetail(status)
  113. return status
  114. }
  115. const isStopQuery = useRef(false)
  116. const stopQueryStatus = useCallback(() => {
  117. isStopQuery.current = true
  118. }, [])
  119. const startQueryStatus = useCallback(async () => {
  120. if (isStopQuery.current)
  121. return
  122. try {
  123. const indexingStatusDetail = await fetchIndexingStatus()
  124. if (['completed', 'error', 'paused'].includes(indexingStatusDetail?.indexing_status)) {
  125. stopQueryStatus()
  126. detailUpdate()
  127. return
  128. }
  129. await sleep(2500)
  130. await startQueryStatus()
  131. }
  132. catch (e) {
  133. await sleep(2500)
  134. await startQueryStatus()
  135. }
  136. }, [stopQueryStatus])
  137. useEffect(() => {
  138. isStopQuery.current = false
  139. startQueryStatus()
  140. return () => {
  141. stopQueryStatus()
  142. }
  143. }, [startQueryStatus, stopQueryStatus])
  144. const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({
  145. action: 'fetchIndexingEstimate',
  146. datasetId: localDatasetId,
  147. documentId: localDocumentId,
  148. }, apiParams => fetchIndexingEstimate(omit(apiParams, 'action')), {
  149. revalidateOnFocus: false,
  150. })
  151. const { data: ruleDetail, error: ruleError } = useSWR({
  152. action: 'fetchProcessRule',
  153. params: { documentId: localDocumentId },
  154. }, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
  155. revalidateOnFocus: false,
  156. })
  157. const [showModal, setShowModal] = useState(false)
  158. const modalShowHandle = () => setShowModal(true)
  159. const modalCloseHandle = () => setShowModal(false)
  160. const router = useRouter()
  161. const navToDocument = () => {
  162. router.push(`/datasets/${localDatasetId}/documents/${localDocumentId}`)
  163. }
  164. const isEmbedding = useMemo(() => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
  165. const isEmbeddingCompleted = useMemo(() => ['completed'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
  166. const isEmbeddingPaused = useMemo(() => ['paused'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
  167. const isEmbeddingError = useMemo(() => ['error'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
  168. const percent = useMemo(() => {
  169. const completedCount = indexingStatusDetail?.completed_segments || 0
  170. const totalCount = indexingStatusDetail?.total_segments || 0
  171. if (totalCount === 0)
  172. return 0
  173. const percent = Math.round(completedCount * 100 / totalCount)
  174. return percent > 100 ? 100 : percent
  175. }, [indexingStatusDetail])
  176. const handleSwitch = async () => {
  177. const opApi = isEmbedding ? pauseDocIndexing : resumeDocIndexing
  178. const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>)
  179. if (!e) {
  180. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  181. setIndexingStatusDetail(null)
  182. }
  183. else {
  184. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  185. }
  186. }
  187. // if (!ruleDetail && !error)
  188. // return <Loading type='app' />
  189. return (
  190. <>
  191. <div className={s.embeddingStatus}>
  192. {isEmbedding && t('datasetDocuments.embedding.processing')}
  193. {isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
  194. {isEmbeddingPaused && t('datasetDocuments.embedding.paused')}
  195. {isEmbeddingError && t('datasetDocuments.embedding.error')}
  196. {onTop && isEmbedding && (
  197. <Button onClick={handleSwitch} className={s.opBtn}>
  198. <StopIcon className={s.opIcon} />
  199. {t('datasetDocuments.embedding.stop')}
  200. </Button>
  201. )}
  202. {onTop && isEmbeddingPaused && (
  203. <Button onClick={handleSwitch} className={s.opBtn}>
  204. <ResumeIcon className={s.opIcon} />
  205. {t('datasetDocuments.embedding.resume')}
  206. </Button>
  207. )}
  208. </div>
  209. {/* progress bar */}
  210. <div className={s.progressContainer}>
  211. {new Array(10).fill('').map((_, idx) => <div
  212. key={idx}
  213. className={cn(s.progressBgItem, isEmbedding ? 'bg-primary-50' : 'bg-gray-100')}
  214. />)}
  215. <div
  216. className={cn(
  217. 'rounded-l-md',
  218. s.progressBar,
  219. (isEmbedding || isEmbeddingCompleted) && s.barProcessing,
  220. (isEmbeddingPaused || isEmbeddingError) && s.barPaused,
  221. indexingStatusDetail?.indexing_status === 'completed' && 'rounded-r-md',
  222. )}
  223. style={{ width: `${percent}%` }}
  224. />
  225. </div>
  226. <div className={s.progressData}>
  227. <div>{t('datasetDocuments.embedding.segments')} {indexingStatusDetail?.completed_segments}/{indexingStatusDetail?.total_segments} · {percent}%</div>
  228. {localIndexingTechnique === 'high_quaility' && (
  229. <div className='flex items-center'>
  230. <div className={cn(s.commonIcon, s.highIcon)} />
  231. {t('datasetDocuments.embedding.highQuality')} · {t('datasetDocuments.embedding.estimate')}
  232. <span className={s.tokens}>{formatNumber(indexingEstimateDetail?.tokens || 0)}</span>tokens
  233. (<span className={s.price}>${formatNumber(indexingEstimateDetail?.total_price || 0)}</span>)
  234. </div>
  235. )}
  236. {localIndexingTechnique === 'economy' && (
  237. <div className='flex items-center'>
  238. <div className={cn(s.commonIcon, s.economyIcon)} />
  239. {t('datasetDocuments.embedding.economy')} · {t('datasetDocuments.embedding.estimate')}
  240. <span className={s.tokens}>0</span>tokens
  241. </div>
  242. )}
  243. </div>
  244. <RuleDetail sourceData={ruleDetail} docName={detail?.name} />
  245. {!onTop && (
  246. <div className='flex items-center gap-2 mt-10'>
  247. {isEmbedding && (
  248. <Button onClick={modalShowHandle} className='w-fit'>
  249. {t('datasetCreation.stepThree.stop')}
  250. </Button>
  251. )}
  252. {isEmbeddingPaused && (
  253. <Button onClick={handleSwitch} className='w-fit'>
  254. {t('datasetCreation.stepThree.resume')}
  255. </Button>
  256. )}
  257. <Button className='w-fit' variant='primary' onClick={navToDocument}>
  258. <span>{t('datasetCreation.stepThree.navTo')}</span>
  259. <ArrowRightIcon className='h-4 w-4 ml-2 stroke-current stroke-1' />
  260. </Button>
  261. </div>
  262. )}
  263. {onTop && <>
  264. <Divider />
  265. <div className={s.previewTip}>{t('datasetDocuments.embedding.previewTip')}</div>
  266. <div className={style.cardWrapper}>
  267. {[1, 2, 3].map((v, index) => (
  268. <SegmentCard key={index} loading={true} detail={{ position: v } as any} />
  269. ))}
  270. </div>
  271. </>}
  272. <StopEmbeddingModal show={showModal} onConfirm={handleSwitch} onHide={modalCloseHandle} />
  273. </>
  274. )
  275. }
  276. export default React.memo(EmbeddingDetail)