mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
refactor(knowledge-fs): isolate document results surface
This commit is contained in:
parent
b82c426285
commit
264e054707
@ -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<HTMLButtonElement>(null)
|
||||
const dependencyRetryButtonRef = useRef<HTMLButtonElement>(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<unknown>[] = []
|
||||
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 }
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<DocumentPermissionRecoveryBoundary
|
||||
bulkActionsVisible={bulkActionsVisible}
|
||||
<DocumentResultsSurface
|
||||
canDownload={canDownload}
|
||||
ensureModelReady={ensureModelReady}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
metadataOpen={metadataOpen}
|
||||
onOpenMetadata={() => setMetadataOpen(true)}
|
||||
onOpenTasks={() => setTasksOpen(true)}
|
||||
onReadDenied={handleReadPermissionDenied}
|
||||
readSurfaceOpen={tasksOpen || metadataOpen}
|
||||
permission={uploadPermission}
|
||||
recoverySurface={recoverySurface}
|
||||
>
|
||||
<DocumentUploadSurface
|
||||
bulkActionsVisible={bulkActionsVisible}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onUploadStarted={refreshDocumentsAndTasks}
|
||||
permission={uploadPermission}
|
||||
>
|
||||
<DocumentUploadHeader />
|
||||
<KnowledgeModelReadinessBanner capability="index" knowledgeSpaceId={knowledgeSpaceId} />
|
||||
{documentsQuery.error &&
|
||||
documentsQuery.data &&
|
||||
canRead &&
|
||||
!documentsQuery.isFetchNextPageError && (
|
||||
<div
|
||||
className="mt-4 flex items-center justify-between gap-3 rounded-lg border border-divider-regular bg-background-section px-3 py-2"
|
||||
role="alert"
|
||||
>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t(($) =>
|
||||
responseStatus(documentsQuery.error) === 403
|
||||
? $['newKnowledge.documentsPermissionDescription']
|
||||
: $['newKnowledge.documentsErrorDescription'],
|
||||
)}
|
||||
</span>
|
||||
{responseStatus(documentsQuery.error) !== 403 && (
|
||||
<Button
|
||||
ref={documentsRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
|
||||
aria-busy={documentsQuery.isRefetching}
|
||||
loading={documentsQuery.isRefetching}
|
||||
size="small"
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
void refetchDocumentsQuery()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{canRead && !dependencyQueryBlockingError && dependencyQueryWarning && (
|
||||
<div
|
||||
className="mt-4 flex items-center justify-between gap-3 rounded-lg border border-divider-regular bg-background-section px-3 py-2"
|
||||
role="alert"
|
||||
>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{sourcesQuery.error || sourcesQuery.isFetchNextPageError
|
||||
? t(($) => $['newKnowledge.sourcesErrorDescription'])
|
||||
: t(($) => $['newKnowledge.tasksErrorDescription'])}
|
||||
</span>
|
||||
<Button
|
||||
ref={dependencyRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${
|
||||
sourcesQuery.error || sourcesQuery.isFetchNextPageError
|
||||
? t(($) => $['newKnowledge.sourcesErrorDescription'])
|
||||
: t(($) => $['newKnowledge.tasksErrorDescription'])
|
||||
}`}
|
||||
aria-busy={dependencyRetryFetching}
|
||||
loading={dependencyRetryFetching}
|
||||
size="small"
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
retryDependencyQueries()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{documentsQuery.isPending && canRead ? (
|
||||
<div className="flex min-h-64 flex-1 items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
) : !canRead ? (
|
||||
<DocumentReadPermissionRecovery
|
||||
fetching={documentsQuery.isFetching}
|
||||
recoverySurface={recoverySurface}
|
||||
/>
|
||||
) : documentsQuery.error && !documentsQuery.data ? (
|
||||
<div
|
||||
className="flex min-h-64 flex-1 flex-col items-center justify-center px-6 text-center"
|
||||
role="alert"
|
||||
>
|
||||
<span aria-hidden className="i-ri-error-warning-line size-7 text-text-tertiary" />
|
||||
<h2 className="mt-3 title-xl-semi-bold text-text-primary">
|
||||
{t(($) => $['newKnowledge.documentsErrorTitle'])}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-md body-sm-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.documentsErrorDescription'])}
|
||||
</p>
|
||||
<Button
|
||||
ref={documentsRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
|
||||
aria-busy={documentsQuery.isFetching}
|
||||
className="mt-4"
|
||||
loading={documentsQuery.isFetching}
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
void refetchDocumentsQuery()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : dependencyQueryBlockingError ? (
|
||||
<div
|
||||
className="flex min-h-64 flex-1 flex-col items-center justify-center px-6 text-center"
|
||||
role="alert"
|
||||
>
|
||||
<span aria-hidden className="i-ri-error-warning-line size-7 text-text-tertiary" />
|
||||
<p className="mt-3 max-w-md body-sm-regular text-text-tertiary">
|
||||
{taskQueryBlockingError
|
||||
? t(($) => $['newKnowledge.tasksErrorDescription'])
|
||||
: t(($) => $['newKnowledge.sourcesErrorDescription'])}
|
||||
</p>
|
||||
<Button
|
||||
ref={dependencyRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${
|
||||
taskQueryBlockingError
|
||||
? t(($) => $['newKnowledge.tasksErrorDescription'])
|
||||
: t(($) => $['newKnowledge.sourcesErrorDescription'])
|
||||
}`}
|
||||
aria-busy={blockingDependencyRetryFetching}
|
||||
className="mt-4"
|
||||
loading={blockingDependencyRetryFetching}
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
retryDependencyQueries()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<DocumentUploadContent>
|
||||
{(upload) =>
|
||||
!documents.length ? (
|
||||
<DocumentsEmpty
|
||||
canEdit={upload.canUpload}
|
||||
onAddDocument={() => upload.openUpload()}
|
||||
onOpenMetadata={() => setMetadataOpen(true)}
|
||||
readOnlyReasonId={upload.uploadRestrictionReasonId}
|
||||
uploading={upload.uploading}
|
||||
/>
|
||||
) : (
|
||||
<DocumentsList
|
||||
activeTaskCount={activeTasks.length}
|
||||
allSelected={bulkSelection.allFilteredSelected}
|
||||
attentionTaskBadge={attentionTaskBadge}
|
||||
canDownload={canDownload}
|
||||
canEdit={canWrite}
|
||||
canUpload={upload.canUpload}
|
||||
completingResults={completingFilteredResults}
|
||||
documents={filteredDocuments}
|
||||
ensureModelReady={ensureModelReady}
|
||||
failureReasons={documentFailureReasons}
|
||||
filter={filter}
|
||||
getDocumentHref={(documentId) =>
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</DocumentUploadContent>
|
||||
)}
|
||||
</DocumentUploadSurface>
|
||||
<DocumentPermissionRecoveryBulkRegion>
|
||||
{bulkActionsVisible && (
|
||||
<DocumentBulkActionsToolbar
|
||||
canDownload={canDownload}
|
||||
canWrite={canWrite}
|
||||
disabled={selectionDisabled}
|
||||
disabledReason={reindexUnavailableReason}
|
||||
ensureModelReady={ensureModelReady}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onWriteDenied={handleWritePermissionDenied}
|
||||
selection={bulkSelection}
|
||||
/>
|
||||
)}
|
||||
</DocumentPermissionRecoveryBulkRegion>
|
||||
</DocumentPermissionRecoveryBoundary>
|
||||
taskRuntime={taskRuntime}
|
||||
tasksOpen={tasksOpen}
|
||||
/>
|
||||
<ProcessingTasksDrawer
|
||||
actionResultsValid={canRead}
|
||||
canEdit={canWrite}
|
||||
|
||||
635
web/features/new-rag/documents/results/surface.tsx
Normal file
635
web/features/new-rag/documents/results/surface.tsx
Normal file
@ -0,0 +1,635 @@
|
||||
'use client'
|
||||
|
||||
import type { EnsureKnowledgeModelReady } from '../../use-knowledge-model-setup-guard'
|
||||
import type { DocumentPermissionRecoverySurface } from '../permission-recovery/recovery-boundary'
|
||||
import type { useTaskRuntime } from '../tasks/use-task-runtime'
|
||||
import type { DocumentUploadPermission } from '../upload/surface'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { KnowledgeModelReadinessBanner } from '../../components/knowledge-model-readiness-banner'
|
||||
import { knowledgeFsTaskFailureMessageKey } from '../../knowledge-fs-task-error'
|
||||
import { newKnowledgeDocumentDetailPath } from '../../routes'
|
||||
import { sourceFromApi } from '../../sources/source-models'
|
||||
import { useDocumentBulkSelection } from '../bulk/selection-state'
|
||||
import { DocumentBulkActionsToolbar } from '../bulk/toolbar'
|
||||
import { DocumentsEmpty, DocumentsList } from '../list'
|
||||
import {
|
||||
documentDisplayStatus,
|
||||
newestTaskByDocument,
|
||||
sourceName,
|
||||
taskNeedsAttention,
|
||||
} from '../model'
|
||||
import { logicalDocumentListFromApi } from '../models'
|
||||
import {
|
||||
DocumentPermissionRecoveryBoundary,
|
||||
DocumentPermissionRecoveryBulkRegion,
|
||||
DocumentReadPermissionRecovery,
|
||||
} from '../permission-recovery/recovery-boundary'
|
||||
import { documentSourcesInfiniteOptions, logicalDocumentsInfiniteOptions } from '../queries'
|
||||
import { documentFilterParser, documentSearchParser } from '../query-state'
|
||||
import { responseStatus } from '../request-error'
|
||||
import { MAX_AUTO_CURSOR_PAGES, queryKeyMatchesKnowledgeSpace } from '../tasks/recovery'
|
||||
import {
|
||||
DocumentUploadContent,
|
||||
DocumentUploadHeader,
|
||||
DocumentUploadSurface,
|
||||
} from '../upload/surface'
|
||||
|
||||
type DocumentTaskRuntime = ReturnType<typeof useTaskRuntime>
|
||||
|
||||
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<HTMLButtonElement>(null)
|
||||
const dependencyRetryButtonRef = useRef<HTMLButtonElement>(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<unknown>[] = []
|
||||
if (hasNextDocumentPage && !isFetchingNextDocumentPage) requests.push(fetchNextDocumentPage())
|
||||
if (hasRelevantNextSourcePage && !isFetchingNextSourcePage) requests.push(fetchNextSourcePage())
|
||||
void Promise.allSettled(requests)
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentPermissionRecoveryBoundary
|
||||
bulkActionsVisible={bulkActionsVisible}
|
||||
onReadDenied={onReadDenied}
|
||||
readSurfaceOpen={tasksOpen || metadataOpen}
|
||||
recoverySurface={recoverySurface}
|
||||
>
|
||||
<DocumentUploadSurface
|
||||
bulkActionsVisible={bulkActionsVisible}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onUploadStarted={refreshDocumentsAndTasks}
|
||||
permission={permission}
|
||||
>
|
||||
<DocumentUploadHeader />
|
||||
<KnowledgeModelReadinessBanner capability="index" knowledgeSpaceId={knowledgeSpaceId} />
|
||||
{documentsQuery.error &&
|
||||
documentsQuery.data &&
|
||||
canRead &&
|
||||
!documentsQuery.isFetchNextPageError && (
|
||||
<div
|
||||
className="mt-4 flex items-center justify-between gap-3 rounded-lg border border-divider-regular bg-background-section px-3 py-2"
|
||||
role="alert"
|
||||
>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t(($) =>
|
||||
responseStatus(documentsQuery.error) === 403
|
||||
? $['newKnowledge.documentsPermissionDescription']
|
||||
: $['newKnowledge.documentsErrorDescription'],
|
||||
)}
|
||||
</span>
|
||||
{responseStatus(documentsQuery.error) !== 403 && (
|
||||
<Button
|
||||
ref={documentsRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
|
||||
aria-busy={documentsQuery.isRefetching}
|
||||
loading={documentsQuery.isRefetching}
|
||||
size="small"
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
void documentsQuery.refetch()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{canRead && !dependencyQueryBlockingError && dependencyQueryWarning && (
|
||||
<div
|
||||
className="mt-4 flex items-center justify-between gap-3 rounded-lg border border-divider-regular bg-background-section px-3 py-2"
|
||||
role="alert"
|
||||
>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{sourcesQuery.error || sourcesQuery.isFetchNextPageError
|
||||
? t(($) => $['newKnowledge.sourcesErrorDescription'])
|
||||
: t(($) => $['newKnowledge.tasksErrorDescription'])}
|
||||
</span>
|
||||
<Button
|
||||
ref={dependencyRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${
|
||||
sourcesQuery.error || sourcesQuery.isFetchNextPageError
|
||||
? t(($) => $['newKnowledge.sourcesErrorDescription'])
|
||||
: t(($) => $['newKnowledge.tasksErrorDescription'])
|
||||
}`}
|
||||
aria-busy={dependencyRetryFetching}
|
||||
loading={dependencyRetryFetching}
|
||||
size="small"
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
retryDependencyQueries()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{documentsQuery.isPending && canRead ? (
|
||||
<div className="flex min-h-64 flex-1 items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
) : !canRead ? (
|
||||
<DocumentReadPermissionRecovery
|
||||
fetching={documentsQuery.isFetching}
|
||||
recoverySurface={recoverySurface}
|
||||
/>
|
||||
) : documentsQuery.error && !documentsQuery.data ? (
|
||||
<div
|
||||
className="flex min-h-64 flex-1 flex-col items-center justify-center px-6 text-center"
|
||||
role="alert"
|
||||
>
|
||||
<span aria-hidden className="i-ri-error-warning-line size-7 text-text-tertiary" />
|
||||
<h2 className="mt-3 title-xl-semi-bold text-text-primary">
|
||||
{t(($) => $['newKnowledge.documentsErrorTitle'])}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-md body-sm-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.documentsErrorDescription'])}
|
||||
</p>
|
||||
<Button
|
||||
ref={documentsRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
|
||||
aria-busy={documentsQuery.isFetching}
|
||||
className="mt-4"
|
||||
loading={documentsQuery.isFetching}
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
void documentsQuery.refetch()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : dependencyQueryBlockingError ? (
|
||||
<div
|
||||
className="flex min-h-64 flex-1 flex-col items-center justify-center px-6 text-center"
|
||||
role="alert"
|
||||
>
|
||||
<span aria-hidden className="i-ri-error-warning-line size-7 text-text-tertiary" />
|
||||
<p className="mt-3 max-w-md body-sm-regular text-text-tertiary">
|
||||
{taskQueryBlockingError
|
||||
? t(($) => $['newKnowledge.tasksErrorDescription'])
|
||||
: t(($) => $['newKnowledge.sourcesErrorDescription'])}
|
||||
</p>
|
||||
<Button
|
||||
ref={dependencyRetryButtonRef}
|
||||
aria-label={`${tCommon(($) => $['operation.retry'])} · ${
|
||||
taskQueryBlockingError
|
||||
? t(($) => $['newKnowledge.tasksErrorDescription'])
|
||||
: t(($) => $['newKnowledge.sourcesErrorDescription'])
|
||||
}`}
|
||||
aria-busy={blockingDependencyRetryFetching}
|
||||
className="mt-4"
|
||||
loading={blockingDependencyRetryFetching}
|
||||
onBlur={(event) => {
|
||||
if (event.relatedTarget) mainRetryFocusRequestedRef.current = false
|
||||
}}
|
||||
onClick={() => {
|
||||
mainRetryFocusRequestedRef.current = true
|
||||
retryDependencyQueries()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<DocumentUploadContent>
|
||||
{(upload) =>
|
||||
!documents.length ? (
|
||||
<DocumentsEmpty
|
||||
canEdit={upload.canUpload}
|
||||
onAddDocument={() => upload.openUpload()}
|
||||
onOpenMetadata={onOpenMetadata}
|
||||
readOnlyReasonId={upload.uploadRestrictionReasonId}
|
||||
uploading={upload.uploading}
|
||||
/>
|
||||
) : (
|
||||
<DocumentsList
|
||||
activeTaskCount={activeTasks.length}
|
||||
allSelected={bulkSelection.allFilteredSelected}
|
||||
attentionTaskBadge={attentionTaskBadge}
|
||||
canDownload={canDownload}
|
||||
canEdit={canWrite}
|
||||
canUpload={upload.canUpload}
|
||||
completingResults={completingFilteredResults}
|
||||
documents={filteredDocuments}
|
||||
ensureModelReady={ensureModelReady}
|
||||
failureReasons={documentFailureReasons}
|
||||
filter={filter}
|
||||
getDocumentHref={(documentId) =>
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</DocumentUploadContent>
|
||||
)}
|
||||
</DocumentUploadSurface>
|
||||
<DocumentPermissionRecoveryBulkRegion>
|
||||
{bulkActionsVisible && (
|
||||
<DocumentBulkActionsToolbar
|
||||
canDownload={canDownload}
|
||||
canWrite={canWrite}
|
||||
disabled={selectionDisabled}
|
||||
disabledReason={reindexUnavailableReason}
|
||||
ensureModelReady={ensureModelReady}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onWriteDenied={onWriteDenied}
|
||||
selection={bulkSelection}
|
||||
/>
|
||||
)}
|
||||
</DocumentPermissionRecoveryBulkRegion>
|
||||
</DocumentPermissionRecoveryBoundary>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user