diff --git a/api/services/knowledge_fs/initial_source_preview.py b/api/services/knowledge_fs/initial_source_preview.py index 39d0b0bdcb9..60a099eddec 100644 --- a/api/services/knowledge_fs/initial_source_preview.py +++ b/api/services/knowledge_fs/initial_source_preview.py @@ -66,6 +66,13 @@ class KnowledgeFSInitialSourcePreviewService: payload: KnowledgeFSInitialSourcePreviewPayload | KnowledgeFSInitialWebsiteSourcePreviewPayload, is_canceled: Callable[[], bool] | None = None, ) -> KnowledgeFSInitialSourcePreviewResponse: + """Preview a visible credential without creating a Space. + + The caller supplies valid plugin parameters and an account in the tenant. Each + online-document invocation returns only the pages emitted for that request; + optional tree ancestry and cursor metadata remain intact. Previewing does not + persist a source or alter credentials, and plugin failures propagate to the caller. + """ _raise_if_canceled(is_canceled) self.require_visible_credential(tenant_id=tenant_id, account=account, payload=payload) from extensions.application_services.data_sources import build_data_source_credentials @@ -117,19 +124,24 @@ class KnowledgeFSInitialSourcePreviewService: if payload.kind == "online_document": document_runtime = cast(OnlineDocumentDatasourcePlugin, runtime) documents: list[KnowledgeFSInitialSourcePreviewDocumentResponse] = [] + next_cursor: str | None = None for document_message in document_runtime.get_online_document_pages( user_id=account.id, datasource_parameters=parameters, provider_type=document_runtime.datasource_provider_type(), ): + next_cursor = document_message.next_cursor or next_cursor for workspace in document_message.result: + next_cursor = workspace.next_cursor or next_cursor workspace_id = workspace.workspace_id or payload.provider for document_page in workspace.pages: documents.append( KnowledgeFSInitialSourcePreviewDocumentResponse( + has_children=document_page.has_children, last_edited_time=document_page.last_edited_time, name=document_page.page_name, page_id=document_page.page_id, + parent_id=document_page.parent_id, provider_item_id=json.dumps( [workspace_id, document_page.page_id], separators=(",", ":") ), @@ -142,8 +154,11 @@ class KnowledgeFSInitialSourcePreviewService: return KnowledgeFSInitialSourcePreviewResponse( documents=documents, kind=payload.kind, + next_cursor=next_cursor, ) - return KnowledgeFSInitialSourcePreviewResponse(documents=documents, kind=payload.kind) + return KnowledgeFSInitialSourcePreviewResponse( + documents=documents, kind=payload.kind, next_cursor=next_cursor + ) drive_runtime = cast(OnlineDriveDatasourcePlugin, runtime) files: list[KnowledgeFSInitialSourcePreviewFileResponse] = [] diff --git a/api/services/knowledge_fs/product_dto.py b/api/services/knowledge_fs/product_dto.py index ac23effe260..854fa5897e0 100644 --- a/api/services/knowledge_fs/product_dto.py +++ b/api/services/knowledge_fs/product_dto.py @@ -312,11 +312,13 @@ class KnowledgeFSInitialSourcePreviewPageResponse(ResponseModel): class KnowledgeFSInitialSourcePreviewDocumentResponse(ResponseModel): + has_children: bool | None = Field(default=None, validation_alias=AliasChoices("has_children", "hasChildren")) last_edited_time: str | None = Field( default=None, validation_alias=AliasChoices("last_edited_time", "lastEditedTime") ) name: str page_id: str = Field(validation_alias=AliasChoices("page_id", "pageId")) + parent_id: str | None = Field(default=None, validation_alias=AliasChoices("parent_id", "parentId")) provider_item_id: str = Field(validation_alias=AliasChoices("provider_item_id", "providerItemId")) type: str workspace_id: str = Field(validation_alias=AliasChoices("workspace_id", "workspaceId")) @@ -341,6 +343,7 @@ class KnowledgeFSInitialSourcePreviewResponse(ResponseModel): documents: list[KnowledgeFSInitialSourcePreviewDocumentResponse] = Field(default_factory=list) files: list[KnowledgeFSInitialSourcePreviewFileResponse] = Field(default_factory=list) kind: Literal["online_document", "online_drive", "website_crawl"] + next_cursor: str | None = Field(default=None, validation_alias=AliasChoices("next_cursor", "nextCursor")) next_page_parameters: dict[str, JsonValue] | None = Field( default=None, validation_alias=AliasChoices("next_page_parameters", "nextPageParameters"), diff --git a/api/tests/unit_tests/services/knowledge_fs/test_initial_source_preview.py b/api/tests/unit_tests/services/knowledge_fs/test_initial_source_preview.py index 098d3ac3bf0..b97e757bf45 100644 --- a/api/tests/unit_tests/services/knowledge_fs/test_initial_source_preview.py +++ b/api/tests/unit_tests/services/knowledge_fs/test_initial_source_preview.py @@ -164,6 +164,61 @@ def test_preview_lists_online_documents_with_stable_import_identity( assert runtime.runtime.credentials == {"token": "secret"} +def test_preview_preserves_github_tree_branch_and_cursor( + sqlite_session_factory: sessionmaker[Session], +) -> None: + service = _service(sqlite_session_factory) + runtime = FakeDatasourceRuntime( + provider_type="online_document", + messages=[ + OnlineDocumentPagesMessage( + next_cursor="2", + result=[ + OnlineDocumentInfo( + next_cursor="2", + pages=[ + OnlineDocumentPage( + has_children=True, + last_edited_time="2026-11-10T00:00:00Z", + page_id="dir:FFXN/dify:src", + page_name="src", + parent_id="contents:FFXN/dify", + type="github_directory", + ) + ], + total=1, + workspace_id="github-user-1", + workspace_name="FFXN's GitHub", + ) + ], + ) + ], + ) + payload = _payload("online_document") + payload.parameters = {"view": "tree", "parent_id": "contents:FFXN/dify", "limit": 100} + with ( + patch( + "services.data_source.provider_service.DatasourceProviderService.get_datasource_credentials", + return_value={"token": "secret"}, + ), + patch( + "services.knowledge_fs.initial_source_preview.DatasourceManager.get_datasource_runtime", + return_value=runtime, + ), + ): + response = service.preview( + tenant_id="tenant-1", + account=_account(), + payload=payload, + ) + + assert runtime.online_document_calls[0]["datasource_parameters"] == payload.parameters + assert response.next_cursor == "2" + assert response.documents[0].has_children is True + assert response.documents[0].parent_id == "contents:FFXN/dify" + assert response.documents[0].provider_item_id == '["github-user-1","dir:FFXN/dify:src"]' + + def test_preview_browses_online_drive_and_preserves_pagination(sqlite_session_factory: sessionmaker[Session]) -> None: service = _service(sqlite_session_factory) runtime = FakeDatasourceRuntime( diff --git a/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts b/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts index d54b2bac152..67b543ab04b 100644 --- a/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts +++ b/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts @@ -80,6 +80,7 @@ export type KnowledgeFsInitialSourcePreviewResponse = { documents?: Array files?: Array kind: 'online_document' | 'online_drive' | 'website_crawl' + next_cursor?: string | null next_page_parameters?: { [key: string]: JsonValue } | null @@ -1535,9 +1536,11 @@ export type KnowledgeFsPublicFailureResponse = { export type JsonValue = unknown export type KnowledgeFsInitialSourcePreviewDocumentResponse = { + has_children?: boolean | null last_edited_time?: string | null name: string page_id: string + parent_id?: string | null provider_item_id: string type: string workspace_id: string diff --git a/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts b/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts index 937e23c5289..1d116532d20 100644 --- a/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts +++ b/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts @@ -1240,9 +1240,11 @@ export const zKnowledgeFsInitialWebsiteSourcePreviewPayload = z.object({ * KnowledgeFSInitialSourcePreviewDocumentResponse */ export const zKnowledgeFsInitialSourcePreviewDocumentResponse = z.object({ + has_children: z.boolean().nullish(), last_edited_time: z.string().nullish(), name: z.string(), page_id: z.string(), + parent_id: z.string().nullish(), provider_item_id: z.string(), type: z.string(), workspace_id: z.string(), @@ -1280,6 +1282,7 @@ export const zKnowledgeFsInitialSourcePreviewResponse = z.object({ documents: z.array(zKnowledgeFsInitialSourcePreviewDocumentResponse).optional(), files: z.array(zKnowledgeFsInitialSourcePreviewFileResponse).optional(), kind: z.enum(['online_document', 'online_drive', 'website_crawl']), + next_cursor: z.string().nullish(), next_page_parameters: z.record(z.string(), zJsonValue).nullish(), pages: z.array(zKnowledgeFsInitialSourcePreviewPageResponse).optional(), }) diff --git a/web/features/new-rag/create/__tests__/page.spec.tsx b/web/features/new-rag/create/__tests__/page.spec.tsx index 346c7485077..f624d6b1f18 100644 --- a/web/features/new-rag/create/__tests__/page.spec.tsx +++ b/web/features/new-rag/create/__tests__/page.spec.tsx @@ -403,6 +403,48 @@ const notionDatasourceAuth = { provider: 'notion', } +const githubDatasourcePlugin = { + ...notionDatasourcePlugin, + declaration: { + ...notionDatasourcePlugin.declaration, + datasources: [ + { + ...notionDatasourcePlugin.declaration.datasources[0], + identity: { + ...notionDatasourcePlugin.declaration.datasources[0]?.identity, + label: { en_US: 'GitHub Repository', zh_Hans: 'GitHub Repository' }, + name: 'github', + provider: 'github', + }, + }, + ], + identity: { + ...notionDatasourcePlugin.declaration.identity, + label: { en_US: 'GitHub Repository', zh_Hans: 'GitHub Repository' }, + name: 'github', + }, + }, + plugin_id: 'langgenius/github_datasource', + plugin_unique_identifier: 'langgenius/github_datasource:0.4.12@local', + provider: 'github', +} + +const githubDatasourceAuth = { + ...notionDatasourceAuth, + credentials_list: [ + { + ...notionDatasourceAuth.credentials_list[0], + id: 'github-credential-1', + name: 'GitHub token', + }, + ], + label: { en_US: 'GitHub Repository' }, + name: 'github', + plugin_id: 'langgenius/github_datasource', + plugin_unique_identifier: 'langgenius/github_datasource:0.4.12@local', + provider: 'github', +} + const outlineDatasourcePlugin = { ...notionDatasourcePlugin, declaration: { @@ -2382,6 +2424,100 @@ describe('CreateKnowledgePage', () => { }) }) + it('previews GitHub branches lazily and imports only explicitly selected pages', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + datasourceQueryMock.plugins.data = [ + firecrawlDatasourcePlugin, + notionDatasourcePlugin, + githubDatasourcePlugin, + ] + datasourceQueryMock.auth.data = { + result: [firecrawlDatasourceAuth, notionDatasourceAuth, githubDatasourceAuth], + } + const page = (pageId: string, name: string, type: string, parentId?: string) => ({ + has_children: + type === 'repository' || type === 'github_collection' || type === 'github_directory', + last_edited_time: '2026-11-10T00:00:00Z', + name, + page_id: pageId, + parent_id: parentId ?? null, + provider_item_id: JSON.stringify(['github-user-1', pageId]), + type, + workspace_id: 'github-user-1', + workspace_name: "FFXN's GitHub", + }) + serviceMock.previewInitialSource.mockImplementation( + async ({ body }: { body: { parameters: Record } }) => { + const { parent_id: parentId, cursor } = body.parameters + const documents = + parentId === 'repo:FFXN/dify' + ? [page('contents:FFXN/dify', 'Contents', 'github_collection', parentId)] + : parentId === 'contents:FFXN/dify' + ? [page('dir:FFXN/dify:src', 'src', 'github_directory', parentId)] + : parentId === 'dir:FFXN/dify:src' + ? cursor + ? [page('file:FFXN/dify:src/extra.ts', 'extra.ts', 'file', parentId)] + : [page('file:FFXN/dify:src/index.ts', 'index.ts', 'file', parentId)] + : [page('repo:FFXN/dify', 'dify', 'repository')] + return { + documents, + kind: 'online_document', + next_cursor: parentId === 'dir:FFXN/dify:src' && !cursor ? '100' : null, + } + }, + ) + + await renderPage() + await fillRequiredFields(user) + await user.click(screen.getByRole('radio', { name: 'knowledgeSpace.onlineDocuments' })) + await user.click(screen.getByRole('radio', { name: 'GitHub Repository' })) + await user.type( + screen.getByPlaceholderText('knowledgeSources.sourceNamePlaceholder'), + 'GitHub code', + ) + await user.click(screen.getByRole('button', { name: 'knowledgeSpace.preview' })) + + await user.click(await screen.findByRole('button', { name: 'dify' })) + await user.click(await screen.findByRole('button', { name: 'Contents' })) + await user.click(await screen.findByRole('button', { name: 'src' })) + await user.click(await screen.findByRole('checkbox', { name: 'src' })) + await user.click(screen.getByRole('button', { name: 'knowledgeSpace.loadMore' })) + await screen.findByRole('checkbox', { name: 'extra.ts' }) + + expect(screen.getByRole('checkbox', { name: 'index.ts' })).toBeChecked() + expect(screen.getByRole('checkbox', { name: 'extra.ts' })).not.toBeChecked() + expect( + serviceMock.previewInitialSource.mock.calls.map(([request]) => request.body.parameters), + ).toEqual([ + { limit: 10, view: 'tree' }, + { limit: 30, parent_id: 'repo:FFXN/dify', view: 'tree' }, + { limit: 100, parent_id: 'contents:FFXN/dify', view: 'tree' }, + { limit: 100, parent_id: 'dir:FFXN/dify:src', view: 'tree' }, + { cursor: '100', limit: 100, parent_id: 'dir:FFXN/dify:src', view: 'tree' }, + ]) + + await user.click(screen.getByRole('button', { name: 'knowledgeCreate.createTitle' })) + await waitFor(() => expect(serviceMock.create).toHaveBeenCalledOnce()) + expect(serviceMock.create).toHaveBeenCalledWith({ + body: expect.objectContaining({ + initial_source: expect.objectContaining({ + datasource: 'github', + kind: 'online_document', + name: 'GitHub code', + parameters: {}, + pluginId: 'langgenius/github_datasource', + selection: [ + expect.objectContaining({ + pageId: 'file:FFXN/dify:src/index.ts', + type: 'file', + }), + ], + }), + }), + }) + }) + it('does not inject website defaults when switching connected document providers', async () => { const user = userEvent.setup() navigationMock.startMode = 'source' diff --git a/web/features/new-rag/sources/setup/connected-source-configuration.tsx b/web/features/new-rag/sources/setup/connected-source-configuration.tsx index 1c7b90d7086..aaee22bd559 100644 --- a/web/features/new-rag/sources/setup/connected-source-configuration.tsx +++ b/web/features/new-rag/sources/setup/connected-source-configuration.tsx @@ -25,6 +25,7 @@ import { withDatasourceParameterDefaults, } from './datasource-parameter-model' import { SourceNameField, SourceSyncPolicyField } from './fields' +import { InitialGitHubTreePreview } from './initial-github-tree-preview' type NextPageRequest = { bucket?: string @@ -160,6 +161,9 @@ function ConnectedSourceConfigurationFields({ // previews start a new session so pending root or pagination responses are discarded. const previewSessionRef = useRef(0) const driveTransport = providerOption.providerType === 'online_drive' + const githubTreePreview = + providerOption.plugin.plugin_id === 'langgenius/github_datasource' && + providerOption.datasource.identity.name === 'github' const parameterSchemas = useMemo( () => datasourceParameterSchemas(providerOption.datasource), [providerOption.datasource], @@ -371,7 +375,17 @@ function ConnectedSourceConfigurationFields({ onDraftChange({ ...draft, parameters: nextParameters }) }} /> - {!previewed && ( + {githubTreePreview && ( + + )} + {!githubTreePreview && !previewed && ( )} - {loading && ( + {!githubTreePreview && loading && (
)} - {error && ( + {!githubTreePreview && error && (

{t(($) => $.providerLoadFailed)}

)} - {previewed && !loading && ( + {!githubTreePreview && previewed && !loading && (
+type Branch = { + error: boolean + loaded: boolean + loading: boolean + nextCursor: string | null + requestedCursors: string[] + resources: DocumentResource[] +} +type Row = + | { kind: 'document'; resource: DocumentResource } + | { kind: 'loadMore'; depth: number; parentKey: string } + +const ROOT = 'root' +const ROOT_PAGE_SIZE = 10 +const CHILD_PAGE_SIZE = 30 +const DIRECTORY_PAGE_SIZE = 100 +const MAX_SELECTION = 200 +const SELECTION_LIMIT_ID = 'create-github-source-selection-limit' + +function isNavigationOnly(resource: DocumentResource) { + return ( + resource.document.type === 'github_collection' || resource.document.type === 'github_directory' + ) +} + +function resourceIcon(resource: DocumentResource) { + return resource.document.type === 'repository' || isNavigationOnly(resource) + ? 'i-ri-folder-3-fill text-text-warning' + : 'i-ri-file-text-line text-text-tertiary' +} + +export function InitialGitHubTreePreview({ + binding, + disabled, + parameters, + selectedResources, + onSelectionChange, +}: { + binding: ConnectedSourceConfigurationBinding + disabled: boolean + parameters: DatasourceParameters + selectedResources: ConnectedSourceSelection + onSelectionChange: (selection: ConnectedSourceSelection) => void +}) { + const { t } = useTranslation(['knowledgeSpace', 'knowledgeSources', 'common']) + const [branches, setBranches] = useState>(() => new Map()) + const [expanded, setExpanded] = useState>(() => new Set()) + const root = branches.get(ROOT) + const allResources = [ + ...new Map( + [...branches.values()] + .flatMap((branch) => branch.resources) + .map((resource) => [resource.key, resource]), + ).values(), + ] + const selectable = allResources.filter((resource) => !isNavigationOnly(resource)) + const selected = new Set(selectedResources.map((resource) => resource.key)) + const selectionAtLimit = selected.size >= MAX_SELECTION + + const updateSelection = (keys: Set) => { + onSelectionChange(allResources.filter((resource) => keys.has(resource.key))) + } + + const loadBranch = async (parent?: DocumentResource, cursor?: string) => { + const scope = parent?.key ?? ROOT + const prior = branches.get(scope) + if (prior?.loading || (cursor && prior?.requestedCursors.includes(cursor))) return + if (!parent && !cursor) { + setExpanded(new Set()) + onSelectionChange([]) + } + setBranches((current) => + new Map(current).set(scope, { + error: false, + loaded: prior?.loaded ?? false, + loading: true, + nextCursor: prior?.nextCursor ?? null, + requestedCursors: prior?.requestedCursors ?? [], + resources: prior?.resources ?? [], + }), + ) + try { + const response = await consoleClient.knowledgeFs.sourceProviderPreview.post({ + body: { + credentialId: binding.credentialId, + datasource: binding.datasource, + kind: 'online_document', + parameters: { + ...parameters, + view: 'tree', + limit: parent + ? parent.document.type === 'github_directory' || + parent.document.page_id.startsWith('contents:') + ? DIRECTORY_PAGE_SIZE + : CHILD_PAGE_SIZE + : ROOT_PAGE_SIZE, + ...(parent ? { parent_id: parent.document.page_id } : {}), + ...(cursor ? { cursor } : {}), + }, + pluginId: binding.pluginId, + provider: binding.provider, + }, + }) + const resources: DocumentResource[] = (response.documents ?? []).map((document) => ({ + depth: parent ? parent.depth + 1 : 0, + document, + key: `document:${document.provider_item_id}`, + kind: 'document', + parentKey: parent?.key, + })) + setBranches((current) => { + const existing = current.get(scope) + const merged = new Map( + (cursor ? (existing?.resources ?? []) : []).map((item) => [item.key, item]), + ) + for (const resource of resources) merged.set(resource.key, resource) + const requestedCursors = cursor ? [...(existing?.requestedCursors ?? []), cursor] : [] + const nextCursor = response.next_cursor + return new Map(current).set(scope, { + error: false, + loaded: true, + loading: false, + nextCursor: nextCursor && !requestedCursors.includes(nextCursor) ? nextCursor : null, + requestedCursors, + resources: [...merged.values()], + }) + }) + } catch { + setBranches((current) => { + const existing = current.get(scope) + return new Map(current).set(scope, { + error: true, + loaded: existing?.loaded ?? false, + loading: false, + nextCursor: existing?.nextCursor ?? null, + requestedCursors: existing?.requestedCursors ?? [], + resources: existing?.resources ?? [], + }) + }) + } + } + + const loadedDescendants = (resource: DocumentResource) => { + const result: DocumentResource[] = [] + const pending = [...(branches.get(resource.key)?.resources ?? [])] + const seen = new Set() + while (pending.length) { + const child = pending.shift() + if (!child || seen.has(child.key)) continue + seen.add(child.key) + result.push(child) + pending.push(...(branches.get(child.key)?.resources ?? [])) + } + return result.filter((child) => !isNavigationOnly(child)) + } + + const toggle = (resource: DocumentResource) => { + const targets = [ + ...(!isNavigationOnly(resource) ? [resource] : []), + ...loadedDescendants(resource), + ] + if (!targets.length) return + const next = new Set(selected) + if (targets.every((target) => next.has(target.key))) { + for (const target of targets) next.delete(target.key) + } else { + for (const target of targets) { + if (next.size >= MAX_SELECTION) break + next.add(target.key) + } + } + updateSelection(next) + } + + const toggleAll = () => { + const next = new Set(selected) + if (selectable.length && selectable.every((resource) => next.has(resource.key))) { + for (const resource of selectable) next.delete(resource.key) + } else { + for (const resource of selectable) { + if (next.size >= MAX_SELECTION) break + next.add(resource.key) + } + } + updateSelection(next) + } + + const toggleExpanded = (resource: DocumentResource) => { + const opening = !expanded.has(resource.key) + setExpanded((current) => { + const next = new Set(current) + if (opening) next.add(resource.key) + else next.delete(resource.key) + return next + }) + if (opening && !branches.get(resource.key)?.loaded && !branches.get(resource.key)?.loading) + void loadBranch(resource) + } + + const rows: Row[] = [] + const appendRows = (scope: string, depth: number) => { + const branch = branches.get(scope) + for (const resource of branch?.resources ?? []) { + rows.push({ kind: 'document', resource }) + if (expanded.has(resource.key)) appendRows(resource.key, depth + 1) + } + if (scope !== ROOT && branch?.nextCursor && !branch.loading && !branch.error) + rows.push({ kind: 'loadMore', depth, parentKey: scope }) + } + appendRows(ROOT, 0) + + const allSelected = + selectable.length > 0 && selectable.every((resource) => selected.has(resource.key)) + const providerName = root?.resources[0]?.document.workspace_name || binding.providerDisplayName + + return ( + <> + {!root?.loaded && !root?.loading && !root?.error && ( + + )} + {root?.loading && !root.loaded && ( +
+ +
+ )} + {root?.error && !root.loaded && ( +
+

{t(($) => $.providerLoadFailed)}

+ +
+ )} + {root?.loaded && ( +
+
+ $.selectAll, { ns: 'knowledgeSources' })} + aria-describedby={selectionAtLimit && !allSelected ? SELECTION_LIMIT_ID : undefined} + checked={allSelected} + disabled={disabled || !selectable.length || (selectionAtLimit && !allSelected)} + indeterminate={!allSelected && selected.size > 0} + onCheckedChange={toggleAll} + /> + + {t(($) => $.selectAll, { ns: 'knowledgeSources' })} + + + {providerName} + +
+
    + {rows.map((row) => { + if (row.kind === 'loadMore') { + const parent = allResources.find((resource) => resource.key === row.parentKey) + return ( +
  • + +
  • + ) + } + const { resource } = row + const branch = branches.get(resource.key) + const targets = [ + ...(!isNavigationOnly(resource) ? [resource] : []), + ...loadedDescendants(resource), + ] + const checked = + targets.length > 0 && targets.every((target) => selected.has(target.key)) + const indeterminate = !checked && targets.some((target) => selected.has(target.key)) + const expandable = Boolean(resource.document.has_children) + const isExpanded = expanded.has(resource.key) + return ( +
  • + {expandable ? ( + + ) : ( + + )} + toggle(resource)} + /> + + + {resource.document.name} + + {isExpanded && branch?.loading && ( + $.loading, { ns: 'common' })} + className="i-ri-loader-4-line size-4 animate-spin text-text-tertiary" + /> + )} + {isExpanded && branch?.error && ( + + )} +
  • + ) + })} +
+ {root.nextCursor && !root.loading && ( +
+ +
+ )} +
+ )} + {root?.loaded && ( + + {t(($) => $.pagesSelected, { ns: 'knowledgeSources', count: selected.size })} + {selectionAtLimit && ( + + {t(($) => $.maxPages, { ns: 'knowledgeSources' })}: {MAX_SELECTION} + + )} + + )} + + ) +}