| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 | import type { FC, SVGProps } from 'react'import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'import useSWR from 'swr'import { useRouter } from 'next/navigation'import { useContext } from 'use-context-selector'import { useTranslation } from 'react-i18next'import { omit } from 'lodash-es'import { ArrowRightIcon } from '@heroicons/react/24/solid'import SegmentCard from '../completed/SegmentCard'import { FieldInfo } from '../metadata'import style from '../completed/style.module.css'import { DocumentContext } from '../index'import s from './style.module.css'import cn from '@/utils/classnames'import Button from '@/app/components/base/button'import Divider from '@/app/components/base/divider'import { ToastContext } from '@/app/components/base/toast'import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'import type { CommonResponse } from '@/models/common'import { asyncRunSafe, sleep } from '@/utils'import { fetchIndexingStatus as doFetchIndexingStatus, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'type Props = {  detail?: FullDocumentDetail  stopPosition?: 'top' | 'bottom'  datasetId?: string  documentId?: string  indexingType?: string  detailUpdate: VoidFunction}const StopIcon = ({ className }: SVGProps<SVGElement>) => {  return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>    <g clipPath="url(#clip0_2328_2798)">      <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" />    </g>    <defs>      <clipPath id="clip0_2328_2798">        <rect width="12" height="12" fill="white" />      </clipPath>    </defs>  </svg>}const ResumeIcon = ({ className }: SVGProps<SVGElement>) => {  return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>    <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" />  </svg>}const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ({ sourceData, docName }) => {  const { t } = useTranslation()  const segmentationRuleMap = {    docName: t('datasetDocuments.embedding.docName'),    mode: t('datasetDocuments.embedding.mode'),    segmentLength: t('datasetDocuments.embedding.segmentLength'),    textCleaning: t('datasetDocuments.embedding.textCleaning'),  }  const getRuleName = (key: string) => {    if (key === 'remove_extra_spaces')      return t('datasetCreation.stepTwo.removeExtraSpaces')    if (key === 'remove_urls_emails')      return t('datasetCreation.stepTwo.removeUrlEmails')    if (key === 'remove_stopwords')      return t('datasetCreation.stepTwo.removeStopwords')  }  const getValue = useCallback((field: string) => {    let value: string | number | undefined = '-'    switch (field) {      case 'docName':        value = docName        break      case 'mode':        value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)        break      case 'segmentLength':        value = sourceData?.rules?.segmentation?.max_tokens        break      default:        value = sourceData?.mode === 'automatic'          ? (t('datasetDocuments.embedding.automatic') as string)          // eslint-disable-next-line array-callback-return          : sourceData?.rules?.pre_processing_rules?.map((rule) => {            if (rule.enabled)              return getRuleName(rule.id)          }).filter(Boolean).join(';')        break    }    return value  }, [sourceData, docName])  return <div className='flex flex-col pt-8 pb-10 first:mt-0'>    {Object.keys(segmentationRuleMap).map((field) => {      return <FieldInfo        key={field}        label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}        displayedValue={String(getValue(field))}      />    })}  </div>}const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: dstId, documentId: docId, detailUpdate }) => {  const onTop = stopPosition === 'top'  const { t } = useTranslation()  const { notify } = useContext(ToastContext)  const { datasetId = '', documentId = '' } = useContext(DocumentContext)  const localDatasetId = dstId ?? datasetId  const localDocumentId = docId ?? documentId  const [indexingStatusDetail, setIndexingStatusDetail] = useState<any>(null)  const fetchIndexingStatus = async () => {    const status = await doFetchIndexingStatus({ datasetId: localDatasetId, documentId: localDocumentId })    setIndexingStatusDetail(status)    return status  }  const isStopQuery = useRef(false)  const stopQueryStatus = useCallback(() => {    isStopQuery.current = true  }, [])  const startQueryStatus = useCallback(async () => {    if (isStopQuery.current)      return    try {      const indexingStatusDetail = await fetchIndexingStatus()      if (['completed', 'error', 'paused'].includes(indexingStatusDetail?.indexing_status)) {        stopQueryStatus()        detailUpdate()        return      }      await sleep(2500)      await startQueryStatus()    }    catch (e) {      await sleep(2500)      await startQueryStatus()    }  }, [stopQueryStatus])  useEffect(() => {    isStopQuery.current = false    startQueryStatus()    return () => {      stopQueryStatus()    }  }, [startQueryStatus, stopQueryStatus])  const { data: ruleDetail, error: ruleError } = useSWR({    action: 'fetchProcessRule',    params: { documentId: localDocumentId },  }, apiParams => fetchProcessRule(omit(apiParams, 'action')), {    revalidateOnFocus: false,  })  const [showModal, setShowModal] = useState(false)  const modalShowHandle = () => setShowModal(true)  const modalCloseHandle = () => setShowModal(false)  const router = useRouter()  const navToDocument = () => {    router.push(`/datasets/${localDatasetId}/documents/${localDocumentId}`)  }  const isEmbedding = useMemo(() => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])  const isEmbeddingCompleted = useMemo(() => ['completed'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])  const isEmbeddingPaused = useMemo(() => ['paused'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])  const isEmbeddingError = useMemo(() => ['error'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])  const percent = useMemo(() => {    const completedCount = indexingStatusDetail?.completed_segments || 0    const totalCount = indexingStatusDetail?.total_segments || 0    if (totalCount === 0)      return 0    const percent = Math.round(completedCount * 100 / totalCount)    return percent > 100 ? 100 : percent  }, [indexingStatusDetail])  const handleSwitch = async () => {    const opApi = isEmbedding ? pauseDocIndexing : resumeDocIndexing    const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>)    if (!e) {      notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })      setIndexingStatusDetail(null)    }    else {      notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })    }  }  // if (!ruleDetail && !error)  //   return <Loading type='app' />  return (    <>      <div className={s.embeddingStatus}>        {isEmbedding && t('datasetDocuments.embedding.processing')}        {isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}        {isEmbeddingPaused && t('datasetDocuments.embedding.paused')}        {isEmbeddingError && t('datasetDocuments.embedding.error')}        {onTop && isEmbedding && (          <Button onClick={handleSwitch} className={s.opBtn}>            <StopIcon className={s.opIcon} />            {t('datasetDocuments.embedding.stop')}          </Button>        )}        {onTop && isEmbeddingPaused && (          <Button onClick={handleSwitch} className={s.opBtn}>            <ResumeIcon className={s.opIcon} />            {t('datasetDocuments.embedding.resume')}          </Button>        )}      </div>      {/* progress bar */}      <div className={s.progressContainer}>        {new Array(10).fill('').map((_, idx) => <div          key={idx}          className={cn(s.progressBgItem, isEmbedding ? 'bg-primary-50' : 'bg-gray-100')}        />)}        <div          className={cn(            'rounded-l-md',            s.progressBar,            (isEmbedding || isEmbeddingCompleted) && s.barProcessing,            (isEmbeddingPaused || isEmbeddingError) && s.barPaused,            indexingStatusDetail?.indexing_status === 'completed' && 'rounded-r-md',          )}          style={{ width: `${percent}%` }}        />      </div>      <div className={s.progressData}>        <div>{t('datasetDocuments.embedding.segments')} {indexingStatusDetail?.completed_segments}/{indexingStatusDetail?.total_segments} · {percent}%</div>      </div>      <RuleDetail sourceData={ruleDetail} docName={detail?.name} />      {!onTop && (        <div className='flex items-center gap-2 mt-10'>          {isEmbedding && (            <Button onClick={modalShowHandle} className='w-fit'>              {t('datasetCreation.stepThree.stop')}            </Button>          )}          {isEmbeddingPaused && (            <Button onClick={handleSwitch} className='w-fit'>              {t('datasetCreation.stepThree.resume')}            </Button>          )}          <Button className='w-fit' variant='primary' onClick={navToDocument}>            <span>{t('datasetCreation.stepThree.navTo')}</span>            <ArrowRightIcon className='h-4 w-4 ml-2 stroke-current stroke-1' />          </Button>        </div>      )}      {onTop && <>        <Divider />        <div className={s.previewTip}>{t('datasetDocuments.embedding.previewTip')}</div>        <div className={style.cardWrapper}>          {[1, 2, 3].map((v, index) => (            <SegmentCard key={index} loading={true} detail={{ position: v } as any} />          ))}        </div>      </>}      <StopEmbeddingModal show={showModal} onConfirm={handleSwitch} onHide={modalCloseHandle} />    </>  )}export default React.memo(EmbeddingDetail)
 |