'use client' import type { LogicalDocument } from '@dify/contracts/knowledge-fs/types.gen' import type { FocusEventHandler } from 'react' import type { DocumentDisplayStatus } from './document-model' import { Button } from '@langgenius/dify-ui/button' import { Checkbox } from '@langgenius/dify-ui/checkbox' import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { toast } from '@langgenius/dify-ui/toast' import { memo, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import Link from '@/next/link' import { sourceName } from './document-model' export type DocumentFilter = DocumentDisplayStatus | 'all' const DOCUMENT_RENDER_BATCH_SIZE = 100 const PARTIAL_RESULTS_DESCRIPTION_ID = 'partial-document-results' const statusIconClass: Record = { ready: 'i-ri-check-line text-text-success', queued: 'i-ri-time-line text-text-tertiary', processing: 'i-ri-loader-2-line animate-spin text-text-accent motion-reduce:animate-none', failed: 'i-ri-error-warning-fill text-text-destructive', disabled: 'i-ri-indeterminate-circle-line text-text-tertiary', } function TaskTrigger({ activeTaskCount, attentionTaskBadge, hasTaskError, onOpenTasks, tasksButtonLabel, tasksLiveStatus, }: { activeTaskCount: number attentionTaskBadge?: string hasTaskError: boolean onOpenTasks: () => void tasksButtonLabel: string tasksLiveStatus: string }) { const { t } = useTranslation('dataset') return ( <> {tasksLiveStatus} ) } const DocumentRow = memo( ({ document, documentHref, formatTimeFromNow, onSelectedChange, readOnlyReasonId, selected, selectionDisabled, source, sourcePending, status, statusPending, }: { document: LogicalDocument documentHref: string formatTimeFromNow: (time: number) => string onSelectedChange: (documentId: string) => void readOnlyReasonId?: string selected: boolean selectionDisabled: boolean source?: string sourcePending: boolean status: DocumentDisplayStatus statusPending: boolean }) => { const { t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') const titleId = `new-document-${document.id}` const revision = document.activeRevision ?? document.active?.revision const updatedTime = Date.parse(document.updatedAt) return ( onSelectedChange(document.id)} />
{document.title} {revision !== undefined && ( v{revision} )}
{sourcePending ? ( {tCommon(($) => $.loading)} ) : ( {source ?? t(($) => $['newKnowledge.manualUpload'])} )} {statusPending ? ( {tCommon(($) => $.loading)} ) : ( {t(($) => $[`newKnowledge.documentStatus.${status}`])} )} {Number.isNaN(updatedTime) ? document.updatedAt : formatTimeFromNow(updatedTime)} $['newKnowledge.documentActions'], { name: document.title })} className="flex size-7 items-center justify-center rounded-md text-text-tertiary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid" > toast.info(t(($) => $['newKnowledge.documentActionsUnavailable']))} > {t(($) => $['newKnowledge.downloadDocuments'])} toast.info(t(($) => $['newKnowledge.documentActionsUnavailable']))} > {t(($) => $['newKnowledge.deleteDocuments'])} ) }, ) export function DocumentsEmpty({ activeTaskCount, attentionTaskBadge, canEdit, hasTaskError, onAddDocument, onDropFiles, onOpenTasks, readOnlyReasonId, tasksButtonLabel, tasksLiveStatus, uploading, }: { activeTaskCount: number attentionTaskBadge?: string canEdit: boolean hasTaskError: boolean onAddDocument: () => void onDropFiles: (files: File[]) => void onOpenTasks: () => void readOnlyReasonId?: string tasksButtonLabel: string tasksLiveStatus: string uploading: boolean }) { const { t } = useTranslation('dataset') const tasksVisible = activeTaskCount > 0 || Boolean(attentionTaskBadge) || hasTaskError return (
{ event.preventDefault() event.dataTransfer.dropEffect = canEdit ? 'copy' : 'none' }} onDrop={(event) => { event.preventDefault() if (canEdit) onDropFiles([...event.dataTransfer.files]) }} >

{t(($) => $['newKnowledge.documentsEmptyTitle'])}

{t(($) => $['newKnowledge.documentsEmptyDescription'])}

{canEdit && (

{t(($) => $['newKnowledge.documentsDropHint'])}

)} {tasksVisible && (
)}
) } export function DocumentsList({ activeTaskCount, allSelected, attentionTaskBadge, canEdit, completingResults, documents, filter, getDocumentHref, hasNextPage, hasSelectableDocuments, hasTaskError, isFetchNextPageError, isFetchingNextDocumentPage, isFetchingNextPage, onAddDocument, onFilterChange, onLoadMore, onOpenTasks, onSearchChange, onSelectAll, onSelectDocument, readOnlyReasonId, resultsIncomplete, search, selectionDisabled, selectedDocumentIds, someSelected, sourcesPending, sourceNames, statusPending, statuses, tasksPending, tasksButtonLabel, tasksLiveStatus, uploading, }: { activeTaskCount: number allSelected: boolean attentionTaskBadge?: string canEdit: boolean completingResults: boolean documents: LogicalDocument[] filter: DocumentFilter getDocumentHref: (documentId: string) => string hasNextPage: boolean hasSelectableDocuments: boolean hasTaskError: boolean isFetchNextPageError: boolean isFetchingNextDocumentPage: boolean isFetchingNextPage: boolean onAddDocument: () => void onFilterChange: (filter: DocumentFilter) => void onLoadMore: () => void onOpenTasks: () => void onSearchChange: (search: string) => void onSelectAll: () => void onSelectDocument: (documentId: string) => void readOnlyReasonId?: string resultsIncomplete: boolean search: string selectionDisabled: boolean selectedDocumentIds: Set someSelected: boolean sourcesPending: boolean sourceNames: Map statusPending: boolean statuses: Map tasksPending: boolean tasksButtonLabel: string tasksLiveStatus: string uploading: boolean }) { const { t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') const { formatTimeFromNow } = useFormatTimeFromNow() const [visibleDocumentLimit, setVisibleDocumentLimit] = useState(DOCUMENT_RENDER_BATCH_SIZE) const loadMoreButtonRef = useRef(null) const resultsContainerRef = useRef(null) const restoreLoadMoreFocusRef = useRef(false) const filterActive = filter !== 'all' || Boolean(search.trim()) const visibleDocuments = documents.slice(0, visibleDocumentLimit) const hasHiddenDocuments = visibleDocuments.length < documents.length useEffect(() => { if (isFetchingNextPage || !restoreLoadMoreFocusRef.current) return if (hasHiddenDocuments || hasNextPage || isFetchNextPageError) { loadMoreButtonRef.current?.focus() return } restoreLoadMoreFocusRef.current = false resultsContainerRef.current?.focus() }, [hasHiddenDocuments, hasNextPage, isFetchNextPageError, isFetchingNextPage]) return ( <>
{visibleDocuments.map((document) => ( ))}
$['newKnowledge.selectAllDocuments'])} onCheckedChange={onSelectAll} /> {t(($) => $['newKnowledge.documentColumn'])} {t(($) => $['newKnowledge.sourceColumn'])} {t(($) => $['newKnowledge.statusColumn'])} {t(($) => $['newKnowledge.updatedColumn'])} $['newKnowledge.actionsColumn'])} />
{!documents.length && !completingResults && !isFetchNextPageError && !resultsIncomplete && (

{t(($) => $['newKnowledge.noMatchingDocuments'])}

)} {resultsIncomplete && (

{t(($) => $['newKnowledge.partialDocumentResults'])}

)} {completingResults && (
)}

{t(($) => $['newKnowledge.lastReadyRevisionHint'])}

{hasHiddenDocuments ? (
) : isFetchNextPageError ? (
{t(($) => $['newKnowledge.documentsErrorDescription'])}
) : hasNextPage && (!filterActive || !completingResults) ? (
) : null} ) } export function DocumentBulkActions({ disabled, disabledReason, onClear, onBlurCapture, onFocusCapture, onReindex, reindexing, selectedCount, }: { disabled: boolean disabledReason?: string onClear: () => void onBlurCapture: FocusEventHandler onFocusCapture: FocusEventHandler onReindex: () => void reindexing: boolean selectedCount: number }) { const { t } = useTranslation('dataset') return (
$['newKnowledge.bulkDocumentActions'])} className="pointer-events-auto flex max-w-full min-w-0 items-center gap-2 overflow-x-auto rounded-[14px] border border-divider-subtle bg-components-panel-bg px-3 py-2.5 shadow-xl" role="group" onBlurCapture={onBlurCapture} onFocusCapture={onFocusCapture} > {t(($) => $['newKnowledge.documentsSelected'], { count: selectedCount })} {disabled && disabledReason && ( {t(($) => $['newKnowledge.reindexDocuments'])} {' · '} {disabledReason} )} {t(($) => $['newKnowledge.downloadDocuments'])} {' / '} {t(($) => $['newKnowledge.deleteDocuments'])} {' · '} {t(($) => $['cornerLabel.unavailable'])}
) }