diff --git a/web/features/new-rag/documents/page.tsx b/web/features/new-rag/documents/page.tsx index 74ef54d9444..33e64b36d43 100644 --- a/web/features/new-rag/documents/page.tsx +++ b/web/features/new-rag/documents/page.tsx @@ -1,13 +1,11 @@ 'use client' -import { Button } from '@langgenius/dify-ui/button' import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useQueryState } from 'nuqs' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Loading from '@/app/components/base/loading' import { datasetDefaultPermissionKeysAtom, workspacePermissionKeysErrorAtom, @@ -15,46 +13,24 @@ import { } from '@/context/permission-state' import { consoleQuery } from '@/service/client' import { DatasetACLPermission, hasPermission } from '@/utils/permission' -import { KnowledgeModelReadinessBanner } from '../components/knowledge-model-readiness-banner' import { KnowledgeModelSetupDialog } from '../components/knowledge-model-setup-dialog' -import { knowledgeFsTaskFailureMessageKey } from '../knowledge-fs-task-error' -import { newKnowledgeDocumentDetailPath } from '../routes' import { sourceFromApi } from '../sources/source-models' import { useKnowledgeModelSetupGuard } from '../use-knowledge-model-setup-guard' -import { useDocumentBulkSelection } from './bulk/selection-state' -import { DocumentBulkActionsToolbar } from './bulk/toolbar' -import { DocumentsEmpty, DocumentsList } from './list' import { DocumentMetadataDrawer } from './metadata/drawer' -import { - documentDisplayStatus, - newestTaskByDocument, - sourceName, - taskNeedsAttention, -} from './model' import { logicalDocumentListFromApi } from './models' -import { - DocumentPermissionRecoveryBoundary, - DocumentPermissionRecoveryBulkRegion, - DocumentReadPermissionRecovery, -} from './permission-recovery/recovery-boundary' import { useDocumentPermissionRecovery } from './permission-recovery/use-permission-recovery' import { documentSourcesInfiniteOptions, logicalDocumentsInfiniteOptions } from './queries' -import { documentFilterParser, documentMetadataParser, documentSearchParser } from './query-state' +import { documentMetadataParser } from './query-state' import { responseStatus } from './request-error' +import { DocumentResultsSurface } from './results/surface' import { useAuxiliaryTaskReadGuard } from './tasks/auxiliary-read-guard' import { ProcessingTasksDrawer } from './tasks/drawer' import { TaskEventObserver } from './tasks/event-observer' -import { MAX_AUTO_CURSOR_PAGES, queryKeyMatchesKnowledgeSpace } from './tasks/recovery' +import { queryKeyMatchesKnowledgeSpace } from './tasks/recovery' import { useTaskRuntime } from './tasks/use-task-runtime' -import { - DocumentUploadContent, - DocumentUploadHeader, - DocumentUploadSurface, -} from './upload/surface' export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) { const { t } = useTranslation('dataset') - const { t: tCommon } = useTranslation('common') const queryClient = useQueryClient() const datasetDefaultPermissionKeys = useAtomValue(datasetDefaultPermissionKeysAtom) const workspacePermissionKeysLoading = useAtomValue(workspacePermissionKeysLoadingAtom) @@ -67,8 +43,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } hasDocumentDownloadPermission && !workspacePermissionKeysLoading && !workspacePermissionKeysError - const [filter, setFilter] = useQueryState('status', documentFilterParser) - const [search, setSearch] = useQueryState('query', documentSearchParser) const [metadataRequest, setMetadataRequest] = useQueryState('metadata', documentMetadataParser) const metadataOpen = metadataRequest === '1' const setMetadataOpen = useCallback( @@ -78,10 +52,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } [setMetadataRequest], ) const [tasksOpen, setTasksOpen] = useState(false) - const [blockingDependencyRetries, setBlockingDependencyRetries] = useState({ - sources: false, - tasks: false, - }) const { configureModelSetup, ensureModelReady, @@ -116,9 +86,19 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } () => toast.error(t(($) => $['newKnowledge.taskFailedNotification'])), [t], ) + const taskRuntime = useTaskRuntime({ + auxiliaryTaskReadGuard, + denyAuxiliaryTaskRead, + documentPermissionDenied, + externalPermissionDenied: + documentPermissionDenied || auxiliaryReadPermissionDenied || sourcePermissionDenied, + knowledgeSpaceId, + onTaskFailed: notifyTaskFailed, + onTaskReachedTerminal: refreshDocuments, + tasksOpen, + }) const { acceptTaskSnapshot: handleTaskUpdated, - activeTasks, baseTasks, drawerTasks, handleTaskEvent, @@ -131,19 +111,8 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } streamedActiveTasks, taskPermissionDenied, taskProgressStore, - tasks, tasksQuery, - } = useTaskRuntime({ - auxiliaryTaskReadGuard, - denyAuxiliaryTaskRead, - documentPermissionDenied, - externalPermissionDenied: - documentPermissionDenied || auxiliaryReadPermissionDenied || sourcePermissionDenied, - knowledgeSpaceId, - onTaskFailed: notifyTaskFailed, - onTaskReachedTerminal: refreshDocuments, - tasksOpen, - }) + } = taskRuntime const refetchTasksQuery = tasksQuery.refetch const refetchSourcesQuery = sourcesQuery.refetch const { @@ -168,13 +137,9 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } () => ({ canRead, canWrite, denyWrite: handleWritePermissionDenied }), [canRead, canWrite, handleWritePermissionDenied], ) - const mainRetryFocusRequestedRef = useRef(false) - const documentsRetryButtonRef = useRef(null) - const dependencyRetryButtonRef = useRef(null) const { fetchNextPage: fetchNextDocumentPage, hasNextPage: hasNextDocumentPage, - isFetchNextPageError: isFetchNextDocumentPageError, isFetchingNextPage: isFetchingNextDocumentPage, } = documentsQuery const { @@ -182,15 +147,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } hasNextPage: hasNextTaskPage, isFetchingNextPage: isFetchingNextTaskPage, } = tasksQuery - const { - fetchNextPage: fetchNextSourcePage, - hasNextPage: hasNextSourcePage, - isFetchNextPageError: isFetchNextSourcePageError, - isFetchingNextPage: isFetchingNextSourcePage, - } = sourcesQuery - const canAutoFetchDocumentPage = Boolean( - hasNextDocumentPage && (documentsQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, - ) const documents = useMemo( () => documentsQuery.data?.pages.flatMap((page) => logicalDocumentListFromApi(page).items) ?? [], @@ -215,234 +171,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } () => new Map(sources.map((source) => [source.id, source.name])), [sources], ) - const unresolvedDocumentSourceIds = useMemo( - () => - new Set( - documents.flatMap((document) => - document.sourceId && !sourceNames.has(document.sourceId) ? [document.sourceId] : [], - ), - ), - [documents, sourceNames], - ) - const hasRelevantNextSourcePage = Boolean(hasNextSourcePage && unresolvedDocumentSourceIds.size) - const isFetchingNextResultsPage = Boolean( - isFetchingNextDocumentPage || (hasRelevantNextSourcePage && isFetchingNextSourcePage), - ) - const canAutoFetchSourcePage = Boolean( - hasRelevantNextSourcePage && (sourcesQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, - ) - const taskByDocument = useMemo(() => newestTaskByDocument(tasks), [tasks]) - const documentStatuses = useMemo( - () => - new Map( - documents.map((document) => [ - document.id, - documentDisplayStatus(document, taskByDocument.get(document.id)), - ]), - ), - [documents, taskByDocument], - ) - const documentFailureReasons = useMemo( - () => - new Map( - documents.flatMap((document) => { - if (documentStatuses.get(document.id) !== 'failed') return [] - const task = taskByDocument.get(document.id) - const messageKey = - knowledgeFsTaskFailureMessageKey( - task?.failure, - task?.errorCode ?? (task?.errorMessage ? 'LEGACY_TASK_FAILURE' : undefined), - ) ?? 'newKnowledge.taskFailure.internal' - return [[document.id, t(($) => $[messageKey])] as const] - }), - ), - [documentStatuses, documents, t, taskByDocument], - ) - const taskResultsIncomplete = Boolean(!tasksQuery.data || tasksQuery.isPending) - const filterActive = filter !== 'all' || Boolean(search.trim()) - const filteredDocuments = useMemo(() => { - const normalizedSearch = search.trim().toLocaleLowerCase() - return documents.filter((document) => { - const status = documentStatuses.get(document.id) - if (filter !== 'all' && status !== filter) return false - if (!normalizedSearch) return true - const resolvedSourceName = - (document.sourceId && sourceNames.get(document.sourceId)) ?? sourceName(document) - return `${document.title} ${resolvedSourceName ?? ''}` - .toLocaleLowerCase() - .includes(normalizedSearch) - }) - }, [documentStatuses, documents, filter, search, sourceNames]) - const completingFilteredResults = - filterActive && - !documentsQuery.isFetchNextPageError && - (canAutoFetchDocumentPage || documentsQuery.isFetchingNextPage) - const filteredResultsIncomplete = Boolean( - (filterActive && - (hasNextDocumentPage || - documentsQuery.isFetchingNextPage || - documentsQuery.isFetchNextPageError)) || - (filterActive && - unresolvedDocumentSourceIds.size > 0 && - (hasNextSourcePage || sourcesQuery.isFetchingNextPage || sourcesQuery.isFetchNextPageError)), - ) - const taskQueryBlockingError = Boolean( - !tasksQuery.data && (tasksQuery.error || blockingDependencyRetries.tasks), - ) - const sourceQueryBlockingError = Boolean( - !sourcesQuery.data && (sourcesQuery.error || blockingDependencyRetries.sources), - ) - const dependencyQueryBlockingError = taskQueryBlockingError || sourceQueryBlockingError - const dependencyQueryWarning = Boolean( - (tasksQuery.error && tasksQuery.data) || - (sourcesQuery.error && sourcesQuery.data) || - sourcesQuery.isFetchNextPageError, - ) - const sourceResultsIncomplete = Boolean( - !sourcesQuery.data || - sourcesQuery.isPending || - (unresolvedDocumentSourceIds.size && - (hasNextSourcePage || - sourcesQuery.error || - sourcesQuery.isFetchingNextPage || - sourcesQuery.isFetchNextPageError)), - ) - const sourceQueryWarning = Boolean( - (sourcesQuery.error && sourcesQuery.data) || sourcesQuery.isFetchNextPageError, - ) - const taskQueryWarning = Boolean(tasksQuery.error && tasksQuery.data) - const documentQueryWarning = Boolean(documentsQuery.error && documentsQuery.data) - const dependencyRetryFetching = Boolean( - (taskQueryWarning && tasksQuery.isFetching) || (sourceQueryWarning && sourcesQuery.isFetching), - ) - const blockingDependencyRetryFetching = Boolean( - (taskQueryBlockingError && tasksQuery.isFetching) || - (sourceQueryBlockingError && sourcesQuery.isFetching), - ) - const mainRecoveryVisible = Boolean( - documentsQuery.error || dependencyQueryBlockingError || dependencyQueryWarning, - ) - const mainRecoveryIdentity = [ - documentsQuery.error ? 'documents' : '', - taskQueryBlockingError ? 'tasks-blocking' : '', - sourceQueryBlockingError ? 'sources-blocking' : '', - taskQueryWarning ? 'tasks-warning' : '', - sourceQueryWarning ? 'sources-warning' : '', - ].join(':') - const dependencyResultsIncomplete = taskResultsIncomplete || sourceResultsIncomplete - const selectionDisabled = - !canWrite || - dependencyResultsIncomplete || - documentQueryWarning || - taskQueryWarning || - (sourceQueryWarning && unresolvedDocumentSourceIds.size > 0) || - filteredResultsIncomplete - const reindexUnavailableReason = - tasksQuery.error || tasksQuery.isFetchNextPageError - ? t(($) => $['newKnowledge.tasksErrorDescription']) - : (sourcesQuery.error || sourcesQuery.isFetchNextPageError) && - unresolvedDocumentSourceIds.size > 0 - ? t(($) => $['newKnowledge.sourcesErrorDescription']) - : documentsQuery.error || documentsQuery.isFetchNextPageError - ? t(($) => $['newKnowledge.documentsErrorDescription']) - : dependencyResultsIncomplete - ? tCommon(($) => $.loading) - : filteredResultsIncomplete - ? t(($) => $['newKnowledge.partialDocumentResults']) - : undefined - const bulkSelection = useDocumentBulkSelection({ - canSelect: canWrite && !selectionDisabled, - documents, - filteredDocuments, - statuses: documentStatuses, - taskResultsIncomplete, - }) - const attentionTasks = drawerTasks.filter(taskNeedsAttention) - const hasTaskError = attentionTasks.some( - (task) => task.state === 'failed' || task.state === 'canceled', - ) - const incompleteTaskHistoryHint = hasNextTaskPage - ? ` · ${t(($) => $['newKnowledge.taskHistoryIncomplete'])}` - : '' - const attentionTaskBadge = - attentionTasks.length || hasNextTaskPage - ? `${attentionTasks.length}${hasNextTaskPage ? '+' : ''}` - : undefined - const tasksButtonLabel = `${ - attentionTasks.length || hasNextTaskPage - ? t(($) => $['newKnowledge.tasksWithAttention'], { count: attentionTasks.length }) - : t(($) => $['newKnowledge.tasks']) - }${incompleteTaskHistoryHint}` - const tasksLiveStatus = `${ - hasTaskError - ? t(($) => $['newKnowledge.taskAttentionErrorCount'], { count: attentionTasks.length }) - : attentionTasks.length || hasNextTaskPage - ? t(($) => $['newKnowledge.taskAttentionCount'], { count: attentionTasks.length }) - : t(($) => $['newKnowledge.taskAttentionClear']) - }${incompleteTaskHistoryHint}` - const bulkActionsVisible = canWrite && bulkSelection.selectedDocumentIds.size > 0 - - useEffect(() => { - if (!mainRetryFocusRequestedRef.current) return - if (mainRecoveryVisible) { - if (documentsQuery.error && canRead) documentsRetryButtonRef.current?.focus() - else dependencyRetryButtonRef.current?.focus() - return - } - mainRetryFocusRequestedRef.current = false - document.getElementById('new-knowledge-documents-title')?.focus() - }, [canRead, documentsQuery.error, mainRecoveryIdentity, mainRecoveryVisible]) - - useEffect(() => { - if (!blockingDependencyRetries.tasks && !blockingDependencyRetries.sources) return - if ( - (!blockingDependencyRetries.tasks || !tasksQuery.data) && - (!blockingDependencyRetries.sources || !sourcesQuery.data) - ) - return - // oxlint-disable-next-line eslint-react/set-state-in-effect -- Successful first-page retries retire the locally stabilized blocking state. - setBlockingDependencyRetries((current) => ({ - sources: current.sources && !sourcesQuery.data, - tasks: current.tasks && !tasksQuery.data, - })) - }, [blockingDependencyRetries, sourcesQuery.data, tasksQuery.data]) - - useEffect(() => { - if ( - canRead && - (filterActive || (tasksOpen && unresolvedTaskDocumentIds.size > 0)) && - canAutoFetchDocumentPage && - !isFetchingNextDocumentPage && - !isFetchNextDocumentPageError - ) - void fetchNextDocumentPage() - }, [ - fetchNextDocumentPage, - filterActive, - tasksOpen, - unresolvedTaskDocumentIds, - canAutoFetchDocumentPage, - isFetchNextDocumentPageError, - isFetchingNextDocumentPage, - canRead, - ]) - - useEffect(() => { - if ( - canRead && - canAutoFetchSourcePage && - !isFetchingNextSourcePage && - !isFetchNextSourcePageError - ) - void fetchNextSourcePage() - }, [ - canAutoFetchSourcePage, - fetchNextSourcePage, - isFetchNextSourcePageError, - isFetchingNextSourcePage, - canRead, - ]) - const refreshDocumentsAndTasks = useCallback(() => { void Promise.allSettled([ queryClient.invalidateQueries({ @@ -455,25 +183,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } }), ]) }, [knowledgeSpaceId, queryClient]) - - const retryDependencyQueries = () => { - if (taskQueryBlockingError || sourceQueryBlockingError) - setBlockingDependencyRetries((current) => ({ - sources: current.sources || sourceQueryBlockingError, - tasks: current.tasks || taskQueryBlockingError, - })) - if (tasksQuery.error || taskQueryBlockingError) void tasksQuery.refetch() - if (sourcesQuery.isFetchNextPageError) void sourcesQuery.fetchNextPage() - else if (sourcesQuery.error || sourceQueryBlockingError) void sourcesQuery.refetch() - } - - const loadMoreResults = () => { - const requests: Promise[] = [] - if (hasNextDocumentPage && !isFetchingNextDocumentPage) requests.push(fetchNextDocumentPage()) - if (hasRelevantNextSourcePage && !isFetchingNextSourcePage) requests.push(fetchNextSourcePage()) - void Promise.allSettled(requests) - } - const handleReadPermissionDenied = useCallback(() => { setTasksOpen(false) setMetadataOpen(false) @@ -497,245 +206,19 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } /> ) })} - setMetadataOpen(true)} + onOpenTasks={() => setTasksOpen(true)} onReadDenied={handleReadPermissionDenied} - readSurfaceOpen={tasksOpen || metadataOpen} + permission={uploadPermission} recoverySurface={recoverySurface} - > - - - - {documentsQuery.error && - documentsQuery.data && - canRead && - !documentsQuery.isFetchNextPageError && ( -
- - {t(($) => - responseStatus(documentsQuery.error) === 403 - ? $['newKnowledge.documentsPermissionDescription'] - : $['newKnowledge.documentsErrorDescription'], - )} - - {responseStatus(documentsQuery.error) !== 403 && ( - - )} -
- )} - {canRead && !dependencyQueryBlockingError && dependencyQueryWarning && ( -
- - {sourcesQuery.error || sourcesQuery.isFetchNextPageError - ? t(($) => $['newKnowledge.sourcesErrorDescription']) - : t(($) => $['newKnowledge.tasksErrorDescription'])} - - -
- )} - {documentsQuery.isPending && canRead ? ( -
- -
- ) : !canRead ? ( - - ) : documentsQuery.error && !documentsQuery.data ? ( -
- -

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

-

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

- -
- ) : dependencyQueryBlockingError ? ( -
- -

- {taskQueryBlockingError - ? t(($) => $['newKnowledge.tasksErrorDescription']) - : t(($) => $['newKnowledge.sourcesErrorDescription'])} -

- -
- ) : ( - - {(upload) => - !documents.length ? ( - upload.openUpload()} - onOpenMetadata={() => setMetadataOpen(true)} - readOnlyReasonId={upload.uploadRestrictionReasonId} - uploading={upload.uploading} - /> - ) : ( - - newKnowledgeDocumentDetailPath(knowledgeSpaceId, documentId) - } - hasNextPage={Boolean(hasNextDocumentPage || hasRelevantNextSourcePage)} - hasSelectableDocuments={bulkSelection.hasSelectableDocuments} - hasTaskError={hasTaskError} - isFetchNextPageError={documentsQuery.isFetchNextPageError} - isFetchingNextDocumentPage={isFetchingNextDocumentPage} - isFetchingNextPage={isFetchingNextResultsPage} - knowledgeSpaceId={knowledgeSpaceId} - onAddDocument={() => upload.openUpload()} - onDocumentRemoved={bulkSelection.remove} - onFilterChange={setFilter} - onLoadMore={loadMoreResults} - onOpenMetadata={() => setMetadataOpen(true)} - onOpenTasks={() => setTasksOpen(true)} - onSearchChange={setSearch} - onSelectAll={bulkSelection.toggleAllFiltered} - onSelectDocument={bulkSelection.toggle} - onTaskUpdated={handleTaskUpdated} - onWriteDenied={handleWritePermissionDenied} - readOnlyReasonId={upload.readOnlyReasonId} - resultsIncomplete={filteredResultsIncomplete} - search={search} - selectionDisabled={selectionDisabled} - selectedDocumentIds={bulkSelection.selectedDocumentIds} - showTasks={Boolean( - tasks.length || - tasksQuery.error || - tasksQuery.isFetchNextPageError || - hasNextTaskPage, - )} - someSelected={bulkSelection.someFilteredSelected} - sourcesPending={sourceResultsIncomplete} - sourceNames={sourceNames} - statusPending={dependencyResultsIncomplete} - statuses={documentStatuses} - tasksByDocument={taskByDocument} - tasksPending={taskResultsIncomplete} - tasksButtonLabel={tasksButtonLabel} - tasksLiveStatus={tasksLiveStatus} - uploadRestrictionReasonId={upload.uploadRestrictionReasonId} - uploading={upload.uploading} - /> - ) - } - - )} -
- - {bulkActionsVisible && ( - - )} - -
+ taskRuntime={taskRuntime} + tasksOpen={tasksOpen} + /> + +export function DocumentResultsSurface({ + canDownload, + ensureModelReady, + knowledgeSpaceId, + metadataOpen, + onOpenMetadata, + onOpenTasks, + onReadDenied, + permission, + recoverySurface, + taskRuntime, + tasksOpen, +}: { + canDownload: boolean + ensureModelReady: EnsureKnowledgeModelReady + knowledgeSpaceId: string + metadataOpen: boolean + onOpenMetadata: () => void + onOpenTasks: () => void + onReadDenied: () => void + permission: DocumentUploadPermission + recoverySurface: DocumentPermissionRecoverySurface + taskRuntime: DocumentTaskRuntime + tasksOpen: boolean +}) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const queryClient = useQueryClient() + const { canRead, canWrite, denyWrite: onWriteDenied } = permission + const [filter, setFilter] = useQueryState('status', documentFilterParser) + const [search, setSearch] = useQueryState('query', documentSearchParser) + const [blockingDependencyRetries, setBlockingDependencyRetries] = useState({ + sources: false, + tasks: false, + }) + const mainRetryFocusRequestedRef = useRef(false) + const documentsRetryButtonRef = useRef(null) + const dependencyRetryButtonRef = useRef(null) + const documentsQuery = useInfiniteQuery(logicalDocumentsInfiniteOptions(knowledgeSpaceId)) + const documentPermissionDenied = responseStatus(documentsQuery.error) === 403 + const sourcesQuery = useInfiniteQuery( + documentSourcesInfiniteOptions(knowledgeSpaceId, { enabled: !documentPermissionDenied }), + ) + const { + acceptTaskSnapshot: onTaskUpdated, + activeTasks, + baseTasks, + drawerTasks, + tasks, + tasksQuery, + } = taskRuntime + const { + fetchNextPage: fetchNextDocumentPage, + hasNextPage: hasNextDocumentPage, + isFetchNextPageError: isFetchNextDocumentPageError, + isFetchingNextPage: isFetchingNextDocumentPage, + } = documentsQuery + const { + fetchNextPage: fetchNextSourcePage, + hasNextPage: hasNextSourcePage, + isFetchNextPageError: isFetchNextSourcePageError, + isFetchingNextPage: isFetchingNextSourcePage, + } = sourcesQuery + const canAutoFetchDocumentPage = Boolean( + hasNextDocumentPage && (documentsQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, + ) + const documents = useMemo( + () => + documentsQuery.data?.pages.flatMap((page) => logicalDocumentListFromApi(page).items) ?? [], + [documentsQuery.data], + ) + const documentIds = useMemo(() => new Set(documents.map((document) => document.id)), [documents]) + const unresolvedTaskDocumentIds = useMemo( + () => + new Set( + baseTasks.flatMap((task) => (!documentIds.has(task.documentId) ? [task.documentId] : [])), + ), + [baseTasks, documentIds], + ) + const sources = useMemo( + () => + sourcesQuery.data?.pages.flatMap((page) => + page.data.map((source) => sourceFromApi(source)), + ) ?? [], + [sourcesQuery.data], + ) + const sourceNames = useMemo( + () => new Map(sources.map((source) => [source.id, source.name])), + [sources], + ) + const unresolvedDocumentSourceIds = useMemo( + () => + new Set( + documents.flatMap((document) => + document.sourceId && !sourceNames.has(document.sourceId) ? [document.sourceId] : [], + ), + ), + [documents, sourceNames], + ) + const hasRelevantNextSourcePage = Boolean(hasNextSourcePage && unresolvedDocumentSourceIds.size) + const isFetchingNextResultsPage = Boolean( + isFetchingNextDocumentPage || (hasRelevantNextSourcePage && isFetchingNextSourcePage), + ) + const canAutoFetchSourcePage = Boolean( + hasRelevantNextSourcePage && (sourcesQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, + ) + const taskByDocument = useMemo(() => newestTaskByDocument(tasks), [tasks]) + const documentStatuses = useMemo( + () => + new Map( + documents.map((document) => [ + document.id, + documentDisplayStatus(document, taskByDocument.get(document.id)), + ]), + ), + [documents, taskByDocument], + ) + const documentFailureReasons = useMemo( + () => + new Map( + documents.flatMap((document) => { + if (documentStatuses.get(document.id) !== 'failed') return [] + const task = taskByDocument.get(document.id) + const messageKey = + knowledgeFsTaskFailureMessageKey( + task?.failure, + task?.errorCode ?? (task?.errorMessage ? 'LEGACY_TASK_FAILURE' : undefined), + ) ?? 'newKnowledge.taskFailure.internal' + return [[document.id, t(($) => $[messageKey])] as const] + }), + ), + [documentStatuses, documents, t, taskByDocument], + ) + const taskResultsIncomplete = Boolean(!tasksQuery.data || tasksQuery.isPending) + const filterActive = filter !== 'all' || Boolean(search.trim()) + const filteredDocuments = useMemo(() => { + const normalizedSearch = search.trim().toLocaleLowerCase() + return documents.filter((document) => { + const status = documentStatuses.get(document.id) + if (filter !== 'all' && status !== filter) return false + if (!normalizedSearch) return true + const resolvedSourceName = + (document.sourceId && sourceNames.get(document.sourceId)) ?? sourceName(document) + return `${document.title} ${resolvedSourceName ?? ''}` + .toLocaleLowerCase() + .includes(normalizedSearch) + }) + }, [documentStatuses, documents, filter, search, sourceNames]) + const completingFilteredResults = + filterActive && + !documentsQuery.isFetchNextPageError && + (canAutoFetchDocumentPage || documentsQuery.isFetchingNextPage) + const filteredResultsIncomplete = Boolean( + (filterActive && + (hasNextDocumentPage || + documentsQuery.isFetchingNextPage || + documentsQuery.isFetchNextPageError)) || + (filterActive && + unresolvedDocumentSourceIds.size > 0 && + (hasNextSourcePage || sourcesQuery.isFetchingNextPage || sourcesQuery.isFetchNextPageError)), + ) + const taskQueryBlockingError = Boolean( + !tasksQuery.data && (tasksQuery.error || blockingDependencyRetries.tasks), + ) + const sourceQueryBlockingError = Boolean( + !sourcesQuery.data && (sourcesQuery.error || blockingDependencyRetries.sources), + ) + const dependencyQueryBlockingError = taskQueryBlockingError || sourceQueryBlockingError + const dependencyQueryWarning = Boolean( + (tasksQuery.error && tasksQuery.data) || + (sourcesQuery.error && sourcesQuery.data) || + sourcesQuery.isFetchNextPageError, + ) + const sourceResultsIncomplete = Boolean( + !sourcesQuery.data || + sourcesQuery.isPending || + (unresolvedDocumentSourceIds.size && + (hasNextSourcePage || + sourcesQuery.error || + sourcesQuery.isFetchingNextPage || + sourcesQuery.isFetchNextPageError)), + ) + const sourceQueryWarning = Boolean( + (sourcesQuery.error && sourcesQuery.data) || sourcesQuery.isFetchNextPageError, + ) + const taskQueryWarning = Boolean(tasksQuery.error && tasksQuery.data) + const documentQueryWarning = Boolean(documentsQuery.error && documentsQuery.data) + const dependencyRetryFetching = Boolean( + (taskQueryWarning && tasksQuery.isFetching) || (sourceQueryWarning && sourcesQuery.isFetching), + ) + const blockingDependencyRetryFetching = Boolean( + (taskQueryBlockingError && tasksQuery.isFetching) || + (sourceQueryBlockingError && sourcesQuery.isFetching), + ) + const mainRecoveryVisible = Boolean( + documentsQuery.error || dependencyQueryBlockingError || dependencyQueryWarning, + ) + const mainRecoveryIdentity = [ + documentsQuery.error ? 'documents' : '', + taskQueryBlockingError ? 'tasks-blocking' : '', + sourceQueryBlockingError ? 'sources-blocking' : '', + taskQueryWarning ? 'tasks-warning' : '', + sourceQueryWarning ? 'sources-warning' : '', + ].join(':') + const dependencyResultsIncomplete = taskResultsIncomplete || sourceResultsIncomplete + const selectionDisabled = + !canWrite || + dependencyResultsIncomplete || + documentQueryWarning || + taskQueryWarning || + (sourceQueryWarning && unresolvedDocumentSourceIds.size > 0) || + filteredResultsIncomplete + const reindexUnavailableReason = + tasksQuery.error || tasksQuery.isFetchNextPageError + ? t(($) => $['newKnowledge.tasksErrorDescription']) + : (sourcesQuery.error || sourcesQuery.isFetchNextPageError) && + unresolvedDocumentSourceIds.size > 0 + ? t(($) => $['newKnowledge.sourcesErrorDescription']) + : documentsQuery.error || documentsQuery.isFetchNextPageError + ? t(($) => $['newKnowledge.documentsErrorDescription']) + : dependencyResultsIncomplete + ? tCommon(($) => $.loading) + : filteredResultsIncomplete + ? t(($) => $['newKnowledge.partialDocumentResults']) + : undefined + const bulkSelection = useDocumentBulkSelection({ + canSelect: canWrite && !selectionDisabled, + documents, + filteredDocuments, + statuses: documentStatuses, + taskResultsIncomplete, + }) + const attentionTasks = drawerTasks.filter(taskNeedsAttention) + const hasTaskError = attentionTasks.some( + (task) => task.state === 'failed' || task.state === 'canceled', + ) + const incompleteTaskHistoryHint = tasksQuery.hasNextPage + ? ` · ${t(($) => $['newKnowledge.taskHistoryIncomplete'])}` + : '' + const attentionTaskBadge = + attentionTasks.length || tasksQuery.hasNextPage + ? `${attentionTasks.length}${tasksQuery.hasNextPage ? '+' : ''}` + : undefined + const tasksButtonLabel = `${ + attentionTasks.length || tasksQuery.hasNextPage + ? t(($) => $['newKnowledge.tasksWithAttention'], { count: attentionTasks.length }) + : t(($) => $['newKnowledge.tasks']) + }${incompleteTaskHistoryHint}` + const tasksLiveStatus = `${ + hasTaskError + ? t(($) => $['newKnowledge.taskAttentionErrorCount'], { count: attentionTasks.length }) + : attentionTasks.length || tasksQuery.hasNextPage + ? t(($) => $['newKnowledge.taskAttentionCount'], { count: attentionTasks.length }) + : t(($) => $['newKnowledge.taskAttentionClear']) + }${incompleteTaskHistoryHint}` + const bulkActionsVisible = canWrite && bulkSelection.selectedDocumentIds.size > 0 + + useEffect(() => { + if (!mainRetryFocusRequestedRef.current) return + if (mainRecoveryVisible) { + if (documentsQuery.error && canRead) documentsRetryButtonRef.current?.focus() + else dependencyRetryButtonRef.current?.focus() + return + } + mainRetryFocusRequestedRef.current = false + document.getElementById('new-knowledge-documents-title')?.focus() + }, [canRead, documentsQuery.error, mainRecoveryIdentity, mainRecoveryVisible]) + + useEffect(() => { + if (!blockingDependencyRetries.tasks && !blockingDependencyRetries.sources) return + if ( + (!blockingDependencyRetries.tasks || !tasksQuery.data) && + (!blockingDependencyRetries.sources || !sourcesQuery.data) + ) + return + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Successful first-page retries retire the locally stabilized blocking state. + setBlockingDependencyRetries((current) => ({ + sources: current.sources && !sourcesQuery.data, + tasks: current.tasks && !tasksQuery.data, + })) + }, [blockingDependencyRetries, sourcesQuery.data, tasksQuery.data]) + + useEffect(() => { + if ( + canRead && + (filterActive || (tasksOpen && unresolvedTaskDocumentIds.size > 0)) && + canAutoFetchDocumentPage && + !isFetchingNextDocumentPage && + !isFetchNextDocumentPageError + ) + void fetchNextDocumentPage() + }, [ + canAutoFetchDocumentPage, + canRead, + fetchNextDocumentPage, + filterActive, + isFetchNextDocumentPageError, + isFetchingNextDocumentPage, + tasksOpen, + unresolvedTaskDocumentIds, + ]) + + useEffect(() => { + if ( + canRead && + canAutoFetchSourcePage && + !isFetchingNextSourcePage && + !isFetchNextSourcePageError + ) + void fetchNextSourcePage() + }, [ + canAutoFetchSourcePage, + canRead, + fetchNextSourcePage, + isFetchNextSourcePageError, + isFetchingNextSourcePage, + ]) + + const refreshDocumentsAndTasks = () => { + void Promise.allSettled([ + queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.get.key(), + }), + queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.spaces.byControlSpaceId.backgroundTasks.get.key(), + }), + ]) + } + + const retryDependencyQueries = () => { + if (taskQueryBlockingError || sourceQueryBlockingError) + setBlockingDependencyRetries((current) => ({ + sources: current.sources || sourceQueryBlockingError, + tasks: current.tasks || taskQueryBlockingError, + })) + if (tasksQuery.error || taskQueryBlockingError) void tasksQuery.refetch() + if (sourcesQuery.isFetchNextPageError) void sourcesQuery.fetchNextPage() + else if (sourcesQuery.error || sourceQueryBlockingError) void sourcesQuery.refetch() + } + + const loadMoreResults = () => { + const requests: Promise[] = [] + if (hasNextDocumentPage && !isFetchingNextDocumentPage) requests.push(fetchNextDocumentPage()) + if (hasRelevantNextSourcePage && !isFetchingNextSourcePage) requests.push(fetchNextSourcePage()) + void Promise.allSettled(requests) + } + + return ( + + + + + {documentsQuery.error && + documentsQuery.data && + canRead && + !documentsQuery.isFetchNextPageError && ( +
+ + {t(($) => + responseStatus(documentsQuery.error) === 403 + ? $['newKnowledge.documentsPermissionDescription'] + : $['newKnowledge.documentsErrorDescription'], + )} + + {responseStatus(documentsQuery.error) !== 403 && ( + + )} +
+ )} + {canRead && !dependencyQueryBlockingError && dependencyQueryWarning && ( +
+ + {sourcesQuery.error || sourcesQuery.isFetchNextPageError + ? t(($) => $['newKnowledge.sourcesErrorDescription']) + : t(($) => $['newKnowledge.tasksErrorDescription'])} + + +
+ )} + {documentsQuery.isPending && canRead ? ( +
+ +
+ ) : !canRead ? ( + + ) : documentsQuery.error && !documentsQuery.data ? ( +
+ +

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

+

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

+ +
+ ) : dependencyQueryBlockingError ? ( +
+ +

+ {taskQueryBlockingError + ? t(($) => $['newKnowledge.tasksErrorDescription']) + : t(($) => $['newKnowledge.sourcesErrorDescription'])} +

+ +
+ ) : ( + + {(upload) => + !documents.length ? ( + upload.openUpload()} + onOpenMetadata={onOpenMetadata} + readOnlyReasonId={upload.uploadRestrictionReasonId} + uploading={upload.uploading} + /> + ) : ( + + newKnowledgeDocumentDetailPath(knowledgeSpaceId, documentId) + } + hasNextPage={Boolean(hasNextDocumentPage || hasRelevantNextSourcePage)} + hasSelectableDocuments={bulkSelection.hasSelectableDocuments} + hasTaskError={hasTaskError} + isFetchNextPageError={documentsQuery.isFetchNextPageError} + isFetchingNextDocumentPage={isFetchingNextDocumentPage} + isFetchingNextPage={isFetchingNextResultsPage} + knowledgeSpaceId={knowledgeSpaceId} + onAddDocument={() => upload.openUpload()} + onDocumentRemoved={bulkSelection.remove} + onFilterChange={setFilter} + onLoadMore={loadMoreResults} + onOpenMetadata={onOpenMetadata} + onOpenTasks={onOpenTasks} + onSearchChange={setSearch} + onSelectAll={bulkSelection.toggleAllFiltered} + onSelectDocument={bulkSelection.toggle} + onTaskUpdated={onTaskUpdated} + onWriteDenied={onWriteDenied} + readOnlyReasonId={upload.readOnlyReasonId} + resultsIncomplete={filteredResultsIncomplete} + search={search} + selectionDisabled={selectionDisabled} + selectedDocumentIds={bulkSelection.selectedDocumentIds} + showTasks={Boolean( + tasks.length || + tasksQuery.error || + tasksQuery.isFetchNextPageError || + tasksQuery.hasNextPage, + )} + someSelected={bulkSelection.someFilteredSelected} + sourcesPending={sourceResultsIncomplete} + sourceNames={sourceNames} + statusPending={dependencyResultsIncomplete} + statuses={documentStatuses} + tasksByDocument={taskByDocument} + tasksPending={taskResultsIncomplete} + tasksButtonLabel={tasksButtonLabel} + tasksLiveStatus={tasksLiveStatus} + uploadRestrictionReasonId={upload.uploadRestrictionReasonId} + uploading={upload.uploading} + /> + ) + } + + )} +
+ + {bulkActionsVisible && ( + + )} + +
+ ) +}