list.tsx 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useCallback, useEffect, useMemo, useState } from 'react'
  4. import { useBoolean, useDebounceFn } from 'ahooks'
  5. import { ArrowDownIcon } from '@heroicons/react/24/outline'
  6. import { pick, uniq } from 'lodash-es'
  7. import {
  8. RiArchive2Line,
  9. RiDeleteBinLine,
  10. RiEditLine,
  11. RiEqualizer2Line,
  12. RiLoopLeftLine,
  13. RiMoreFill,
  14. } from '@remixicon/react'
  15. import { useContext } from 'use-context-selector'
  16. import { useRouter } from 'next/navigation'
  17. import { useTranslation } from 'react-i18next'
  18. import dayjs from 'dayjs'
  19. import { Globe01 } from '../../base/icons/src/vender/line/mapsAndTravel'
  20. import ChunkingModeLabel from '../common/chunking-mode-label'
  21. import FileTypeIcon from '../../base/file-uploader/file-type-icon'
  22. import s from './style.module.css'
  23. import RenameModal from './rename-modal'
  24. import BatchAction from './detail/completed/common/batch-action'
  25. import cn from '@/utils/classnames'
  26. import Switch from '@/app/components/base/switch'
  27. import Divider from '@/app/components/base/divider'
  28. import Popover from '@/app/components/base/popover'
  29. import Confirm from '@/app/components/base/confirm'
  30. import Tooltip from '@/app/components/base/tooltip'
  31. import Toast, { ToastContext } from '@/app/components/base/toast'
  32. import type { ColorMap, IndicatorProps } from '@/app/components/header/indicator'
  33. import Indicator from '@/app/components/header/indicator'
  34. import { asyncRunSafe } from '@/utils'
  35. import { formatNumber } from '@/utils/format'
  36. import NotionIcon from '@/app/components/base/notion-icon'
  37. import ProgressBar from '@/app/components/base/progress-bar'
  38. import { ChunkingMode, DataSourceType, DocumentActionType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets'
  39. import type { CommonResponse } from '@/models/common'
  40. import useTimestamp from '@/hooks/use-timestamp'
  41. import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '@/context/dataset-detail'
  42. import type { Props as PaginationProps } from '@/app/components/base/pagination'
  43. import Pagination from '@/app/components/base/pagination'
  44. import Checkbox from '@/app/components/base/checkbox'
  45. import {
  46. useDocumentArchive,
  47. useDocumentCheckFail,
  48. useDocumentDelete,
  49. useDocumentDisable,
  50. useDocumentEnable,
  51. useDocumentUnArchive,
  52. useSyncDocument,
  53. useSyncWebsite,
  54. } from '@/service/knowledge/use-document'
  55. import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type'
  56. import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata'
  57. import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
  58. import { useAppContext } from '@/context/app-context'
  59. import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
  60. export const useIndexStatus = () => {
  61. const { t } = useTranslation()
  62. return {
  63. queuing: { color: 'orange', text: t('datasetDocuments.list.status.queuing') }, // waiting
  64. indexing: { color: 'blue', text: t('datasetDocuments.list.status.indexing') }, // indexing splitting parsing cleaning
  65. paused: { color: 'orange', text: t('datasetDocuments.list.status.paused') }, // paused
  66. error: { color: 'red', text: t('datasetDocuments.list.status.error') }, // error
  67. available: { color: 'green', text: t('datasetDocuments.list.status.available') }, // completed,archived = false,enabled = true
  68. enabled: { color: 'green', text: t('datasetDocuments.list.status.enabled') }, // completed,archived = false,enabled = true
  69. disabled: { color: 'gray', text: t('datasetDocuments.list.status.disabled') }, // completed,archived = false,enabled = false
  70. archived: { color: 'gray', text: t('datasetDocuments.list.status.archived') }, // completed,archived = true
  71. }
  72. }
  73. const STATUS_TEXT_COLOR_MAP: ColorMap = {
  74. green: 'text-util-colors-green-green-600',
  75. orange: 'text-util-colors-warning-warning-600',
  76. red: 'text-util-colors-red-red-600',
  77. blue: 'text-util-colors-blue-light-blue-light-600',
  78. yellow: 'text-util-colors-warning-warning-600',
  79. gray: 'text-text-tertiary',
  80. }
  81. // status item for list
  82. export const StatusItem: FC<{
  83. status: DocumentDisplayStatus
  84. reverse?: boolean
  85. scene?: 'list' | 'detail'
  86. textCls?: string
  87. errorMessage?: string
  88. detail?: {
  89. enabled: boolean
  90. archived: boolean
  91. id: string
  92. }
  93. datasetId?: string
  94. onUpdate?: (operationName?: string) => void
  95. }> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage, datasetId = '', detail, onUpdate }) => {
  96. const DOC_INDEX_STATUS_MAP = useIndexStatus()
  97. const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP
  98. const { enabled = false, archived = false, id = '' } = detail || {}
  99. const { notify } = useContext(ToastContext)
  100. const { t } = useTranslation()
  101. const { mutateAsync: enableDocument } = useDocumentEnable()
  102. const { mutateAsync: disableDocument } = useDocumentDisable()
  103. const { mutateAsync: deleteDocument } = useDocumentDelete()
  104. const onOperate = async (operationName: OperationName) => {
  105. let opApi = deleteDocument
  106. switch (operationName) {
  107. case 'enable':
  108. opApi = enableDocument
  109. break
  110. case 'disable':
  111. opApi = disableDocument
  112. break
  113. }
  114. const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
  115. if (!e) {
  116. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  117. onUpdate?.()
  118. // onUpdate?.(operationName)
  119. }
  120. else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
  121. }
  122. const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
  123. if (operationName === 'enable' && enabled)
  124. return
  125. if (operationName === 'disable' && !enabled)
  126. return
  127. onOperate(operationName)
  128. }, { wait: 500 })
  129. const embedding = useMemo(() => {
  130. return ['queuing', 'indexing', 'paused'].includes(localStatus)
  131. }, [localStatus])
  132. return <div className={
  133. cn('flex items-center',
  134. reverse ? 'flex-row-reverse' : '',
  135. scene === 'detail' ? s.statusItemDetail : '')
  136. }>
  137. <Indicator color={DOC_INDEX_STATUS_MAP[localStatus]?.color as IndicatorProps['color']} className={reverse ? 'ml-2' : 'mr-2'} />
  138. <span className={cn(`${STATUS_TEXT_COLOR_MAP[DOC_INDEX_STATUS_MAP[localStatus].color as keyof typeof STATUS_TEXT_COLOR_MAP]} text-sm`, textCls)}>
  139. {DOC_INDEX_STATUS_MAP[localStatus]?.text}
  140. </span>
  141. {
  142. errorMessage && (
  143. <Tooltip
  144. popupContent={
  145. <div className='max-w-[260px] break-all'>{errorMessage}</div>
  146. }
  147. triggerClassName='ml-1 w-4 h-4'
  148. />
  149. )
  150. }
  151. {
  152. scene === 'detail' && (
  153. <div className='ml-1.5 flex items-center justify-between'>
  154. <Tooltip
  155. popupContent={t('datasetDocuments.list.action.enableWarning')}
  156. popupClassName='text-text-secondary system-xs-medium'
  157. needsDelay
  158. disabled={!archived}
  159. >
  160. <Switch
  161. defaultValue={archived ? false : enabled}
  162. onChange={v => !archived && handleSwitch(v ? 'enable' : 'disable')}
  163. disabled={embedding || archived}
  164. size='md'
  165. />
  166. </Tooltip>
  167. </div>
  168. )
  169. }
  170. </div>
  171. }
  172. type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' | 'check_fail'
  173. // operation action for list and detail
  174. export const OperationAction: FC<{
  175. embeddingAvailable: boolean
  176. detail: {
  177. name: string
  178. enabled: boolean
  179. archived: boolean
  180. id: string
  181. data_source_type: string
  182. doc_form: string
  183. check_status: number
  184. }
  185. datasetId: string
  186. onUpdate: (operationName?: string) => void
  187. scene?: 'list' | 'detail'
  188. className?: string
  189. dataset: any
  190. }> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '', dataset }) => {
  191. const { currentWorkspace, userProfile } = useAppContext()
  192. const { id, enabled = false, archived = false, data_source_type } = detail || {}
  193. const [showModal, setShowModal] = useState(false)
  194. const [deleting, setDeleting] = useState(false)
  195. const { notify } = useContext(ToastContext)
  196. const { t } = useTranslation()
  197. const router = useRouter()
  198. const { mutateAsync: archiveDocument } = useDocumentArchive()
  199. const { mutateAsync: unArchiveDocument } = useDocumentUnArchive()
  200. const { mutateAsync: enableDocument } = useDocumentEnable()
  201. const { mutateAsync: disableDocument } = useDocumentDisable()
  202. const { mutateAsync: deleteDocument } = useDocumentDelete()
  203. const { mutateAsync: syncDocument } = useSyncDocument()
  204. const { mutateAsync: syncWebsite } = useSyncWebsite()
  205. const isListScene = scene === 'list'
  206. const onOperate = async (operationName: OperationName) => {
  207. let opApi = deleteDocument
  208. switch (operationName) {
  209. case 'archive':
  210. opApi = archiveDocument
  211. break
  212. case 'un_archive':
  213. opApi = unArchiveDocument
  214. break
  215. case 'enable':
  216. opApi = enableDocument
  217. break
  218. case 'disable':
  219. opApi = disableDocument
  220. break
  221. case 'sync':
  222. if (data_source_type === 'notion_import')
  223. opApi = syncDocument
  224. else
  225. opApi = syncWebsite
  226. break
  227. default:
  228. opApi = deleteDocument
  229. setDeleting(true)
  230. break
  231. }
  232. const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
  233. if (!e) {
  234. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  235. onUpdate(operationName)
  236. }
  237. else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
  238. if (operationName === 'delete')
  239. setDeleting(false)
  240. }
  241. const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
  242. if (operationName === 'enable' && enabled)
  243. return
  244. if (operationName === 'disable' && !enabled)
  245. return
  246. onOperate(operationName)
  247. }, { wait: 500 })
  248. const [currDocument, setCurrDocument] = useState<{
  249. id: string
  250. name: string
  251. } | null>(null)
  252. const [isShowRenameModal, {
  253. setTrue: setShowRenameModalTrue,
  254. setFalse: setShowRenameModalFalse,
  255. }] = useBoolean(false)
  256. const handleShowRenameModal = useCallback((doc: {
  257. id: string
  258. name: string
  259. }) => {
  260. setCurrDocument(doc)
  261. setShowRenameModalTrue()
  262. }, [setShowRenameModalTrue])
  263. const handleRenamed = useCallback(() => {
  264. onUpdate()
  265. }, [onUpdate])
  266. const [showNotDelModal, setShowNotDelModal] = useState(false)
  267. const [showNotSwitchModal, setShowNotSwitchModal] = useState(false)
  268. const beforeDel = () => {
  269. if (!detail.enabled)
  270. setShowModal(true)
  271. else
  272. setShowNotDelModal(true)
  273. }
  274. const [showApplyExamine, setShowApplyExamine] = useState<boolean>(false)
  275. const beforeHandleSwitch = () => {
  276. if (detail.check_status === 1) { // 待审核
  277. setShowNotSwitchModal(true)
  278. return false
  279. }
  280. if (dataset.edit_auth === 1 && dataset.created_by !== userProfile.id) {
  281. setShowApplyExamine(true)
  282. return false
  283. }
  284. if (dataset.edit_auth === 2 && !['owner', 'admin', 'leader'].includes(currentWorkspace.role)) {
  285. setShowApplyExamine(true)
  286. return false
  287. }
  288. return true
  289. }
  290. const onApplyExamine = () => {
  291. handleSwitch(enabled ? 'disable' : 'enable')
  292. }
  293. return <div className='flex items-center' onClick={e => e.stopPropagation()}>
  294. {isListScene && !embeddingAvailable && (
  295. <Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
  296. )}
  297. {isListScene && embeddingAvailable && (
  298. <>
  299. {archived
  300. ? <Tooltip
  301. popupContent={t('datasetDocuments.list.action.enableWarning')}
  302. popupClassName='!font-semibold'
  303. needsDelay
  304. >
  305. <div>
  306. <Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
  307. </div>
  308. </Tooltip>
  309. : <Switch defaultValue={enabled} beforeChange={beforeHandleSwitch} onChange={v => handleSwitch(v ? 'enable' : 'disable')} size='md' />
  310. }
  311. <Divider className='!ml-4 !mr-2 !h-3' type='vertical' />
  312. </>
  313. )}
  314. {embeddingAvailable && (
  315. <>
  316. <Tooltip
  317. popupContent={t('datasetDocuments.list.action.settings')}
  318. popupClassName='text-text-secondary system-xs-medium'
  319. >
  320. <button
  321. className={cn('mr-2 cursor-pointer rounded-lg',
  322. !isListScene
  323. ? 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
  324. : 'p-0.5 hover:bg-state-base-hover')}
  325. onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
  326. <RiEqualizer2Line className='h-4 w-4 text-components-button-secondary-text' />
  327. </button>
  328. </Tooltip>
  329. <Popover
  330. htmlContent={
  331. <div className='w-full py-1'>
  332. {!archived && (
  333. <>
  334. <div className={s.actionItem} onClick={() => {
  335. handleShowRenameModal({
  336. id: detail.id,
  337. name: detail.name,
  338. })
  339. }}>
  340. <RiEditLine className='h-4 w-4 text-text-tertiary' />
  341. <span className={s.actionName}>{t('datasetDocuments.list.table.rename')}</span>
  342. </div>
  343. {['notion_import', DataSourceType.WEB].includes(data_source_type) && (
  344. <div className={s.actionItem} onClick={() => onOperate('sync')}>
  345. <RiLoopLeftLine className='h-4 w-4 text-text-tertiary' />
  346. <span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
  347. </div>
  348. )}
  349. <Divider className='my-1' />
  350. </>
  351. )}
  352. {!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
  353. <RiArchive2Line className='h-4 w-4 text-text-tertiary' />
  354. <span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
  355. </div>}
  356. {archived && (
  357. <div className={s.actionItem} onClick={() => onOperate('un_archive')}>
  358. <RiArchive2Line className='h-4 w-4 text-text-tertiary' />
  359. <span className={s.actionName}>{t('datasetDocuments.list.action.unarchive')}</span>
  360. </div>
  361. )}
  362. <div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => beforeDel()}>
  363. <RiDeleteBinLine className={'h-4 w-4 text-text-tertiary group-hover:text-text-destructive'} />
  364. <span className={cn(s.actionName, 'group-hover:text-text-destructive')}>{t('datasetDocuments.list.action.delete')}</span>
  365. </div>
  366. </div>
  367. }
  368. trigger='click'
  369. position='br'
  370. btnElement={
  371. <div className={cn(s.commonIcon)}>
  372. <RiMoreFill className='h-4 w-4 text-components-button-secondary-text' />
  373. </div>
  374. }
  375. btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!hover:bg-state-base-hover !shadow-none' : '!bg-transparent')}
  376. popupClassName='!w-full'
  377. className={`!z-20 flex h-fit !w-[200px] justify-end ${className}`}
  378. />
  379. </>
  380. )}
  381. {showModal
  382. && <Confirm
  383. isShow={showModal}
  384. isLoading={deleting}
  385. isDisabled={deleting}
  386. title={t('datasetDocuments.list.delete.title')}
  387. content={t('datasetDocuments.list.delete.content')}
  388. confirmText={t('common.operation.sure')}
  389. onConfirm={() => onOperate('delete')}
  390. onCancel={() => setShowModal(false)}
  391. />
  392. }
  393. {showNotDelModal
  394. && <Confirm
  395. isShow={showNotDelModal}
  396. title="操作失败"
  397. content="请将该知识停用后再删除!"
  398. onConfirm={() => setShowNotDelModal(false)}
  399. onCancel={() => setShowNotDelModal(false)}
  400. showCancel={false}
  401. confirmText="好的"
  402. />
  403. }
  404. {showNotSwitchModal
  405. && <Confirm
  406. isShow={showNotSwitchModal}
  407. title="操作失败"
  408. content="该知识有待审核的操作,无法再进行上下线操作!"
  409. onConfirm={() => setShowNotSwitchModal(false)}
  410. onCancel={() => setShowNotSwitchModal(false)}
  411. showCancel={false}
  412. confirmText="好的"
  413. />
  414. }
  415. {showApplyExamine
  416. && <Confirm
  417. isShow={showApplyExamine}
  418. title={`${enabled ? '下线' : '上线'}审核确认`}
  419. content="该操作需要提交审核后生效,请确认是否提交?"
  420. onConfirm={onApplyExamine}
  421. onCancel={() => setShowApplyExamine(false)}
  422. />
  423. }
  424. {isShowRenameModal && currDocument && (
  425. <RenameModal
  426. datasetId={datasetId}
  427. documentId={currDocument.id}
  428. name={currDocument.name}
  429. onClose={setShowRenameModalFalse}
  430. onSaved={handleRenamed}
  431. />
  432. )}
  433. </div>
  434. }
  435. export const renderTdValue = (value: string | number | null, isEmptyStyle = false) => {
  436. return (
  437. <div className={cn(isEmptyStyle ? 'text-text-tertiary' : 'text-text-secondary', s.tdValue)}>
  438. {value ?? '-'}
  439. </div>
  440. )
  441. }
  442. const renderCount = (count: number | undefined) => {
  443. if (!count)
  444. return renderTdValue(0, true)
  445. if (count < 1000)
  446. return count
  447. return `${formatNumber((count / 1000).toFixed(1))}k`
  448. }
  449. type LocalDoc = SimpleDocumentDetail & { percent?: number }
  450. type IDocumentListProps = {
  451. embeddingAvailable: boolean
  452. documents: LocalDoc[]
  453. selectedIds: string[]
  454. onSelectedIdChange: (selectedIds: string[]) => void
  455. datasetId: string
  456. pagination: PaginationProps
  457. onUpdate: () => void
  458. onManageMetadata: () => void,
  459. dataset: any
  460. }
  461. /**
  462. * Document list component including basic information
  463. */
  464. const DocumentList: FC<IDocumentListProps> = ({
  465. embeddingAvailable,
  466. documents = [],
  467. selectedIds,
  468. onSelectedIdChange,
  469. datasetId,
  470. pagination,
  471. onUpdate,
  472. onManageMetadata,
  473. dataset,
  474. }) => {
  475. const { currentWorkspace, userProfile } = useAppContext()
  476. const { isCreate, isEdit, isOperation } = GetDatasetAuth(dataset)
  477. const { t } = useTranslation()
  478. const { formatTime } = useTimestamp()
  479. const router = useRouter()
  480. const [datasetConfig] = useDatasetDetailContext(s => [s.dataset])
  481. const chunkingMode = datasetConfig?.doc_form
  482. const isGeneralMode = chunkingMode !== ChunkingMode.parentChild
  483. const isQAMode = chunkingMode === ChunkingMode.qa
  484. const [localDocs, setLocalDocs] = useState<LocalDoc[]>(documents)
  485. const [enableSort, setEnableSort] = useState(true)
  486. const {
  487. isShowEditModal,
  488. showEditModal,
  489. hideEditModal,
  490. originalList,
  491. handleSave,
  492. } = useBatchEditDocumentMetadata({
  493. datasetId,
  494. docList: documents.filter(item => selectedIds.includes(item.id)),
  495. onUpdate,
  496. })
  497. useEffect(() => {
  498. setLocalDocs(documents)
  499. }, [documents])
  500. const onClickSort = () => {
  501. setEnableSort(!enableSort)
  502. if (enableSort) {
  503. const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1)
  504. setLocalDocs(sortedDocs)
  505. }
  506. else {
  507. setLocalDocs(documents)
  508. }
  509. }
  510. const [currDocument, setCurrDocument] = useState<LocalDoc | null>(null)
  511. const [isShowRenameModal, {
  512. setTrue: setShowRenameModalTrue,
  513. setFalse: setShowRenameModalFalse,
  514. }] = useBoolean(false)
  515. const handleShowRenameModal = useCallback((doc: LocalDoc) => {
  516. setCurrDocument(doc)
  517. setShowRenameModalTrue()
  518. }, [setShowRenameModalTrue])
  519. const handleRenamed = useCallback(() => {
  520. onUpdate()
  521. }, [onUpdate])
  522. const isAllSelected = useMemo(() => {
  523. return localDocs.length > 0 && localDocs.every(doc => selectedIds.includes(doc.id))
  524. }, [localDocs, selectedIds])
  525. const isSomeSelected = useMemo(() => {
  526. return localDocs.some(doc => selectedIds.includes(doc.id))
  527. }, [localDocs, selectedIds])
  528. const onSelectedAll = useCallback(() => {
  529. if (isAllSelected)
  530. onSelectedIdChange([])
  531. else
  532. onSelectedIdChange(uniq([...selectedIds, ...localDocs.map(doc => doc.id)]))
  533. }, [isAllSelected, localDocs, onSelectedIdChange, selectedIds])
  534. const { mutateAsync: archiveDocument } = useDocumentArchive()
  535. const { mutateAsync: enableDocument } = useDocumentEnable()
  536. const { mutateAsync: disableDocument } = useDocumentDisable()
  537. const { mutateAsync: deleteDocument } = useDocumentDelete()
  538. const { mutateAsync: checkFailDocument } = useDocumentCheckFail()
  539. const handleAction = (actionName: DocumentActionType) => {
  540. return async () => {
  541. let opApi = deleteDocument
  542. switch (actionName) {
  543. case DocumentActionType.archive:
  544. opApi = archiveDocument
  545. break
  546. case DocumentActionType.enable:
  547. opApi = enableDocument
  548. break
  549. case DocumentActionType.disable:
  550. opApi = disableDocument
  551. break
  552. default:
  553. opApi = deleteDocument
  554. break
  555. }
  556. const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentIds: selectedIds }) as Promise<CommonResponse>)
  557. if (!e) {
  558. Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  559. onUpdate()
  560. }
  561. else { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
  562. }
  563. }
  564. const ExamineMap: any = {
  565. 1: '待审核',
  566. 2: '审核不通过',
  567. 3: '无',
  568. }
  569. const [row, setRow] = useState<any>({})
  570. const [showConfirmExamineResult, setShowConfirmExamineResult] = useState(false)
  571. const [confirmExamineResultContent, setConfirmExamineResultContent] = useState('')
  572. const [showConfirmExamineHandle, setShowConfirmExamineHandle] = useState(false)
  573. const [confirmExamineHandleTitle, setConfirmExamineHandleTitle] = useState('')
  574. const [confirmExamineHandleContent, setConfirmExamineHandleContent] = useState('')
  575. const { notify } = useContext(ToastContext)
  576. const onOperate = async (operationName: OperationName) => {
  577. let opApi = checkFailDocument
  578. switch (operationName) {
  579. case 'check_fail':
  580. opApi = checkFailDocument
  581. break
  582. case 'enable':
  583. opApi = enableDocument
  584. break
  585. case 'disable':
  586. opApi = disableDocument
  587. break
  588. }
  589. const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: row.id }) as Promise<CommonResponse>)
  590. if (!e) {
  591. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  592. onUpdate()
  593. }
  594. else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
  595. onUpdate()
  596. setShowConfirmExamineHandle(false)
  597. }
  598. const onHandleExamine = (flag: boolean) => {
  599. if (flag)
  600. onOperate(row.enabled ? 'disable' : 'enable')
  601. else
  602. onOperate('check_fail')
  603. }
  604. return (
  605. <div className='relative flex h-full w-full flex-col'>
  606. <div className='relative grow overflow-x-auto'>
  607. <table className={`mt-3 w-full min-w-[700px] max-w-full border-collapse border-0 text-sm ${s.documentTable}`}>
  608. <thead className="h-8 border-b border-divider-subtle text-xs font-medium uppercase leading-8 text-text-tertiary">
  609. <tr>
  610. <td className='w-12'>
  611. <div className='flex items-center' onClick={e => e.stopPropagation()}>
  612. {isOperation && embeddingAvailable && (
  613. <Checkbox
  614. className='mr-2 shrink-0'
  615. checked={isAllSelected}
  616. mixed={!isAllSelected && isSomeSelected}
  617. onCheck={onSelectedAll}
  618. />
  619. )}
  620. #
  621. </div>
  622. </td>
  623. <td>
  624. <div className='flex'>
  625. {t('datasetDocuments.list.table.header.fileName')}
  626. </div>
  627. </td>
  628. <td className='w-[130px]'>{t('datasetDocuments.list.table.header.chunkingMode')}</td>
  629. <td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
  630. <td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
  631. <td className='w-44'>
  632. <div className='flex items-center' onClick={onClickSort}>
  633. {t('datasetDocuments.list.table.header.uploadTime')}
  634. <ArrowDownIcon className={cn('ml-0.5 h-3 w-3 cursor-pointer stroke-current stroke-2', enableSort ? 'text-text-tertiary' : 'text-text-disabled')} />
  635. </div>
  636. </td>
  637. <td className='w-40'>{t('datasetDocuments.list.table.header.status')}</td>
  638. <td className='w-40'>审核状态</td>
  639. {
  640. isOperation && (
  641. <td className='w-20'>{t('datasetDocuments.list.table.header.action')}</td>
  642. )
  643. }
  644. </tr>
  645. </thead>
  646. <tbody className="text-text-secondary">
  647. {localDocs.map((doc, index) => {
  648. const isFile = doc.data_source_type === DataSourceType.FILE
  649. const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : ''
  650. return <tr
  651. key={doc.id}
  652. className={`h-8 border-b border-divider-subtle ${isOperation && (doc.check_status === 2 || doc.check_status === 3) ? 'cursor-pointer hover:bg-background-default-hover' : ''}`}
  653. onClick={() => {
  654. isOperation && (doc.check_status === 2 || doc.check_status === 3) && router.push(`/datasets/${datasetId}/documents/${doc.id}`)
  655. }}>
  656. <td className='text-left align-middle text-xs text-text-tertiary'>
  657. <div className='flex items-center' onClick={e => e.stopPropagation()}>
  658. {
  659. isOperation && (
  660. <Checkbox
  661. className='mr-2 shrink-0'
  662. checked={selectedIds.includes(doc.id)}
  663. onCheck={() => {
  664. onSelectedIdChange(
  665. selectedIds.includes(doc.id)
  666. ? selectedIds.filter(id => id !== doc.id)
  667. : [...selectedIds, doc.id],
  668. )
  669. }}
  670. />
  671. )
  672. }
  673. {/* {doc.position} */}
  674. {index + 1}
  675. </div>
  676. </td>
  677. <td>
  678. <div className={`group mr-6 flex max-w-[460px] items-center ${isOperation ? 'hover:mr-0' : ''}`}>
  679. <div className='shrink-0'>
  680. {doc?.data_source_type === DataSourceType.NOTION && <NotionIcon className='mr-1.5 mt-[-3px] inline-flex align-middle' type='page' src={doc.data_source_info.notion_page_icon} />}
  681. {doc?.data_source_type === DataSourceType.FILE && <FileTypeIcon type={extensionToFileType(doc?.data_source_info?.upload_file?.extension ?? fileType)} className='mr-1.5' />}
  682. {doc?.data_source_type === DataSourceType.WEB && <Globe01 className='mr-1.5 mt-[-3px] inline-flex align-middle' />}
  683. </div>
  684. <span className='grow-1 truncate text-sm'>{doc.name}</span>
  685. {
  686. isOperation && (
  687. <div className='hidden shrink-0 group-hover:ml-auto group-hover:flex'>
  688. <Tooltip
  689. popupContent={t('datasetDocuments.list.table.rename')}
  690. >
  691. <div
  692. className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
  693. onClick={(e) => {
  694. e.stopPropagation()
  695. handleShowRenameModal(doc)
  696. }}
  697. >
  698. <RiEditLine className='h-4 w-4 text-text-tertiary' />
  699. </div>
  700. </Tooltip>
  701. </div>
  702. )
  703. }
  704. </div>
  705. </td>
  706. <td>
  707. <ChunkingModeLabel
  708. isGeneralMode={isGeneralMode}
  709. isQAMode={isQAMode}
  710. />
  711. </td>
  712. <td>{renderCount(doc.word_count)}</td>
  713. <td>{renderCount(doc.hit_count)}</td>
  714. <td className='text-[13px] text-text-secondary'>
  715. {formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
  716. </td>
  717. <td>
  718. {
  719. (['indexing', 'splitting', 'parsing', 'cleaning'].includes(doc.indexing_status) && doc?.data_source_type === DataSourceType.NOTION)
  720. ? <ProgressBar percent={doc.percent || 0} />
  721. : <StatusItem status={doc.display_status} />
  722. }
  723. </td>
  724. <td>
  725. {
  726. isOperation ? (<>
  727. {
  728. doc.check_status === 1 && (<>
  729. {
  730. dataset.edit_auth === 1 && (<>
  731. {
  732. dataset.created_by === userProfile.id
  733. ? <div className="cursor-pointer text-[#155aef]" onClick={(e) => {
  734. e.stopPropagation()
  735. setRow(doc)
  736. setConfirmExamineHandleTitle(`${doc.enabled ? '下线' : '上线'}审核`)
  737. setConfirmExamineHandleContent(`用户“${doc.enable_application}”申请将该知识${doc.enabled ? '下线' : '上线'},请审核!`)
  738. setShowConfirmExamineHandle(true)
  739. }}>{ExamineMap[doc.check_status]}</div>
  740. : <div>{ExamineMap[doc.check_status]}</div>
  741. }
  742. </>)
  743. }
  744. {
  745. dataset.edit_auth === 2 && (<>
  746. {
  747. ['owner', 'admin', 'leader'].includes(currentWorkspace.role)
  748. ? <div className="cursor-pointer text-[#155aef]" onClick={(e) => {
  749. e.stopPropagation()
  750. setRow(doc)
  751. setConfirmExamineHandleTitle(`${doc.enabled ? '下线' : '上线'}审核`)
  752. setConfirmExamineHandleContent(`用户“${doc.enable_application}”申请将该知识${doc.enabled ? '下线' : '上线'},请审核!`)
  753. setShowConfirmExamineHandle(true)
  754. }}>{ExamineMap[doc.check_status]}</div>
  755. : <div>{ExamineMap[doc.check_status]}</div>
  756. }
  757. </>)
  758. }
  759. </>)
  760. }
  761. {
  762. doc.check_status === 2 && (
  763. <div className="cursor text-[#155aef]" onClick={(e) => {
  764. e.stopPropagation()
  765. setConfirmExamineResultContent(`用户“${doc.check_by}”不同意将该知识上线!`)
  766. setShowConfirmExamineResult(true)
  767. }}>{ExamineMap[doc.check_status]}</div>
  768. )
  769. }
  770. {
  771. doc.check_status === 3 && (
  772. <div>{ExamineMap[doc.check_status]}</div>
  773. )
  774. }
  775. </>) : (
  776. <div>{ExamineMap[doc.check_status]}</div>
  777. )
  778. }
  779. </td>
  780. {
  781. isOperation && (
  782. <td>
  783. {
  784. (doc.check_status === 2 || doc.check_status === 3) && (<OperationAction
  785. embeddingAvailable={embeddingAvailable}
  786. datasetId={datasetId}
  787. detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form', 'check_status'])}
  788. onUpdate={onUpdate}
  789. dataset={dataset}
  790. />)
  791. }
  792. </td>
  793. )
  794. }
  795. </tr>
  796. })}
  797. </tbody>
  798. </table>
  799. </div>
  800. {(selectedIds.length > 0) && (
  801. <BatchAction
  802. className='absolute bottom-16 left-0 z-20'
  803. selectedIds={selectedIds}
  804. onArchive={handleAction(DocumentActionType.archive)}
  805. onBatchEnable={handleAction(DocumentActionType.enable)}
  806. onBatchDisable={handleAction(DocumentActionType.disable)}
  807. onBatchDelete={handleAction(DocumentActionType.delete)}
  808. onEditMetadata={showEditModal}
  809. onCancel={() => {
  810. onSelectedIdChange([])
  811. }}
  812. />
  813. )}
  814. {/* Show Pagination only if the total is more than the limit */}
  815. {pagination.total && (
  816. <Pagination
  817. {...pagination}
  818. className='w-full shrink-0 px-0 pb-0'
  819. />
  820. )}
  821. {isShowRenameModal && currDocument && (
  822. <RenameModal
  823. datasetId={datasetId}
  824. documentId={currDocument.id}
  825. name={currDocument.name}
  826. onClose={setShowRenameModalFalse}
  827. onSaved={handleRenamed}
  828. />
  829. )}
  830. {isShowEditModal && (
  831. <EditMetadataBatchModal
  832. datasetId={datasetId}
  833. documentNum={selectedIds.length}
  834. list={originalList}
  835. onSave={handleSave}
  836. onHide={hideEditModal}
  837. onShowManage={() => {
  838. hideEditModal()
  839. onManageMetadata()
  840. }}
  841. />
  842. )}
  843. {showConfirmExamineResult && (
  844. <Confirm
  845. title="审核结果"
  846. content={confirmExamineResultContent}
  847. isShow={showConfirmExamineResult}
  848. onConfirm={() => setShowConfirmExamineResult(false)}
  849. onCancel={() => setShowConfirmExamineResult(false)}
  850. showCancel={false}
  851. confirmText="好的"
  852. />
  853. )}
  854. {showConfirmExamineHandle && (
  855. <Confirm
  856. title={confirmExamineHandleTitle}
  857. content={confirmExamineHandleContent}
  858. isShow={showConfirmExamineHandle}
  859. onConfirm={() => onHandleExamine(true)}
  860. onCancel={() => onHandleExamine(false)}
  861. confirmText="同意"
  862. cancelText="不同意"
  863. maskClosable={false}
  864. />
  865. )}
  866. </div>
  867. )
  868. }
  869. export default DocumentList