'use client' import type { FC } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useBoolean, useDebounceFn } from 'ahooks' import { ArrowDownIcon } from '@heroicons/react/24/outline' import { pick, uniq } from 'lodash-es' import { RiArchive2Line, RiDeleteBinLine, RiEditLine, RiEqualizer2Line, RiLoopLeftLine, RiMoreFill, } from '@remixicon/react' import { useContext } from 'use-context-selector' import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' import { Globe01 } from '../../base/icons/src/vender/line/mapsAndTravel' import ChunkingModeLabel from '../common/chunking-mode-label' import FileTypeIcon from '../../base/file-uploader/file-type-icon' import s from './style.module.css' import RenameModal from './rename-modal' import BatchAction from './detail/completed/common/batch-action' import cn from '@/utils/classnames' import Switch from '@/app/components/base/switch' import Divider from '@/app/components/base/divider' import Popover from '@/app/components/base/popover' import Confirm from '@/app/components/base/confirm' import Tooltip from '@/app/components/base/tooltip' import Toast, { ToastContext } from '@/app/components/base/toast' import type { ColorMap, IndicatorProps } from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator' import { asyncRunSafe } from '@/utils' import { formatNumber } from '@/utils/format' import NotionIcon from '@/app/components/base/notion-icon' import ProgressBar from '@/app/components/base/progress-bar' import { ChunkingMode, DataSourceType, DocumentActionType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets' import type { CommonResponse } from '@/models/common' import useTimestamp from '@/hooks/use-timestamp' import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '@/context/dataset-detail' import type { Props as PaginationProps } from '@/app/components/base/pagination' import Pagination from '@/app/components/base/pagination' import Checkbox from '@/app/components/base/checkbox' import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document' import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata' import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal' export const useIndexStatus = () => { const { t } = useTranslation() return { queuing: { color: 'orange', text: t('datasetDocuments.list.status.queuing') }, // waiting indexing: { color: 'blue', text: t('datasetDocuments.list.status.indexing') }, // indexing splitting parsing cleaning paused: { color: 'orange', text: t('datasetDocuments.list.status.paused') }, // paused error: { color: 'red', text: t('datasetDocuments.list.status.error') }, // error available: { color: 'green', text: t('datasetDocuments.list.status.available') }, // completed,archived = false,enabled = true enabled: { color: 'green', text: t('datasetDocuments.list.status.enabled') }, // completed,archived = false,enabled = true disabled: { color: 'gray', text: t('datasetDocuments.list.status.disabled') }, // completed,archived = false,enabled = false archived: { color: 'gray', text: t('datasetDocuments.list.status.archived') }, // completed,archived = true } } const STATUS_TEXT_COLOR_MAP: ColorMap = { green: 'text-util-colors-green-green-600', orange: 'text-util-colors-warning-warning-600', red: 'text-util-colors-red-red-600', blue: 'text-util-colors-blue-light-blue-light-600', yellow: 'text-util-colors-warning-warning-600', gray: 'text-text-tertiary', } // status item for list export const StatusItem: FC<{ status: DocumentDisplayStatus reverse?: boolean scene?: 'list' | 'detail' textCls?: string errorMessage?: string detail?: { enabled: boolean archived: boolean id: string } datasetId?: string onUpdate?: (operationName?: string) => void }> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage, datasetId = '', detail, onUpdate }) => { const DOC_INDEX_STATUS_MAP = useIndexStatus() const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP const { enabled = false, archived = false, id = '' } = detail || {} const { notify } = useContext(ToastContext) const { t } = useTranslation() const { mutateAsync: enableDocument } = useDocumentEnable() const { mutateAsync: disableDocument } = useDocumentDisable() const { mutateAsync: deleteDocument } = useDocumentDelete() const onOperate = async (operationName: OperationName) => { let opApi = deleteDocument switch (operationName) { case 'enable': opApi = enableDocument break case 'disable': opApi = disableDocument break } const [e] = await asyncRunSafe(opApi({ datasetId, documentId: id }) as Promise) if (!e) { notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) onUpdate?.() // onUpdate?.(operationName) } else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } } const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => { if (operationName === 'enable' && enabled) return if (operationName === 'disable' && !enabled) return onOperate(operationName) }, { wait: 500 }) const embedding = useMemo(() => { return ['queuing', 'indexing', 'paused'].includes(localStatus) }, [localStatus]) return
{DOC_INDEX_STATUS_MAP[localStatus]?.text} { errorMessage && ( {errorMessage}
} triggerClassName='ml-1 w-4 h-4' /> ) } { scene === 'detail' && (
!archived && handleSwitch(v ? 'enable' : 'disable')} disabled={embedding || archived} size='md' />
) } } type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' // operation action for list and detail export const OperationAction: FC<{ embeddingAvailable: boolean detail: { name: string enabled: boolean archived: boolean id: string data_source_type: string doc_form: string } datasetId: string onUpdate: (operationName?: string) => void scene?: 'list' | 'detail' className?: string }> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => { const { id, enabled = false, archived = false, data_source_type } = detail || {} const [showModal, setShowModal] = useState(false) const [deleting, setDeleting] = useState(false) const { notify } = useContext(ToastContext) const { t } = useTranslation() const router = useRouter() const { mutateAsync: archiveDocument } = useDocumentArchive() const { mutateAsync: unArchiveDocument } = useDocumentUnArchive() const { mutateAsync: enableDocument } = useDocumentEnable() const { mutateAsync: disableDocument } = useDocumentDisable() const { mutateAsync: deleteDocument } = useDocumentDelete() const { mutateAsync: syncDocument } = useSyncDocument() const { mutateAsync: syncWebsite } = useSyncWebsite() const isListScene = scene === 'list' const onOperate = async (operationName: OperationName) => { let opApi = deleteDocument switch (operationName) { case 'archive': opApi = archiveDocument break case 'un_archive': opApi = unArchiveDocument break case 'enable': opApi = enableDocument break case 'disable': opApi = disableDocument break case 'sync': if (data_source_type === 'notion_import') opApi = syncDocument else opApi = syncWebsite break default: opApi = deleteDocument setDeleting(true) break } const [e] = await asyncRunSafe(opApi({ datasetId, documentId: id }) as Promise) if (!e) { notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) onUpdate(operationName) } else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } if (operationName === 'delete') setDeleting(false) } const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => { if (operationName === 'enable' && enabled) return if (operationName === 'disable' && !enabled) return onOperate(operationName) }, { wait: 500 }) const [currDocument, setCurrDocument] = useState<{ id: string name: string } | null>(null) const [isShowRenameModal, { setTrue: setShowRenameModalTrue, setFalse: setShowRenameModalFalse, }] = useBoolean(false) const handleShowRenameModal = useCallback((doc: { id: string name: string }) => { setCurrDocument(doc) setShowRenameModalTrue() }, [setShowRenameModalTrue]) const handleRenamed = useCallback(() => { onUpdate() }, [onUpdate]) return
e.stopPropagation()}> {isListScene && !embeddingAvailable && ( { }} disabled={true} size='md' /> )} {isListScene && embeddingAvailable && ( <> {archived ?
{ }} disabled={true} size='md' />
: handleSwitch(v ? 'enable' : 'disable')} size='md' /> } )} {embeddingAvailable && ( <> {!archived && ( <>
{ handleShowRenameModal({ id: detail.id, name: detail.name, }) }}> {t('datasetDocuments.list.table.rename')}
{['notion_import', DataSourceType.WEB].includes(data_source_type) && (
onOperate('sync')}> {t('datasetDocuments.list.action.sync')}
)} )} {!archived &&
onOperate('archive')}> {t('datasetDocuments.list.action.archive')}
} {archived && (
onOperate('un_archive')}> {t('datasetDocuments.list.action.unarchive')}
)}
setShowModal(true)}> {t('datasetDocuments.list.action.delete')}
} trigger='click' position='br' btnElement={
} btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!hover:bg-state-base-hover !shadow-none' : '!bg-transparent')} popupClassName='!w-full' className={`!z-20 flex h-fit !w-[200px] justify-end ${className}`} /> )} {showModal && onOperate('delete')} onCancel={() => setShowModal(false)} /> } {isShowRenameModal && currDocument && ( )} } export const renderTdValue = (value: string | number | null, isEmptyStyle = false) => { return (
{value ?? '-'}
) } const renderCount = (count: number | undefined) => { if (!count) return renderTdValue(0, true) if (count < 1000) return count return `${formatNumber((count / 1000).toFixed(1))}k` } type LocalDoc = SimpleDocumentDetail & { percent?: number } type IDocumentListProps = { embeddingAvailable: boolean documents: LocalDoc[] selectedIds: string[] onSelectedIdChange: (selectedIds: string[]) => void datasetId: string pagination: PaginationProps onUpdate: () => void onManageMetadata: () => void } /** * Document list component including basic information */ const DocumentList: FC = ({ embeddingAvailable, documents = [], selectedIds, onSelectedIdChange, datasetId, pagination, onUpdate, onManageMetadata, }) => { const { t } = useTranslation() const { formatTime } = useTimestamp() const router = useRouter() const [datasetConfig] = useDatasetDetailContext(s => [s.dataset]) const chunkingMode = datasetConfig?.doc_form const isGeneralMode = chunkingMode !== ChunkingMode.parentChild const isQAMode = chunkingMode === ChunkingMode.qa const [localDocs, setLocalDocs] = useState(documents) const [enableSort, setEnableSort] = useState(true) const { isShowEditModal, showEditModal, hideEditModal, originalList, handleSave, } = useBatchEditDocumentMetadata({ datasetId, docList: documents.filter(item => selectedIds.includes(item.id)), onUpdate, }) useEffect(() => { setLocalDocs(documents) }, [documents]) const onClickSort = () => { setEnableSort(!enableSort) if (enableSort) { const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1) setLocalDocs(sortedDocs) } else { setLocalDocs(documents) } } const [currDocument, setCurrDocument] = useState(null) const [isShowRenameModal, { setTrue: setShowRenameModalTrue, setFalse: setShowRenameModalFalse, }] = useBoolean(false) const handleShowRenameModal = useCallback((doc: LocalDoc) => { setCurrDocument(doc) setShowRenameModalTrue() }, [setShowRenameModalTrue]) const handleRenamed = useCallback(() => { onUpdate() }, [onUpdate]) const isAllSelected = useMemo(() => { return localDocs.length > 0 && localDocs.every(doc => selectedIds.includes(doc.id)) }, [localDocs, selectedIds]) const isSomeSelected = useMemo(() => { return localDocs.some(doc => selectedIds.includes(doc.id)) }, [localDocs, selectedIds]) const onSelectedAll = useCallback(() => { if (isAllSelected) onSelectedIdChange([]) else onSelectedIdChange(uniq([...selectedIds, ...localDocs.map(doc => doc.id)])) }, [isAllSelected, localDocs, onSelectedIdChange, selectedIds]) const { mutateAsync: archiveDocument } = useDocumentArchive() const { mutateAsync: enableDocument } = useDocumentEnable() const { mutateAsync: disableDocument } = useDocumentDisable() const { mutateAsync: deleteDocument } = useDocumentDelete() const handleAction = (actionName: DocumentActionType) => { return async () => { let opApi = deleteDocument switch (actionName) { case DocumentActionType.archive: opApi = archiveDocument break case DocumentActionType.enable: opApi = enableDocument break case DocumentActionType.disable: opApi = disableDocument break default: opApi = deleteDocument break } const [e] = await asyncRunSafe(opApi({ datasetId, documentIds: selectedIds }) as Promise) if (!e) { Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) onUpdate() } else { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } } } return (
{localDocs.map((doc, index) => { const isFile = doc.data_source_type === DataSourceType.FILE const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : '' return { router.push(`/datasets/${datasetId}/documents/${doc.id}`) }}> })}
e.stopPropagation()}> {embeddingAvailable && ( )} #
{t('datasetDocuments.list.table.header.fileName')}
{t('datasetDocuments.list.table.header.chunkingMode')} {t('datasetDocuments.list.table.header.words')} {t('datasetDocuments.list.table.header.hitCount')}
{t('datasetDocuments.list.table.header.uploadTime')}
{t('datasetDocuments.list.table.header.status')} {t('datasetDocuments.list.table.header.action')}
e.stopPropagation()}> { onSelectedIdChange( selectedIds.includes(doc.id) ? selectedIds.filter(id => id !== doc.id) : [...selectedIds, doc.id], ) }} /> {/* {doc.position} */} {index + 1}
{doc?.data_source_type === DataSourceType.NOTION && } {doc?.data_source_type === DataSourceType.FILE && } {doc?.data_source_type === DataSourceType.WEB && }
{doc.name}
{ e.stopPropagation() handleShowRenameModal(doc) }} >
{renderCount(doc.word_count)} {renderCount(doc.hit_count)} {formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)} { (['indexing', 'splitting', 'parsing', 'cleaning'].includes(doc.indexing_status) && doc?.data_source_type === DataSourceType.NOTION) ? : }
{(selectedIds.length > 0) && ( { onSelectedIdChange([]) }} /> )} {/* Show Pagination only if the total is more than the limit */} {pagination.total && ( )} {isShowRenameModal && currDocument && ( )} {isShowEditModal && ( { hideEditModal() onManageMetadata() }} /> )}
) } export default DocumentList