mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
refactor(knowledge-fs): model document revision state
This commit is contained in:
parent
0ece06dde5
commit
e1aed49d40
@ -344,6 +344,42 @@ vi.mock('jotai-tanstack-query', async (importOriginal) => {
|
||||
|
||||
return {
|
||||
...original,
|
||||
atomWithInfiniteQuery: (
|
||||
getOptions: (get: <Value>(target: import('jotai').Atom<Value>) => Value) => {
|
||||
queryKind?: string
|
||||
},
|
||||
) =>
|
||||
atom((get) => {
|
||||
get(versionAtom)
|
||||
const options = getOptions(get)
|
||||
if (options.queryKind === 'revisions')
|
||||
return {
|
||||
...revisionsQuery,
|
||||
data: revisionsQuery.data
|
||||
? {
|
||||
pages: revisionsQuery.data.pages.map((page) => ({
|
||||
data: page.items.flatMap((revision) =>
|
||||
revision ? [revisionApiResponse(revision)] : [],
|
||||
),
|
||||
next_cursor: page.nextCursor ?? null,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
if (options.queryKind === 'chunks')
|
||||
return {
|
||||
...chunksQuery,
|
||||
data: chunksQuery.data
|
||||
? {
|
||||
pages: chunksQuery.data.pages.map((page) => ({
|
||||
data: page.items.map(chunkApiResponse),
|
||||
next_cursor: page.nextCursor ?? null,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
throw new Error(`Unexpected infinite query atom: ${options.queryKind ?? 'unknown'}`)
|
||||
}),
|
||||
atomWithQuery: (
|
||||
getOptions: (get: <Value>(target: import('jotai').Atom<Value>) => Value) => {
|
||||
queryKind?: string
|
||||
@ -353,6 +389,8 @@ vi.mock('jotai-tanstack-query', async (importOriginal) => {
|
||||
get(versionAtom)
|
||||
const options = getOptions(get)
|
||||
if (options.queryKind === 'document') return { ...documentQuery }
|
||||
if (options.queryKind === 'outline') return { ...outlineQuery }
|
||||
if (options.queryKind === 'multimodal') return { ...multimodalQuery }
|
||||
throw new Error(`Unexpected query atom: ${options.queryKind ?? 'unknown'}`)
|
||||
}),
|
||||
}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
import type { KnowledgeFsDocumentMultimodalItemResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import type { DocumentRevisionChunk, LogicalDocument, LogicalDocumentRevision } from '../models'
|
||||
import type { DocumentContentBlock } from './model'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
@ -12,12 +9,23 @@ import {
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { DocumentMetadataCard } from '../metadata/card'
|
||||
import { chunkCharacterCount, placeDocumentMultimodalItems } from './model'
|
||||
import { DocumentMultimodalAsset } from './multimodal-asset'
|
||||
import {
|
||||
documentChunksQueryIsFetchingNextPageAtom,
|
||||
documentDetailChunksCompleteAtom,
|
||||
documentDetailModelAtom,
|
||||
documentDetailMultimodalItemsAtom,
|
||||
documentDetailSelectedChunkIdAtom,
|
||||
} from './state/content'
|
||||
import { documentDetailKnowledgeSpaceIdAtom } from './state/inputs'
|
||||
import { documentDetailDocumentAtom } from './state/queries'
|
||||
import { documentDetailRevisionAtom } from './state/revisions'
|
||||
import { useDocumentWriteAccess } from './workflow-context'
|
||||
|
||||
const SELECTED_CHUNK_TOP_OFFSET = 8
|
||||
@ -115,19 +123,13 @@ function DocumentSectionSummary({ children }: { children: React.ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DocumentReadingPane({
|
||||
contentBlocks,
|
||||
isLoadingMore,
|
||||
multimodalItems,
|
||||
selectedChunkId,
|
||||
}: {
|
||||
contentBlocks: DocumentContentBlock[]
|
||||
isLoadingMore: boolean
|
||||
multimodalItems: KnowledgeFsDocumentMultimodalItemResponse[]
|
||||
selectedChunkId?: string
|
||||
}) {
|
||||
export function DocumentReadingPane() {
|
||||
const { t } = useTranslation('dataset')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const contentBlocks = useAtomValue(documentDetailModelAtom).contentBlocks
|
||||
const isLoadingMore = useAtomValue(documentChunksQueryIsFetchingNextPageAtom)
|
||||
const multimodalItems = useAtomValue(documentDetailMultimodalItemsAtom)
|
||||
const selectedChunkId = useAtomValue(documentDetailSelectedChunkIdAtom)
|
||||
const contentScrollRef = useRef<HTMLDivElement>(null)
|
||||
const contentChunks = useMemo(() => contentBlocks.map((block) => block.chunk), [contentBlocks])
|
||||
const multimodalPlacement = useMemo(
|
||||
@ -271,24 +273,16 @@ export function DocumentReadingPane({
|
||||
)
|
||||
}
|
||||
|
||||
export function DocumentFactsSidebar({
|
||||
chunksComplete,
|
||||
controlSpaceId,
|
||||
document,
|
||||
indexChunks,
|
||||
locale,
|
||||
revision,
|
||||
}: {
|
||||
chunksComplete: boolean
|
||||
controlSpaceId: string
|
||||
document: LogicalDocument
|
||||
indexChunks: DocumentRevisionChunk[]
|
||||
locale: string
|
||||
revision?: Exclude<LogicalDocumentRevision, null>
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
export function DocumentFactsSidebar() {
|
||||
const { i18n, t } = useTranslation('dataset')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { canEdit } = useDocumentWriteAccess()
|
||||
const chunksComplete = useAtomValue(documentDetailChunksCompleteAtom)
|
||||
const controlSpaceId = useAtomValue(documentDetailKnowledgeSpaceIdAtom)
|
||||
const document = useAtomValue(documentDetailDocumentAtom)
|
||||
const indexChunks = useAtomValue(documentDetailModelAtom).indexChunks
|
||||
const revision = useAtomValue(documentDetailRevisionAtom)
|
||||
const locale = i18n.resolvedLanguage ?? i18n.language
|
||||
const characterCount = useMemo(
|
||||
() => indexChunks.reduce((total, chunk) => total + chunkCharacterCount(chunk.text), 0),
|
||||
[indexChunks],
|
||||
|
||||
@ -1,13 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import type { DocumentChunkTree } from './model'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { chunkTreeLabel, visibleDocumentChunkNodes } from './model'
|
||||
import {
|
||||
documentChunksQueryErrorAtom,
|
||||
documentChunksQueryHasNextPageAtom,
|
||||
documentChunksQueryIsFetchingNextPageAtom,
|
||||
documentChunksQueryIsFetchNextPageErrorAtom,
|
||||
documentChunksQueryIsPendingAtom,
|
||||
documentDetailChunksAtom,
|
||||
documentDetailModelAtom,
|
||||
documentDetailSelectedChunkIdAtom,
|
||||
loadNextDocumentChunkPageAtom,
|
||||
retryDocumentChunksAtom,
|
||||
} from './state/content'
|
||||
import { selectDocumentChunkAtom } from './state/runtime'
|
||||
|
||||
const VIRTUALIZATION_THRESHOLD = 80
|
||||
const TREE_ROW_SIZE = 30
|
||||
@ -51,33 +64,20 @@ function AutomaticChunkPageLoader({
|
||||
return <div ref={sentinelRef} aria-hidden className="h-px" />
|
||||
}
|
||||
|
||||
export function DocumentChunkTreePanel({
|
||||
chunkCount,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchNextPageError,
|
||||
isFetchingNextPage,
|
||||
isPending,
|
||||
onRetry,
|
||||
onSelectChunk,
|
||||
selectedChunkId,
|
||||
tree,
|
||||
}: {
|
||||
chunkCount: number
|
||||
error: boolean
|
||||
fetchNextPage: () => Promise<unknown>
|
||||
hasNextPage: boolean
|
||||
isFetchNextPageError: boolean
|
||||
isFetchingNextPage: boolean
|
||||
isPending: boolean
|
||||
onRetry: () => void
|
||||
onSelectChunk: (chunkId: string) => void
|
||||
selectedChunkId?: string
|
||||
tree: DocumentChunkTree
|
||||
}) {
|
||||
export function DocumentChunkTreePanel() {
|
||||
const { t } = useTranslation('dataset')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const chunkCount = useAtomValue(documentDetailChunksAtom).length
|
||||
const error = Boolean(useAtomValue(documentChunksQueryErrorAtom))
|
||||
const hasNextPage = useAtomValue(documentChunksQueryHasNextPageAtom)
|
||||
const isFetchNextPageError = useAtomValue(documentChunksQueryIsFetchNextPageErrorAtom)
|
||||
const isFetchingNextPage = useAtomValue(documentChunksQueryIsFetchingNextPageAtom)
|
||||
const isPending = useAtomValue(documentChunksQueryIsPendingAtom)
|
||||
const selectedChunkId = useAtomValue(documentDetailSelectedChunkIdAtom)
|
||||
const tree = useAtomValue(documentDetailModelAtom).tree
|
||||
const fetchNextPage = useSetAtom(loadNextDocumentChunkPageAtom)
|
||||
const retryChunks = useSetAtom(retryDocumentChunksAtom)
|
||||
const selectChunk = useSetAtom(selectDocumentChunkAtom)
|
||||
const [expansionOverrides, setExpansionOverrides] = useState<{
|
||||
collapsed: Set<string>
|
||||
expanded: Set<string>
|
||||
@ -156,7 +156,7 @@ export function DocumentChunkTreePanel({
|
||||
|
||||
const selectNode = (node: (typeof visibleNodes)[number]['node']) => {
|
||||
setSelectedNodeId(node.id)
|
||||
onSelectChunk(node.targetChunkId)
|
||||
void selectChunk(node.targetChunkId)
|
||||
}
|
||||
|
||||
const handleTreeKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
@ -241,7 +241,7 @@ export function DocumentChunkTreePanel({
|
||||
role="alert"
|
||||
>
|
||||
<span>{t(($) => $['newKnowledge.documentChunksLoadError'])}</span>
|
||||
<Button onClick={onRetry}>{tCommon(($) => $['operation.retry'])}</Button>
|
||||
<Button onClick={() => void retryChunks()}>{tCommon(($) => $['operation.retry'])}</Button>
|
||||
</div>
|
||||
)}
|
||||
{isPending ? (
|
||||
@ -254,7 +254,7 @@ export function DocumentChunkTreePanel({
|
||||
<p className="system-xs-regular text-text-destructive">
|
||||
{t(($) => $['newKnowledge.documentChunksLoadError'])}
|
||||
</p>
|
||||
<Button className="mt-3" onClick={onRetry}>
|
||||
<Button className="mt-3" onClick={() => void retryChunks()}>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
export function documentChunksQueryOptions({
|
||||
documentId,
|
||||
effectiveRevision,
|
||||
knowledgeSpaceId,
|
||||
}: {
|
||||
documentId: string
|
||||
effectiveRevision: number
|
||||
knowledgeSpaceId: string
|
||||
}) {
|
||||
const chunksQuery =
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.revisions.byRevision
|
||||
.chunks
|
||||
|
||||
return chunksQuery.get.infiniteOptions({
|
||||
input: (pageParam) => ({
|
||||
params: {
|
||||
control_space_id: knowledgeSpaceId,
|
||||
document_id: documentId,
|
||||
revision: effectiveRevision,
|
||||
},
|
||||
query: {
|
||||
...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
|
||||
},
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage.next_cursor,
|
||||
initialPageParam: null as string | null,
|
||||
})
|
||||
}
|
||||
|
||||
export function documentOutlineQueryOptions({
|
||||
documentAssetId,
|
||||
knowledgeSpaceId,
|
||||
}: {
|
||||
documentAssetId?: string
|
||||
knowledgeSpaceId: string
|
||||
}) {
|
||||
const outlineQuery =
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.outline
|
||||
|
||||
return outlineQuery.get.queryOptions({
|
||||
context: { silent: true },
|
||||
input: documentAssetId
|
||||
? {
|
||||
params: {
|
||||
control_space_id: knowledgeSpaceId,
|
||||
document_id: documentAssetId,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function documentMultimodalQueryOptions({
|
||||
documentAssetId,
|
||||
knowledgeSpaceId,
|
||||
}: {
|
||||
documentAssetId?: string
|
||||
knowledgeSpaceId: string
|
||||
}) {
|
||||
const multimodalQuery =
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.multimodal
|
||||
|
||||
return multimodalQuery.get.queryOptions({
|
||||
context: { silent: true },
|
||||
input: documentAssetId
|
||||
? {
|
||||
params: {
|
||||
control_space_id: knowledgeSpaceId,
|
||||
document_id: documentAssetId,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
@ -1,27 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { createParser, parseAsString, useQueryStates } from 'nuqs'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { documentRevisionListFromApi } from '../models'
|
||||
import { initialDocumentRevision } from './model'
|
||||
import { DocumentRevisionData } from './revision-content'
|
||||
import { documentDetailKnowledgeSpaceIdAtom } from './state/inputs'
|
||||
import { documentDetailDocumentAtom } from './state/queries'
|
||||
|
||||
const documentRevisionParser = createParser<number>({
|
||||
parse: (value) => {
|
||||
const revision = Number(value)
|
||||
return Number.isInteger(revision) && revision > 0 ? revision : null
|
||||
},
|
||||
serialize: String,
|
||||
}).withOptions({ history: 'push' })
|
||||
const documentChunkParser = parseAsString.withOptions({ history: 'replace' })
|
||||
import { documentDetailRequestedRevisionAtom } from './state/inputs'
|
||||
import {
|
||||
documentDetailEffectiveRevisionAtom,
|
||||
documentDetailRevisionAtom,
|
||||
documentRevisionsQueryErrorAtom,
|
||||
documentRevisionsQueryHasNextPageAtom,
|
||||
documentRevisionsQueryIsFetchingNextPageAtom,
|
||||
documentRevisionsQueryIsFetchNextPageErrorAtom,
|
||||
documentRevisionsQueryIsPendingAtom,
|
||||
loadNextDocumentRevisionPageAtom,
|
||||
retryDocumentRevisionsAtom,
|
||||
} from './state/revisions'
|
||||
|
||||
function RevisionLoadingState() {
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
@ -60,88 +56,51 @@ function RevisionErrorState({
|
||||
}
|
||||
|
||||
export function DocumentRevisionBrowser() {
|
||||
const { i18n, t } = useTranslation('dataset')
|
||||
const { t } = useTranslation('dataset')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const document = useAtomValue(documentDetailDocumentAtom)
|
||||
const knowledgeSpaceId = useAtomValue(documentDetailKnowledgeSpaceIdAtom)
|
||||
const locale = i18n.resolvedLanguage ?? i18n.language
|
||||
const [documentLocation, setDocumentLocation] = useQueryStates({
|
||||
chunk: documentChunkParser,
|
||||
revision: documentRevisionParser,
|
||||
})
|
||||
const { chunk: selectedChunkId, revision: selectedRevision } = documentLocation
|
||||
const revisionsQuery = useInfiniteQuery(
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.revisions.get.infiniteOptions(
|
||||
{
|
||||
input: (pageParam) => ({
|
||||
params: {
|
||||
control_space_id: knowledgeSpaceId,
|
||||
document_id: document.id,
|
||||
},
|
||||
query: {
|
||||
...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
|
||||
},
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage.next_cursor,
|
||||
initialPageParam: null as string | null,
|
||||
},
|
||||
),
|
||||
)
|
||||
const { fetchNextPage, hasNextPage, isFetchNextPageError, isFetchingNextPage } = revisionsQuery
|
||||
const revisions = useMemo(
|
||||
() =>
|
||||
revisionsQuery.data?.pages.flatMap((page) => documentRevisionListFromApi(page).items) ?? [],
|
||||
[revisionsQuery.data],
|
||||
)
|
||||
const availableRevisions = useMemo(() => {
|
||||
const byRevision = new Map(revisions.map((revision) => [revision.revision, revision]))
|
||||
if (document.active) byRevision.set(document.active.revision, document.active)
|
||||
return [...byRevision.values()].sort((left, right) => right.revision - left.revision)
|
||||
}, [document.active, revisions])
|
||||
const requestedRevision =
|
||||
selectedRevision ?? initialDocumentRevision(document, availableRevisions)
|
||||
const revision = availableRevisions.find((candidate) => candidate.revision === requestedRevision)
|
||||
const effectiveRevision = revision?.revision
|
||||
const requestedRevision = useAtomValue(documentDetailRequestedRevisionAtom)
|
||||
const revision = useAtomValue(documentDetailRevisionAtom)
|
||||
const effectiveRevision = useAtomValue(documentDetailEffectiveRevisionAtom)
|
||||
const error = useAtomValue(documentRevisionsQueryErrorAtom)
|
||||
const hasNextPage = useAtomValue(documentRevisionsQueryHasNextPageAtom)
|
||||
const isFetchNextPageError = useAtomValue(documentRevisionsQueryIsFetchNextPageErrorAtom)
|
||||
const isFetchingNextPage = useAtomValue(documentRevisionsQueryIsFetchingNextPageAtom)
|
||||
const isPending = useAtomValue(documentRevisionsQueryIsPendingAtom)
|
||||
const loadNextPage = useSetAtom(loadNextDocumentRevisionPageAtom)
|
||||
const retryRevisions = useSetAtom(retryDocumentRevisionsAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedRevision === null ||
|
||||
requestedRevision === null ||
|
||||
revision ||
|
||||
!hasNextPage ||
|
||||
isFetchingNextPage ||
|
||||
isFetchNextPageError
|
||||
)
|
||||
return
|
||||
void fetchNextPage()
|
||||
void loadNextPage()
|
||||
}, [
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchNextPageError,
|
||||
isFetchingNextPage,
|
||||
loadNextPage,
|
||||
requestedRevision,
|
||||
revision,
|
||||
selectedRevision,
|
||||
])
|
||||
|
||||
if (
|
||||
selectedRevision !== null &&
|
||||
!revision &&
|
||||
(revisionsQuery.isPending || isFetchingNextPage || hasNextPage)
|
||||
)
|
||||
if (requestedRevision !== null && !revision && (isPending || isFetchingNextPage || hasNextPage))
|
||||
return <RevisionLoadingState />
|
||||
|
||||
if (selectedRevision !== null && !revision && revisionsQuery.error)
|
||||
if (requestedRevision !== null && !revision && error)
|
||||
return (
|
||||
<RevisionErrorState
|
||||
description={t(($) => $['newKnowledge.documentRevisionsLoadError'])}
|
||||
onRetry={() => {
|
||||
if (isFetchNextPageError) void fetchNextPage()
|
||||
else void revisionsQuery.refetch()
|
||||
}}
|
||||
onRetry={() => void retryRevisions()}
|
||||
title={t(($) => $['newKnowledge.documentLoadErrorTitle'])}
|
||||
/>
|
||||
)
|
||||
|
||||
if (selectedRevision !== null && !revision)
|
||||
if (requestedRevision !== null && !revision)
|
||||
return (
|
||||
<RevisionErrorState
|
||||
description={t(($) => $['newKnowledge.documentNotFoundDescription'])}
|
||||
@ -149,13 +108,13 @@ export function DocumentRevisionBrowser() {
|
||||
/>
|
||||
)
|
||||
|
||||
if (effectiveRevision === undefined && revisionsQuery.isPending) return <RevisionLoadingState />
|
||||
if (effectiveRevision === undefined && isPending) return <RevisionLoadingState />
|
||||
|
||||
if (effectiveRevision === undefined && revisionsQuery.error)
|
||||
if (effectiveRevision === undefined && error)
|
||||
return (
|
||||
<RevisionErrorState
|
||||
description={t(($) => $['newKnowledge.documentLoadErrorDescription'])}
|
||||
onRetry={() => void revisionsQuery.refetch()}
|
||||
onRetry={() => void retryRevisions()}
|
||||
title={t(($) => $['newKnowledge.documentLoadErrorTitle'])}
|
||||
/>
|
||||
)
|
||||
@ -175,26 +134,18 @@ export function DocumentRevisionBrowser() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{revisionsQuery.error && !revisionsQuery.isFetchNextPageError && (
|
||||
{error && !isFetchNextPageError && (
|
||||
<div
|
||||
className="mt-4 flex flex-wrap items-center justify-between gap-2 rounded-lg bg-state-warning-hover px-3 py-2 system-xs-regular text-text-warning"
|
||||
role="alert"
|
||||
>
|
||||
<span>{t(($) => $['newKnowledge.documentRevisionsLoadError'])}</span>
|
||||
<Button onClick={() => void revisionsQuery.refetch()}>
|
||||
<Button onClick={() => void retryRevisions()}>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<DocumentRevisionData
|
||||
document={document}
|
||||
effectiveRevision={effectiveRevision}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
locale={locale}
|
||||
onSelectChunk={(chunkId) => void setDocumentLocation({ chunk: chunkId })}
|
||||
revision={revision}
|
||||
selectedChunkId={selectedChunkId ?? undefined}
|
||||
/>
|
||||
<DocumentRevisionData />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,166 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import type { LogicalDocument, LogicalDocumentRevision } from '../models'
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { documentChunkListFromApi } from '../models'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useEffect } from 'react'
|
||||
import { DocumentFactsSidebar, DocumentReadingPane } from './chunk-detail'
|
||||
import { DocumentChunkTreePanel } from './chunk-tree'
|
||||
import { buildDocumentDetailModel } from './model'
|
||||
import {
|
||||
documentChunksQueryOptions,
|
||||
documentMultimodalQueryOptions,
|
||||
documentOutlineQueryOptions,
|
||||
} from './queries'
|
||||
documentChunksQueryHasNextPageAtom,
|
||||
documentChunksQueryIsFetchingNextPageAtom,
|
||||
documentChunksQueryIsFetchNextPageErrorAtom,
|
||||
documentDetailSelectedChunkKnownAtom,
|
||||
loadNextDocumentChunkPageAtom,
|
||||
} from './state/content'
|
||||
import { documentDetailRequestedChunkIdAtom } from './state/inputs'
|
||||
import { documentDetailRevisionSessionKeyAtom } from './state/revisions'
|
||||
|
||||
export function DocumentRevisionData({
|
||||
document,
|
||||
effectiveRevision,
|
||||
knowledgeSpaceId,
|
||||
locale,
|
||||
onSelectChunk,
|
||||
revision,
|
||||
selectedChunkId,
|
||||
}: {
|
||||
document: LogicalDocument
|
||||
effectiveRevision: number
|
||||
knowledgeSpaceId: string
|
||||
locale: string
|
||||
onSelectChunk: (chunkId: string) => void
|
||||
revision?: Exclude<LogicalDocumentRevision, null>
|
||||
selectedChunkId?: string
|
||||
}) {
|
||||
const chunksQueryOptions = useMemo(
|
||||
() =>
|
||||
documentChunksQueryOptions({
|
||||
documentId: document.id,
|
||||
effectiveRevision,
|
||||
knowledgeSpaceId,
|
||||
}),
|
||||
[document.id, effectiveRevision, knowledgeSpaceId],
|
||||
)
|
||||
const chunksQuery = useInfiniteQuery(chunksQueryOptions)
|
||||
const {
|
||||
fetchNextPage: fetchNextChunkPage,
|
||||
hasNextPage: hasNextChunkPage,
|
||||
isFetchNextPageError: isFetchNextChunkPageError,
|
||||
isFetchingNextPage: isFetchingNextChunkPage,
|
||||
} = chunksQuery
|
||||
const documentAsset =
|
||||
revision ?? (document.active?.revision === effectiveRevision ? document.active : undefined)
|
||||
const outlineQueryOptions = useMemo(
|
||||
() =>
|
||||
documentOutlineQueryOptions({
|
||||
documentAssetId: documentAsset?.documentAssetId,
|
||||
knowledgeSpaceId,
|
||||
}),
|
||||
[documentAsset?.documentAssetId, knowledgeSpaceId],
|
||||
)
|
||||
const outlineQuery = useQuery(outlineQueryOptions)
|
||||
const multimodalQueryOptions = useMemo(
|
||||
() =>
|
||||
documentMultimodalQueryOptions({
|
||||
documentAssetId: documentAsset?.documentAssetId,
|
||||
knowledgeSpaceId,
|
||||
}),
|
||||
[documentAsset?.documentAssetId, knowledgeSpaceId],
|
||||
)
|
||||
const multimodalQuery = useQuery(multimodalQueryOptions)
|
||||
const chunks = useMemo(
|
||||
() =>
|
||||
[
|
||||
...(chunksQuery.data?.pages.flatMap((page) => documentChunkListFromApi(page).items) ?? []),
|
||||
].sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)),
|
||||
[chunksQuery.data],
|
||||
)
|
||||
const multimodalItems = useMemo(() => {
|
||||
const manifest = multimodalQuery.data
|
||||
if (!manifest || manifest.version !== documentAsset?.documentAssetVersion) return []
|
||||
return manifest.items ?? []
|
||||
}, [documentAsset?.documentAssetVersion, multimodalQuery.data])
|
||||
const detailModel = useMemo(() => {
|
||||
const outline = outlineQuery.data
|
||||
return buildDocumentDetailModel(
|
||||
chunks,
|
||||
outline && outline.version === documentAsset?.documentAssetVersion ? outline.nodes : [],
|
||||
multimodalItems,
|
||||
)
|
||||
}, [chunks, documentAsset?.documentAssetVersion, multimodalItems, outlineQuery.data])
|
||||
const targetedBlock = selectedChunkId
|
||||
? detailModel.contentBlocksByChunkId.get(selectedChunkId)
|
||||
: undefined
|
||||
const selectedChunkKnown = selectedChunkId
|
||||
? detailModel.sourceChunksById.has(selectedChunkId)
|
||||
: false
|
||||
const targetLookupComplete =
|
||||
!selectedChunkId ||
|
||||
selectedChunkKnown ||
|
||||
(!chunksQuery.isPending && (!hasNextChunkPage || isFetchNextChunkPageError))
|
||||
const fallbackBlock = detailModel.tree.roots[0]
|
||||
? detailModel.contentBlocksByChunkId.get(detailModel.tree.roots[0].targetChunkId)
|
||||
: undefined
|
||||
const selectedBlock = targetedBlock ?? (targetLookupComplete ? fallbackBlock : undefined)
|
||||
const revisionSessionKey = `${document.id}:${effectiveRevision}`
|
||||
function RequestedChunkPageLoader() {
|
||||
const selectedChunkId = useAtomValue(documentDetailRequestedChunkIdAtom)
|
||||
const selectedChunkKnown = useAtomValue(documentDetailSelectedChunkKnownAtom)
|
||||
const hasNextPage = useAtomValue(documentChunksQueryHasNextPageAtom)
|
||||
const isFetchNextPageError = useAtomValue(documentChunksQueryIsFetchNextPageErrorAtom)
|
||||
const isFetchingNextPage = useAtomValue(documentChunksQueryIsFetchingNextPageAtom)
|
||||
const loadNextPage = useSetAtom(loadNextDocumentChunkPageAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!selectedChunkId ||
|
||||
selectedChunkKnown ||
|
||||
!hasNextChunkPage ||
|
||||
isFetchingNextChunkPage ||
|
||||
isFetchNextChunkPageError
|
||||
!hasNextPage ||
|
||||
isFetchingNextPage ||
|
||||
isFetchNextPageError
|
||||
)
|
||||
return
|
||||
void fetchNextChunkPage()
|
||||
void loadNextPage()
|
||||
}, [
|
||||
fetchNextChunkPage,
|
||||
hasNextChunkPage,
|
||||
isFetchNextChunkPageError,
|
||||
isFetchingNextChunkPage,
|
||||
hasNextPage,
|
||||
isFetchNextPageError,
|
||||
isFetchingNextPage,
|
||||
loadNextPage,
|
||||
selectedChunkId,
|
||||
selectedChunkKnown,
|
||||
])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function DocumentRevisionData() {
|
||||
const revisionSessionKey = useAtomValue(documentDetailRevisionSessionKeyAtom)
|
||||
if (!revisionSessionKey) return null
|
||||
|
||||
return (
|
||||
<div className="mt-4 grid min-h-0 flex-1 gap-4 xl:grid-cols-[14rem_minmax(0,1fr)_20rem] xl:gap-0">
|
||||
<DocumentChunkTreePanel
|
||||
key={`tree:${revisionSessionKey}`}
|
||||
chunkCount={chunks.length}
|
||||
error={Boolean(chunksQuery.error)}
|
||||
fetchNextPage={chunksQuery.fetchNextPage}
|
||||
hasNextPage={chunksQuery.hasNextPage}
|
||||
isFetchNextPageError={chunksQuery.isFetchNextPageError}
|
||||
isFetchingNextPage={chunksQuery.isFetchingNextPage}
|
||||
isPending={chunksQuery.isPending}
|
||||
onRetry={() => void chunksQuery.refetch()}
|
||||
onSelectChunk={onSelectChunk}
|
||||
selectedChunkId={selectedBlock?.chunk.id}
|
||||
tree={detailModel.tree}
|
||||
/>
|
||||
|
||||
<DocumentReadingPane
|
||||
key={`content:${revisionSessionKey}`}
|
||||
contentBlocks={detailModel.contentBlocks}
|
||||
isLoadingMore={chunksQuery.isFetchingNextPage}
|
||||
multimodalItems={multimodalItems}
|
||||
selectedChunkId={selectedBlock?.chunk.id}
|
||||
/>
|
||||
|
||||
<DocumentFactsSidebar
|
||||
key={`facts:${document.id}`}
|
||||
chunksComplete={
|
||||
Boolean(chunksQuery.data) &&
|
||||
!chunksQuery.error &&
|
||||
!chunksQuery.hasNextPage &&
|
||||
!chunksQuery.isFetchingNextPage &&
|
||||
!chunksQuery.isFetchNextPageError
|
||||
}
|
||||
controlSpaceId={knowledgeSpaceId}
|
||||
document={document}
|
||||
indexChunks={detailModel.indexChunks}
|
||||
locale={locale}
|
||||
revision={revision}
|
||||
/>
|
||||
<RequestedChunkPageLoader />
|
||||
<DocumentChunkTreePanel key={`tree:${revisionSessionKey}`} />
|
||||
<DocumentReadingPane key={`content:${revisionSessionKey}`} />
|
||||
<DocumentFactsSidebar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,7 +2,23 @@
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useHydrateAtoms } from 'jotai/utils'
|
||||
import { documentDetailDocumentIdAtom, documentDetailKnowledgeSpaceIdAtom } from './inputs'
|
||||
import { createParser, parseAsString, useQueryStates } from 'nuqs'
|
||||
import {
|
||||
documentDetailDocumentIdAtom,
|
||||
documentDetailKnowledgeSpaceIdAtom,
|
||||
documentDetailRequestedChunkIdAtom,
|
||||
documentDetailRequestedRevisionAtom,
|
||||
} from './inputs'
|
||||
import { documentDetailLocationRuntimeAtom } from './runtime'
|
||||
|
||||
const documentRevisionParser = createParser<number>({
|
||||
parse: (value) => {
|
||||
const revision = Number(value)
|
||||
return Number.isInteger(revision) && revision > 0 ? revision : null
|
||||
},
|
||||
serialize: String,
|
||||
}).withOptions({ history: 'push' })
|
||||
const documentChunkParser = parseAsString.withOptions({ history: 'replace' })
|
||||
|
||||
export function DocumentDetailStateBoundary({
|
||||
children,
|
||||
@ -13,10 +29,18 @@ export function DocumentDetailStateBoundary({
|
||||
documentId: string
|
||||
knowledgeSpaceId: string
|
||||
}) {
|
||||
const [documentLocation, setDocumentLocation] = useQueryStates({
|
||||
chunk: documentChunkParser,
|
||||
revision: documentRevisionParser,
|
||||
})
|
||||
|
||||
useHydrateAtoms(
|
||||
[
|
||||
[documentDetailDocumentIdAtom, documentId],
|
||||
[documentDetailKnowledgeSpaceIdAtom, knowledgeSpaceId],
|
||||
[documentDetailRequestedChunkIdAtom, documentLocation.chunk],
|
||||
[documentDetailRequestedRevisionAtom, documentLocation.revision],
|
||||
[documentDetailLocationRuntimeAtom, { setDocumentLocation }],
|
||||
],
|
||||
{ dangerouslyForceHydrate: true },
|
||||
)
|
||||
|
||||
181
web/features/new-rag/documents/detail/state/content.ts
Normal file
181
web/features/new-rag/documents/detail/state/content.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { documentChunkListFromApi } from '../../models'
|
||||
import { buildDocumentDetailModel } from '../model'
|
||||
import {
|
||||
documentDetailDocumentIdAtom,
|
||||
documentDetailKnowledgeSpaceIdAtom,
|
||||
documentDetailRequestedChunkIdAtom,
|
||||
} from './inputs'
|
||||
import { documentDetailDocumentAtom } from './queries'
|
||||
import { documentDetailEffectiveRevisionAtom, documentDetailRevisionAtom } from './revisions'
|
||||
|
||||
const documentChunksQueryAtom = atomWithInfiniteQuery((get) => {
|
||||
const effectiveRevision = get(documentDetailEffectiveRevisionAtom)
|
||||
if (effectiveRevision === undefined)
|
||||
throw new Error('Document revision is unavailable for chunk loading')
|
||||
|
||||
return consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.revisions.byRevision.chunks.get.infiniteOptions(
|
||||
{
|
||||
input: (pageParam) => ({
|
||||
params: {
|
||||
control_space_id: get(documentDetailKnowledgeSpaceIdAtom),
|
||||
document_id: get(documentDetailDocumentIdAtom),
|
||||
revision: effectiveRevision,
|
||||
},
|
||||
query: {
|
||||
...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
|
||||
},
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage.next_cursor,
|
||||
initialPageParam: null as string | null,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const documentChunksQueryDataAtom = selectAtom(documentChunksQueryAtom, (query) => query.data)
|
||||
|
||||
export const documentChunksQueryErrorAtom = selectAtom(
|
||||
documentChunksQueryAtom,
|
||||
(query) => query.error,
|
||||
)
|
||||
export const documentChunksQueryHasDataAtom = atom((get) =>
|
||||
Boolean(get(documentChunksQueryDataAtom)),
|
||||
)
|
||||
export const documentChunksQueryHasNextPageAtom = selectAtom(
|
||||
documentChunksQueryAtom,
|
||||
(query) => query.hasNextPage,
|
||||
)
|
||||
export const documentChunksQueryIsFetchNextPageErrorAtom = selectAtom(
|
||||
documentChunksQueryAtom,
|
||||
(query) => query.isFetchNextPageError,
|
||||
)
|
||||
export const documentChunksQueryIsFetchingNextPageAtom = selectAtom(
|
||||
documentChunksQueryAtom,
|
||||
(query) => query.isFetchingNextPage,
|
||||
)
|
||||
export const documentChunksQueryIsPendingAtom = selectAtom(
|
||||
documentChunksQueryAtom,
|
||||
(query) => query.isPending,
|
||||
)
|
||||
|
||||
export const documentDetailChunksAtom = atom((get) =>
|
||||
[
|
||||
...(get(documentChunksQueryDataAtom)?.pages.flatMap(
|
||||
(page) => documentChunkListFromApi(page).items,
|
||||
) ?? []),
|
||||
].sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)),
|
||||
)
|
||||
|
||||
const documentDetailAssetAtom = atom((get) => {
|
||||
const document = get(documentDetailDocumentAtom)
|
||||
const effectiveRevision = get(documentDetailEffectiveRevisionAtom)
|
||||
return (
|
||||
get(documentDetailRevisionAtom) ??
|
||||
(document.active?.revision === effectiveRevision ? document.active : undefined)
|
||||
)
|
||||
})
|
||||
|
||||
const documentOutlineQueryAtom = atomWithQuery((get) => {
|
||||
const documentAssetId = get(documentDetailAssetAtom)?.documentAssetId
|
||||
return consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.outline.get.queryOptions(
|
||||
{
|
||||
context: { silent: true },
|
||||
input: documentAssetId
|
||||
? {
|
||||
params: {
|
||||
control_space_id: get(documentDetailKnowledgeSpaceIdAtom),
|
||||
document_id: documentAssetId,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
retry: false,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const documentMultimodalQueryAtom = atomWithQuery((get) => {
|
||||
const documentAssetId = get(documentDetailAssetAtom)?.documentAssetId
|
||||
return consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.multimodal.get.queryOptions(
|
||||
{
|
||||
context: { silent: true },
|
||||
input: documentAssetId
|
||||
? {
|
||||
params: {
|
||||
control_space_id: get(documentDetailKnowledgeSpaceIdAtom),
|
||||
document_id: documentAssetId,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
retry: false,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const documentOutlineDataAtom = selectAtom(documentOutlineQueryAtom, (query) => query.data)
|
||||
const documentMultimodalDataAtom = selectAtom(documentMultimodalQueryAtom, (query) => query.data)
|
||||
|
||||
export const documentDetailMultimodalItemsAtom = atom((get) => {
|
||||
const manifest = get(documentMultimodalDataAtom)
|
||||
if (!manifest || manifest.version !== get(documentDetailAssetAtom)?.documentAssetVersion)
|
||||
return []
|
||||
return manifest.items ?? []
|
||||
})
|
||||
|
||||
export const documentDetailModelAtom = atom((get) => {
|
||||
const documentAssetVersion = get(documentDetailAssetAtom)?.documentAssetVersion
|
||||
const outline = get(documentOutlineDataAtom)
|
||||
return buildDocumentDetailModel(
|
||||
get(documentDetailChunksAtom),
|
||||
outline && outline.version === documentAssetVersion ? outline.nodes : [],
|
||||
get(documentDetailMultimodalItemsAtom),
|
||||
)
|
||||
})
|
||||
|
||||
export const documentDetailSelectedChunkKnownAtom = atom((get) => {
|
||||
const selectedChunkId = get(documentDetailRequestedChunkIdAtom)
|
||||
return selectedChunkId
|
||||
? get(documentDetailModelAtom).sourceChunksById.has(selectedChunkId)
|
||||
: false
|
||||
})
|
||||
|
||||
export const documentDetailSelectedBlockAtom = atom((get) => {
|
||||
const selectedChunkId = get(documentDetailRequestedChunkIdAtom)
|
||||
const detailModel = get(documentDetailModelAtom)
|
||||
const targetedBlock = selectedChunkId
|
||||
? detailModel.contentBlocksByChunkId.get(selectedChunkId)
|
||||
: undefined
|
||||
const targetLookupComplete =
|
||||
!selectedChunkId ||
|
||||
get(documentDetailSelectedChunkKnownAtom) ||
|
||||
(!get(documentChunksQueryIsPendingAtom) &&
|
||||
(!get(documentChunksQueryHasNextPageAtom) ||
|
||||
get(documentChunksQueryIsFetchNextPageErrorAtom)))
|
||||
const firstRoot = detailModel.tree.roots[0]
|
||||
const fallbackBlock = firstRoot
|
||||
? detailModel.contentBlocksByChunkId.get(firstRoot.targetChunkId)
|
||||
: undefined
|
||||
return targetedBlock ?? (targetLookupComplete ? fallbackBlock : undefined)
|
||||
})
|
||||
|
||||
export const documentDetailSelectedChunkIdAtom = atom(
|
||||
(get) => get(documentDetailSelectedBlockAtom)?.chunk.id,
|
||||
)
|
||||
|
||||
export const documentDetailChunksCompleteAtom = atom(
|
||||
(get) =>
|
||||
get(documentChunksQueryHasDataAtom) &&
|
||||
!get(documentChunksQueryErrorAtom) &&
|
||||
!get(documentChunksQueryHasNextPageAtom) &&
|
||||
!get(documentChunksQueryIsFetchingNextPageAtom) &&
|
||||
!get(documentChunksQueryIsFetchNextPageErrorAtom),
|
||||
)
|
||||
|
||||
export const loadNextDocumentChunkPageAtom = atom(null, (get) =>
|
||||
get(documentChunksQueryAtom).fetchNextPage(),
|
||||
)
|
||||
|
||||
export const retryDocumentChunksAtom = atom(null, (get) => get(documentChunksQueryAtom).refetch())
|
||||
@ -1,3 +1,4 @@
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithLazy } from 'jotai/utils'
|
||||
|
||||
export const documentDetailKnowledgeSpaceIdAtom = atomWithLazy<string>(() => {
|
||||
@ -7,3 +8,7 @@ export const documentDetailKnowledgeSpaceIdAtom = atomWithLazy<string>(() => {
|
||||
export const documentDetailDocumentIdAtom = atomWithLazy<string>(() => {
|
||||
throw new Error('Missing document detail document id')
|
||||
})
|
||||
|
||||
export const documentDetailRequestedRevisionAtom = atom<number | null>(null)
|
||||
|
||||
export const documentDetailRequestedChunkIdAtom = atom<string | null>(null)
|
||||
|
||||
98
web/features/new-rag/documents/detail/state/revisions.ts
Normal file
98
web/features/new-rag/documents/detail/state/revisions.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithInfiniteQuery } from 'jotai-tanstack-query'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { documentRevisionListFromApi } from '../../models'
|
||||
import { initialDocumentRevision } from '../model'
|
||||
import {
|
||||
documentDetailDocumentIdAtom,
|
||||
documentDetailKnowledgeSpaceIdAtom,
|
||||
documentDetailRequestedRevisionAtom,
|
||||
} from './inputs'
|
||||
import { documentDetailDocumentAtom } from './queries'
|
||||
|
||||
const documentRevisionsQueryAtom = atomWithInfiniteQuery((get) =>
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.revisions.get.infiniteOptions(
|
||||
{
|
||||
input: (pageParam) => ({
|
||||
params: {
|
||||
control_space_id: get(documentDetailKnowledgeSpaceIdAtom),
|
||||
document_id: get(documentDetailDocumentIdAtom),
|
||||
},
|
||||
query: {
|
||||
...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
|
||||
},
|
||||
}),
|
||||
getNextPageParam: (lastPage) => lastPage.next_cursor,
|
||||
initialPageParam: null as string | null,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const documentRevisionsQueryDataAtom = selectAtom(documentRevisionsQueryAtom, (query) => query.data)
|
||||
|
||||
const documentRevisionsAtom = atom(
|
||||
(get) =>
|
||||
get(documentRevisionsQueryDataAtom)?.pages.flatMap(
|
||||
(page) => documentRevisionListFromApi(page).items,
|
||||
) ?? [],
|
||||
)
|
||||
|
||||
export const documentDetailAvailableRevisionsAtom = atom((get) => {
|
||||
const document = get(documentDetailDocumentAtom)
|
||||
const byRevision = new Map(
|
||||
get(documentRevisionsAtom).map((revision) => [revision.revision, revision]),
|
||||
)
|
||||
if (document.active) byRevision.set(document.active.revision, document.active)
|
||||
return [...byRevision.values()].sort((left, right) => right.revision - left.revision)
|
||||
})
|
||||
|
||||
export const documentDetailRevisionAtom = atom((get) => {
|
||||
const document = get(documentDetailDocumentAtom)
|
||||
const availableRevisions = get(documentDetailAvailableRevisionsAtom)
|
||||
const requestedRevision =
|
||||
get(documentDetailRequestedRevisionAtom) ??
|
||||
initialDocumentRevision(document, availableRevisions)
|
||||
return availableRevisions.find((candidate) => candidate.revision === requestedRevision)
|
||||
})
|
||||
|
||||
export const documentDetailEffectiveRevisionAtom = atom(
|
||||
(get) => get(documentDetailRevisionAtom)?.revision,
|
||||
)
|
||||
|
||||
export const documentDetailRevisionSessionKeyAtom = atom((get) => {
|
||||
const effectiveRevision = get(documentDetailEffectiveRevisionAtom)
|
||||
if (effectiveRevision === undefined) return undefined
|
||||
return `${get(documentDetailDocumentIdAtom)}:${effectiveRevision}`
|
||||
})
|
||||
|
||||
export const documentRevisionsQueryErrorAtom = selectAtom(
|
||||
documentRevisionsQueryAtom,
|
||||
(query) => query.error,
|
||||
)
|
||||
export const documentRevisionsQueryHasNextPageAtom = selectAtom(
|
||||
documentRevisionsQueryAtom,
|
||||
(query) => query.hasNextPage,
|
||||
)
|
||||
export const documentRevisionsQueryIsFetchNextPageErrorAtom = selectAtom(
|
||||
documentRevisionsQueryAtom,
|
||||
(query) => query.isFetchNextPageError,
|
||||
)
|
||||
export const documentRevisionsQueryIsFetchingNextPageAtom = selectAtom(
|
||||
documentRevisionsQueryAtom,
|
||||
(query) => query.isFetchingNextPage,
|
||||
)
|
||||
export const documentRevisionsQueryIsPendingAtom = selectAtom(
|
||||
documentRevisionsQueryAtom,
|
||||
(query) => query.isPending,
|
||||
)
|
||||
|
||||
export const loadNextDocumentRevisionPageAtom = atom(null, (get) =>
|
||||
get(documentRevisionsQueryAtom).fetchNextPage(),
|
||||
)
|
||||
|
||||
export const retryDocumentRevisionsAtom = atom(null, (get) => {
|
||||
const query = get(documentRevisionsQueryAtom)
|
||||
if (query.isFetchNextPageError) return query.fetchNextPage()
|
||||
return query.refetch()
|
||||
})
|
||||
22
web/features/new-rag/documents/detail/state/runtime.ts
Normal file
22
web/features/new-rag/documents/detail/state/runtime.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
type DocumentDetailLocation = {
|
||||
chunk?: string | null
|
||||
revision?: number | null
|
||||
}
|
||||
|
||||
type DocumentDetailLocationRuntime = {
|
||||
setDocumentLocation: (location: DocumentDetailLocation) => Promise<URLSearchParams>
|
||||
}
|
||||
|
||||
const unavailableLocationRuntime = async () => {
|
||||
throw new Error('Document detail location runtime is unavailable')
|
||||
}
|
||||
|
||||
export const documentDetailLocationRuntimeAtom = atom<DocumentDetailLocationRuntime>({
|
||||
setDocumentLocation: unavailableLocationRuntime,
|
||||
})
|
||||
|
||||
export const selectDocumentChunkAtom = atom(null, (get, _set, chunkId: string) =>
|
||||
get(documentDetailLocationRuntimeAtom).setDocumentLocation({ chunk: chunkId }),
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user