'use client' import type { FC } from 'react' import type { DataSourceInfo, DocumentDisplayStatus, FileItem, FullDocumentDetail, LegacyDataSourceInfo } from '@/models/datasets' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import FloatRightContainer from '@/app/components/base/float-right-container' import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' import Metadata from '@/app/components/datasets/metadata/metadata-document' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { ChunkingMode, DisplayStatusList } from '@/models/datasets' import { useRouter, useSearchParams } from '@/next/navigation' import { useDocumentDetail, useDocumentMetadata, useInvalidDocumentList } from '@/service/knowledge/use-document' import { useCheckSegmentBatchImportProgress, useChildSegmentListKey, useSegmentBatchImport, useSegmentListKey } from '@/service/knowledge/use-segment' import { useInvalid } from '@/service/use-base' import { cn } from '@/utils/classnames' import Operations from '../components/operations' import StatusItem from '../status-item' import BatchModal from './batch-modal' import Completed from './completed' import { DocumentContext } from './context' import { DocumentTitle } from './document-title' import Embedding from './embedding' import SegmentAdd, { ProcessStatus } from './segment-add' import style from './style.module.css' type DocumentDetailProps = { datasetId: string documentId: string } const NON_TERMINAL_DISPLAY_STATUSES = new Set( DisplayStatusList.filter(s => s === 'queuing' || s === 'indexing' || s === 'paused'), ) const isLegacyDataSourceInfo = (info?: DataSourceInfo): info is LegacyDataSourceInfo => { return !!info && 'upload_file' in info } const DocumentDetail: FC = ({ datasetId, documentId }) => { const router = useRouter() const searchParams = useSearchParams() const { t } = useTranslation() const media = useBreakpoints() const isMobile = media === MediaType.mobile const dataset = useDatasetDetailContextWithSelector(s => s.dataset) const embeddingAvailable = !!dataset?.embedding_available const [showMetadata, setShowMetadata] = useState(!isMobile) const [newSegmentModalVisible, setNewSegmentModalVisible] = useState(false) const [batchModalVisible, setBatchModalVisible] = useState(false) const [importStatus, setImportStatus] = useState() const showNewSegmentModal = () => setNewSegmentModalVisible(true) const showBatchModal = () => setBatchModalVisible(true) const hideBatchModal = () => setBatchModalVisible(false) const resetProcessStatus = () => setImportStatus('') const { mutateAsync: checkSegmentBatchImportProgress } = useCheckSegmentBatchImportProgress() const checkProcess = async (jobID: string) => { await checkSegmentBatchImportProgress({ jobID }, { onSuccess: (res) => { setImportStatus(res.job_status) if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING) setTimeout(() => checkProcess(res.job_id), 2500) if (res.job_status === ProcessStatus.ERROR) Toast.notify({ type: 'error', message: `${t('list.batchModal.runError', { ns: 'datasetDocuments' })}` }) }, onError: (e) => { const message = 'message' in e ? `: ${e.message}` : '' Toast.notify({ type: 'error', message: `${t('list.batchModal.runError', { ns: 'datasetDocuments' })}${message}` }) }, }) } const { mutateAsync: segmentBatchImport } = useSegmentBatchImport() const runBatch = async (csv: FileItem) => { await segmentBatchImport({ url: `/datasets/${datasetId}/documents/${documentId}/segments/batch_import`, body: { upload_file_id: csv.file.id! }, }, { onSuccess: (res) => { setImportStatus(res.job_status) checkProcess(res.job_id) }, onError: (e) => { const message = 'message' in e ? `: ${e.message}` : '' Toast.notify({ type: 'error', message: `${t('list.batchModal.runError', { ns: 'datasetDocuments' })}${message}` }) }, }) } const { data: documentDetail, error, refetch: detailMutate } = useDocumentDetail({ datasetId, documentId, params: { metadata: 'without' }, refetchInterval: (query) => { const status = query.state.data?.display_status if (!status || NON_TERMINAL_DISPLAY_STATUSES.has(status)) return 2500 return false }, }) const { data: documentMetadata } = useDocumentMetadata({ datasetId, documentId, params: { metadata: 'only' }, }) const backToPrev = useCallback(() => { const queryString = searchParams.toString() const backPath = `/datasets/${datasetId}/documents${queryString ? `?${queryString}` : ''}` router.push(backPath) }, [searchParams, datasetId, router]) const isDetailLoading = !documentDetail && !error const embedding = NON_TERMINAL_DISPLAY_STATUSES.has(documentDetail?.display_status as DocumentDisplayStatus) const documentUploadFile = useMemo(() => { if (!documentDetail?.data_source_info) return undefined if (isLegacyDataSourceInfo(documentDetail.data_source_info)) return documentDetail.data_source_info.upload_file return undefined }, [documentDetail?.data_source_info]) const invalidChunkList = useInvalid(useSegmentListKey) const invalidChildChunkList = useInvalid(useChildSegmentListKey) const invalidDocumentList = useInvalidDocumentList(datasetId) const handleOperate = useCallback((operateName?: string) => { invalidDocumentList() if (operateName === 'delete') { backToPrev() } else { detailMutate() // If operation is not rename, refresh the chunk list after 5 seconds if (operateName) { setTimeout(() => { invalidChunkList() invalidChildChunkList() }, 5000) } } }, [invalidDocumentList, backToPrev, detailMutate, invalidChunkList, invalidChildChunkList]) const parentMode = useMemo(() => { return documentDetail?.document_process_rule?.rules?.parent_mode || documentDetail?.dataset_process_rule?.rules?.parent_mode || 'paragraph' }, [documentDetail?.document_process_rule?.rules?.parent_mode, documentDetail?.dataset_process_rule?.rules?.parent_mode]) const isFullDocMode = useMemo(() => { const chunkMode = documentDetail?.doc_form return chunkMode === ChunkingMode.parentChild && parentMode === 'full-doc' }, [documentDetail?.doc_form, parentMode]) const contextValue = useMemo(() => ({ datasetId, documentId, docForm: documentDetail?.doc_form as ChunkingMode, parentMode, }), [datasetId, documentId, documentDetail?.doc_form, parentMode]) const statusDetail = useMemo(() => ({ enabled: documentDetail?.enabled || false, archived: documentDetail?.archived || false, id: documentId, }), [documentDetail?.enabled, documentDetail?.archived, documentId]) const operationsDetail = useMemo(() => ({ name: documentDetail?.name || '', enabled: documentDetail?.enabled || false, archived: documentDetail?.archived || false, id: documentId, data_source_type: documentDetail?.data_source_type || '', doc_form: documentDetail?.doc_form || '', }), [documentDetail?.name, documentDetail?.enabled, documentDetail?.archived, documentId, documentDetail?.data_source_type, documentDetail?.doc_form]) const docDetail = useMemo(() => ({ ...documentDetail, ...documentMetadata, doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type, } as FullDocumentDetail), [documentDetail, documentMetadata]) const backButtonLabel = t('operation.back', { ns: 'common' }) const metadataToggleLabel = `${showMetadata ? t('operation.close', { ns: 'common' }) : t('operation.view', { ns: 'common' })} ${t('metadata.title', { ns: 'datasetDocuments' })}` return (
{embeddingAvailable && documentDetail && !documentDetail.archived && !isFullDocMode && ( <> )} {documentDetail && ( )}
{isDetailLoading ? : (
{embedding ? ( ) : ( )}
)} setShowMetadata(false)} isMobile={isMobile} panelClassName="!justify-start" footer={null}>
) } export default DocumentDetail