index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useState } from 'react'
  4. import useSWR from 'swr'
  5. import { ArrowLeftIcon } from '@heroicons/react/24/solid'
  6. import { createContext, useContext } from 'use-context-selector'
  7. import { useTranslation } from 'react-i18next'
  8. import { useRouter } from 'next/navigation'
  9. import { omit } from 'lodash-es'
  10. import { OperationAction, StatusItem } from '../list'
  11. import s from '../style.module.css'
  12. import Completed from './completed'
  13. import Embedding from './embedding'
  14. import Metadata from './metadata'
  15. import SegmentAdd, { ProcessStatus } from './segment-add'
  16. import BatchModal from './batch-modal'
  17. import style from './style.module.css'
  18. import cn from '@/utils/classnames'
  19. import Divider from '@/app/components/base/divider'
  20. import Loading from '@/app/components/base/loading'
  21. import type { MetadataType } from '@/service/datasets'
  22. import { checkSegmentBatchImportProgress, fetchDocumentDetail, segmentBatchImport } from '@/service/datasets'
  23. import { ToastContext } from '@/app/components/base/toast'
  24. import type { DocForm } from '@/models/datasets'
  25. import { useDatasetDetailContext } from '@/context/dataset-detail'
  26. import FloatRightContainer from '@/app/components/base/float-right-container'
  27. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  28. export const DocumentContext = createContext<{ datasetId?: string; documentId?: string; docForm: string }>({ docForm: '' })
  29. type DocumentTitleProps = {
  30. extension?: string
  31. name?: string
  32. iconCls?: string
  33. textCls?: string
  34. wrapperCls?: string
  35. }
  36. export const DocumentTitle: FC<DocumentTitleProps> = ({ extension, name, iconCls, textCls, wrapperCls }) => {
  37. const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase()
  38. return <div className={cn('flex items-center justify-start flex-1', wrapperCls)}>
  39. <div className={cn(s[`${localExtension || 'txt'}Icon`], style.titleIcon, iconCls)}></div>
  40. <span className={cn('font-semibold text-lg text-gray-900 ml-1', textCls)}> {name || '--'}</span>
  41. </div>
  42. }
  43. type Props = {
  44. datasetId: string
  45. documentId: string
  46. }
  47. const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
  48. const router = useRouter()
  49. const { t } = useTranslation()
  50. const media = useBreakpoints()
  51. const isMobile = media === MediaType.mobile
  52. const { notify } = useContext(ToastContext)
  53. const { dataset } = useDatasetDetailContext()
  54. const embeddingAvailable = !!dataset?.embedding_available
  55. const [showMetadata, setShowMetadata] = useState(!isMobile)
  56. const [newSegmentModalVisible, setNewSegmentModalVisible] = useState(false)
  57. const [batchModalVisible, setBatchModalVisible] = useState(false)
  58. const [importStatus, setImportStatus] = useState<ProcessStatus | string>()
  59. const showNewSegmentModal = () => setNewSegmentModalVisible(true)
  60. const showBatchModal = () => setBatchModalVisible(true)
  61. const hideBatchModal = () => setBatchModalVisible(false)
  62. const resetProcessStatus = () => setImportStatus('')
  63. const checkProcess = async (jobID: string) => {
  64. try {
  65. const res = await checkSegmentBatchImportProgress({ jobID })
  66. setImportStatus(res.job_status)
  67. if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
  68. setTimeout(() => checkProcess(res.job_id), 2500)
  69. if (res.job_status === ProcessStatus.ERROR)
  70. notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}` })
  71. }
  72. catch (e: any) {
  73. notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
  74. }
  75. }
  76. const runBatch = async (csv: File) => {
  77. const formData = new FormData()
  78. formData.append('file', csv)
  79. try {
  80. const res = await segmentBatchImport({
  81. url: `/datasets/${datasetId}/documents/${documentId}/segments/batch_import`,
  82. body: formData,
  83. })
  84. setImportStatus(res.job_status)
  85. checkProcess(res.job_id)
  86. }
  87. catch (e: any) {
  88. notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
  89. }
  90. }
  91. const { data: documentDetail, error, mutate: detailMutate } = useSWR({
  92. action: 'fetchDocumentDetail',
  93. datasetId,
  94. documentId,
  95. params: { metadata: 'without' as MetadataType },
  96. }, apiParams => fetchDocumentDetail(omit(apiParams, 'action')))
  97. const { data: documentMetadata, error: metadataErr, mutate: metadataMutate } = useSWR({
  98. action: 'fetchDocumentDetail',
  99. datasetId,
  100. documentId,
  101. params: { metadata: 'only' as MetadataType },
  102. }, apiParams => fetchDocumentDetail(omit(apiParams, 'action')),
  103. )
  104. const backToPrev = () => {
  105. router.push(`/datasets/${datasetId}/documents`)
  106. }
  107. const isDetailLoading = !documentDetail && !error
  108. const isMetadataLoading = !documentMetadata && !metadataErr
  109. const embedding = ['queuing', 'indexing', 'paused'].includes((documentDetail?.display_status || '').toLowerCase())
  110. const handleOperate = (operateName?: string) => {
  111. if (operateName === 'delete')
  112. backToPrev()
  113. else
  114. detailMutate()
  115. }
  116. return (
  117. <DocumentContext.Provider value={{ datasetId, documentId, docForm: documentDetail?.doc_form || '' }}>
  118. <div className='flex flex-col h-full'>
  119. <div className='flex min-h-16 border-b-gray-100 border-b items-center p-4 justify-between flex-wrap gap-y-2'>
  120. <div onClick={backToPrev} className={'shrink-0 rounded-full w-8 h-8 flex justify-center items-center border-gray-100 cursor-pointer border hover:border-gray-300 shadow-[0px_12px_16px_-4px_rgba(16,24,40,0.08),0px_4px_6px_-2px_rgba(16,24,40,0.03)]'}>
  121. <ArrowLeftIcon className='text-primary-600 fill-current stroke-current h-4 w-4' />
  122. </div>
  123. <Divider className='!h-4' type='vertical' />
  124. <DocumentTitle extension={documentDetail?.data_source_info?.upload_file?.extension} name={documentDetail?.name} />
  125. <div className='flex items-center flex-wrap gap-y-2'>
  126. <StatusItem status={documentDetail?.display_status || 'available'} scene='detail' errorMessage={documentDetail?.error || ''} />
  127. {embeddingAvailable && documentDetail && !documentDetail.archived && (
  128. <SegmentAdd
  129. importStatus={importStatus}
  130. clearProcessStatus={resetProcessStatus}
  131. showNewSegmentModal={showNewSegmentModal}
  132. showBatchModal={showBatchModal}
  133. />
  134. )}
  135. <OperationAction
  136. scene='detail'
  137. embeddingAvailable={embeddingAvailable}
  138. detail={{
  139. name: documentDetail?.name || '',
  140. enabled: documentDetail?.enabled || false,
  141. archived: documentDetail?.archived || false,
  142. id: documentId,
  143. data_source_type: documentDetail?.data_source_type || '',
  144. doc_form: documentDetail?.doc_form || '',
  145. }}
  146. datasetId={datasetId}
  147. onUpdate={handleOperate}
  148. className='!w-[216px]'
  149. />
  150. <button
  151. className={cn(style.layoutRightIcon, showMetadata ? style.iconShow : style.iconClose)}
  152. onClick={() => setShowMetadata(!showMetadata)}
  153. />
  154. </div>
  155. </div>
  156. <div className='flex flex-row flex-1' style={{ height: 'calc(100% - 4rem)' }}>
  157. {isDetailLoading
  158. ? <Loading type='app' />
  159. : <div className={`h-full w-full flex flex-col ${embedding ? 'px-6 py-3 sm:py-12 sm:px-16' : 'pb-[30px] pt-3 px-6'}`}>
  160. {embedding
  161. ? <Embedding detail={documentDetail} detailUpdate={detailMutate} />
  162. : <Completed
  163. embeddingAvailable={embeddingAvailable}
  164. showNewSegmentModal={newSegmentModalVisible}
  165. onNewSegmentModalChange={setNewSegmentModalVisible}
  166. importStatus={importStatus}
  167. archived={documentDetail?.archived}
  168. />
  169. }
  170. </div>
  171. }
  172. <FloatRightContainer showClose isOpen={showMetadata} onClose={() => setShowMetadata(false)} isMobile={isMobile} panelClassname='!justify-start' footer={null}>
  173. <Metadata
  174. docDetail={{ ...documentDetail, ...documentMetadata, doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type } as any}
  175. loading={isMetadataLoading}
  176. onUpdate={metadataMutate}
  177. />
  178. </FloatRightContainer>
  179. </div>
  180. <BatchModal
  181. isShow={batchModalVisible}
  182. onCancel={hideBatchModal}
  183. onConfirm={runBatch}
  184. docForm={documentDetail?.doc_form as DocForm}
  185. />
  186. </div>
  187. </DocumentContext.Provider>
  188. )
  189. }
  190. export default DocumentDetail