'use client' import { Button } from '@langgenius/dify-ui/button' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { createParser, useQueryState } from 'nuqs' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { datasetDefaultPermissionKeysAtom } from '@/context/permission-state' import useDocumentTitle from '@/hooks/use-document-title' import { consoleQuery } from '@/service/client' import { DatasetACLPermission, hasPermission } from '@/utils/permission' import { DocumentDetailHeader } from './document-detail-header' import { initialDocumentRevision, responseStatus } from './document-detail-model' import { DocumentDetailStatus } from './document-detail-status' import { DocumentRevisionContent } from './document-revision-content' import { newKnowledgeDocumentsPath } from './routes' import { useDocumentReindex } from './use-document-reindex' const REVISION_PAGE_SIZE = 50 const REINDEX_RESTRICTION_ID = 'document-reindex-restriction' const documentRevisionParser = createParser({ parse: (value) => { const revision = Number(value) return Number.isInteger(revision) && revision > 0 ? revision : null }, serialize: String, }).withOptions({ history: 'push' }) function ErrorState({ description, onRetry, title, }: { description: string onRetry?: () => void title: string }) { const { t: tCommon } = useTranslation('common') return (

{title}

{description}

{onRetry && ( )}
) } export function DocumentDetailPage({ documentId, knowledgeSpaceId, }: { documentId: string knowledgeSpaceId: string }) { const { i18n, t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') const permissionKeys = useAtomValue(datasetDefaultPermissionKeysAtom) const [selectedRevision, setSelectedRevision] = useQueryState('revision', documentRevisionParser) const titleRef = useRef(null) const documentQueryOptions = useMemo( () => consoleQuery.knowledgeFs.getKnowledgeSpacesByIdLogicalDocumentsByDocumentId.queryOptions({ input: { params: { documentId, id: knowledgeSpaceId } }, retry: (failureCount, error) => { const status = responseStatus(error) return status !== 403 && status !== 404 && failureCount < 2 }, }), [documentId, knowledgeSpaceId], ) const documentQuery = useQuery(documentQueryOptions) const knowledgeSpaceQuery = useQuery( consoleQuery.knowledgeFs.getKnowledgeSpacesById.queryOptions({ input: { params: { id: knowledgeSpaceId } }, }), ) const documentTitle = documentQuery.data?.title ?? t(($) => $['newKnowledge.documents']) const knowledgeSpaceTitle = knowledgeSpaceQuery.data?.name ?? t(($) => $.knowledge) useDocumentTitle(`${documentTitle} ยท ${knowledgeSpaceTitle}`) const revisionsQueryOptions = useMemo( () => consoleQuery.knowledgeFs.getKnowledgeSpacesByIdDocumentsByDocumentIdRevisions.infiniteOptions( { input: (pageParam) => ({ params: { documentId, id: knowledgeSpaceId }, query: { limit: REVISION_PAGE_SIZE, ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}), }, }), getNextPageParam: (lastPage) => lastPage.nextCursor, initialPageParam: null as string | null, }, ), [documentId, knowledgeSpaceId], ) const revisionsQuery = useInfiniteQuery(revisionsQueryOptions) const revisions = useMemo( () => revisionsQuery.data?.pages.flatMap((page) => page.items).filter(Boolean) ?? [], [revisionsQuery.data], ) const availableRevisions = useMemo(() => { const byRevision = new Map(revisions.map((revision) => [revision.revision, revision])) if (documentQuery.data?.active) byRevision.set(documentQuery.data.active.revision, documentQuery.data.active) return [...byRevision.values()].sort((left, right) => right.revision - left.revision) }, [documentQuery.data?.active, revisions]) const effectiveRevision = documentQuery.data ? (selectedRevision ?? initialDocumentRevision(documentQuery.data, availableRevisions)) : undefined const chunksQueryKey = consoleQuery.knowledgeFs.getKnowledgeSpacesByIdDocumentsByDocumentIdRevisionsByRevisionChunks.key() const documentActiveRevision = documentQuery.data?.activeRevision ?? documentQuery.data?.active?.revision ?? 0 const documentErrorStatus = responseStatus(documentQuery.error) const { continueLookup, documentMissing, isFetchingNextPage: isFetchingNextTaskPage, isLookingUp: isLookingUpTask, isPending: tasksPending, latestTask, lookupExhausted, permissionRecoveryBusy, permissionRecoveryNeeded, refetch: refetchTasks, recheckTimedOutSubmission, reindex, reindexBusy, retryTimedOutSubmission, retryWritePermission, submissionPending, submissionRecoveryBusy, submissionTimedOut, taskIsActive, tasksError, writePermissionRevoked, } = useDocumentReindex({ documentActiveRevision, chunksQueryKey, documentId, documentQueryKey: documentQueryOptions.queryKey, enabled: Boolean(documentQuery.data) && documentErrorStatus !== 403 && documentErrorStatus !== 404, knowledgeSpaceId, revisionsQueryKey: revisionsQueryOptions.queryKey, }) const hasEditPermission = hasPermission(permissionKeys, DatasetACLPermission.Edit) const canEdit = hasEditPermission && !writePermissionRevoked const activeRevision = availableRevisions.find( (revision) => revision.revision === effectiveRevision, ) const backPath = newKnowledgeDocumentsPath(knowledgeSpaceId) const locale = i18n.resolvedLanguage ?? i18n.language if (documentQuery.isPending) return (
{tCommon(($) => $.loading)}
) if (documentMissing || documentErrorStatus === 403 || documentErrorStatus === 404) return ( $['newKnowledge.documentNotFoundDescription'])} title={t(($) => $['newKnowledge.documentNotFoundTitle'])} /> ) if (!documentQuery.data) { return ( $['newKnowledge.documentLoadErrorDescription'])} onRetry={() => void documentQuery.refetch()} title={t(($) => $['newKnowledge.documentLoadErrorTitle'])} /> ) } const document = documentQuery.data return (
void revisionsQuery.fetchNextPage()} hasNextRevisionPage={revisionsQuery.hasNextPage} isFetchNextRevisionPageError={revisionsQuery.isFetchNextPageError} isFetchingNextRevisionPage={revisionsQuery.isFetchingNextPage} onReindex={() => void reindex()} onRevisionChange={(revision) => void setSelectedRevision(revision)} reindexDisabled={ !canEdit || taskIsActive || tasksPending || isFetchingNextTaskPage || isLookingUpTask || lookupExhausted || document.status === 'deleting' || Boolean(tasksError) } reindexDisabledReasonId={!hasEditPermission ? REINDEX_RESTRICTION_ID : undefined} reindexing={reindexBusy || submissionPending} revisions={availableRevisions} titleRef={titleRef} /> {!hasEditPermission && (

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

)} void revisionsQuery.refetch()} refetchTasks={() => void refetchTasks()} retryTimedOutSubmission={retryTimedOutSubmission} retryWritePermission={retryWritePermission} revisionHistoryBackgroundError={Boolean( revisionsQuery.error && !revisionsQuery.isFetchNextPageError, )} submissionRecoveryBusy={submissionRecoveryBusy} submissionTimedOut={submissionTimedOut} taskIsActive={taskIsActive} tasksError={Boolean(tasksError)} titleRef={titleRef} /> void revisionsQuery.refetch()} />
) }