diff --git a/packages/contracts/generated/knowledge-fs/metadata.gen.ts b/packages/contracts/generated/knowledge-fs/metadata.gen.ts index ecbe25e5b49..1b975bf9339 100644 --- a/packages/contracts/generated/knowledge-fs/metadata.gen.ts +++ b/packages/contracts/generated/knowledge-fs/metadata.gen.ts @@ -8,5 +8,5 @@ export const knowledgeFsConsoleDeclarationsSha256 = export const knowledgeFsGeneratedArtifactSha256 = { 'orpc.gen.ts': 'e0d9954f817e97a4e95dd38c4522fb403c8659d741ce485fa671b1b4ce90a540', 'types.gen.ts': 'a558ab80f32a8555bb5b44b7a596ef4a4a7a8cb7904390993aabcc587915f530', - 'zod.gen.ts': '6aa3d3e768fe0008ca405ecbe423ee325bd8f65f745dd2d1c86f2bdb887a97fd', + 'zod.gen.ts': 'ca698a6fa64a0717e29da5d4678976b55355a2c0fe5ddfa7037325aa79ab4762', } as const diff --git a/packages/contracts/generated/knowledge-fs/zod.gen.ts b/packages/contracts/generated/knowledge-fs/zod.gen.ts index a4ef693c8b0..359304ad6b1 100644 --- a/packages/contracts/generated/knowledge-fs/zod.gen.ts +++ b/packages/contracts/generated/knowledge-fs/zod.gen.ts @@ -1725,7 +1725,7 @@ export const zPostKnowledgeSpacesByIdDocumentsBody = z.object({ documentId: z.uuid().optional(), expectedActiveRevision: z.union([z.int().gt(0), z.enum(['null'])]).optional(), expectedDocumentRowVersion: z.int().gte(0).nullish(), - file: z.string(), + file: z.custom(), sourceId: z.uuid().optional(), }) @@ -1768,7 +1768,7 @@ export const zDeleteKnowledgeSpacesByIdDocumentsBulkPath = z.object({ export const zDeleteKnowledgeSpacesByIdDocumentsBulkResponse = zDurableBulkDeletionAccepted export const zPostKnowledgeSpacesByIdDocumentsBulkBody = z.object({ - files: z.array(z.string()).min(1), + files: z.array(z.custom()).min(1), targets: z.string().optional(), }) diff --git a/packages/contracts/openapi-ts.knowledge-fs.config.ts b/packages/contracts/openapi-ts.knowledge-fs.config.ts index 603267c39d1..7270068414d 100644 --- a/packages/contracts/openapi-ts.knowledge-fs.config.ts +++ b/packages/contracts/openapi-ts.knowledge-fs.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from '@hey-api/openapi-ts' +import { $, defineConfig } from '@hey-api/openapi-ts' const input = process.env.KNOWLEDGE_FS_OPENAPI const outputPath = process.env.KNOWLEDGE_FS_OUTPUT ?? 'generated/knowledge-fs' @@ -38,6 +38,17 @@ export default defineConfig({ }, { name: 'zod', + '~resolvers': { + string: (ctx) => { + if (ctx.schema.format === 'binary') + return $(ctx.symbols.z) + .attr('custom') + .call() + .generic($.type.or($.type('Blob'), $.type('File'))) + + return undefined + }, + }, }, { contracts: { diff --git a/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/documents/page.tsx b/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/documents/page.tsx index 3714c1248ed..185ef20d07a 100644 --- a/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/documents/page.tsx +++ b/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/documents/page.tsx @@ -1,5 +1,7 @@ -import { KnowledgeRoutePlaceholder } from '@/features/new-rag/knowledge-route-placeholder' +import { DocumentsPage } from '@/features/new-rag/documents-page' -export default function Page() { - return +export default async function Page({ params }: { params: Promise<{ knowledgeSpaceId: string }> }) { + const { knowledgeSpaceId } = await params + + return } diff --git a/web/app/components/base/loading/style.css b/web/app/components/base/loading/style.css index 688cfb31777..cb6501f2913 100644 --- a/web/app/components/base/loading/style.css +++ b/web/app/components/base/loading/style.css @@ -39,3 +39,9 @@ .spin-animation path:nth-child(4) { animation-delay: 1.5s; } + +@media (prefers-reduced-motion: reduce) { + .spin-animation path { + animation: none; + } +} diff --git a/web/context/__tests__/console-bootstrap.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx index ae5b3f6accc..53936b20974 100644 --- a/web/context/__tests__/console-bootstrap.spec.tsx +++ b/web/context/__tests__/console-bootstrap.spec.tsx @@ -16,6 +16,8 @@ import { createSystemFeaturesFixture } from '@/test/console/system-features' import { refreshUserProfileAtom, userProfileAtom } from '../account-state' import { initialWorkspaceInfo } from '../app-context-defaults' import { + datasetDefaultPermissionKeysAtom, + refreshWorkspacePermissionKeysAfterMutationDenialAtom, workspacePermissionKeysAtom, workspacePermissionKeysLoadingAtom, } from '../permission-state' @@ -31,7 +33,9 @@ import { } from '../workspace-state' const mockGetRequest = vi.hoisted(() => vi.fn()) +const mockGetPermissionKeys = vi.hoisted(() => vi.fn()) const mockPermissionKeysState = vi.hoisted(() => ({ + datasetPermissionKeys: ['dataset.acl.edit'], isPending: false, permissionKeys: ['app.create_and_management'], })) @@ -158,23 +162,7 @@ vi.mock('@/service/client', () => ({ get: { queryOptions: () => ({ queryKey: ['current-permissions'], - queryFn: async () => { - if (mockPermissionKeysState.isPending) return new Promise(() => {}) - - return { - workspace: { - permission_keys: mockPermissionKeysState.permissionKeys, - }, - app: { - default_permission_keys: [], - overrides: [], - }, - dataset: { - default_permission_keys: [], - overrides: [], - }, - } - }, + queryFn: mockGetPermissionKeys, }), }, }, @@ -234,11 +222,15 @@ function ConsoleBootstrapProbe() { const isCurrentWorkspaceEditor = useAtomValue(isCurrentWorkspaceEditorAtom) const isCurrentWorkspaceDatasetOperator = useAtomValue(isCurrentWorkspaceDatasetOperatorAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) + const datasetDefaultPermissionKeys = useAtomValue(datasetDefaultPermissionKeysAtom) const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom) const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom) const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) const refreshUserProfile = useSetAtom(refreshUserProfileAtom) const refreshCurrentWorkspace = useSetAtom(refreshCurrentWorkspaceAtom) + const refreshPermissionsAfterMutationDenial = useSetAtom( + refreshWorkspacePermissionKeysAfterMutationDenialAtom, + ) return ( <> @@ -246,6 +238,10 @@ function ConsoleBootstrapProbe() { keys: {workspacePermissionKeys.join(',')} + + dataset keys: + {datasetDefaultPermissionKeys.join(',')} + permission loading: {String(isLoadingWorkspacePermissionKeys)} @@ -293,6 +289,9 @@ function ConsoleBootstrapProbe() { + ) } @@ -349,7 +348,25 @@ describe('Console bootstrap', () => { vi.clearAllMocks() setAnalyticsConsent('granted') mockPermissionKeysState.isPending = false + mockPermissionKeysState.datasetPermissionKeys = ['dataset.acl.edit'] mockPermissionKeysState.permissionKeys = ['app.create_and_management'] + mockGetPermissionKeys.mockImplementation(async () => { + if (mockPermissionKeysState.isPending) return new Promise(() => {}) + + return { + workspace: { + permission_keys: mockPermissionKeysState.permissionKeys, + }, + app: { + default_permission_keys: [], + overrides: [], + }, + dataset: { + default_permission_keys: mockPermissionKeysState.datasetPermissionKeys, + overrides: [], + }, + } + }) mockCurrentWorkspaceQueryState.data = mockCurrentWorkspaceResponse mockCurrentWorkspaceQueryState.isPending = false mockUserProfileResponseState.data = { @@ -393,6 +410,7 @@ describe('Console bootstrap', () => { expect(await screen.findByText('user:user@example.com')).toBeInTheDocument() expect(await screen.findByText('workspace:Workspace')).toBeInTheDocument() expect(await screen.findByText('keys:app.create_and_management')).toBeInTheDocument() + expect(screen.getByText('dataset keys:dataset.acl.edit')).toBeInTheDocument() expect(screen.getByText('permission loading:false')).toBeInTheDocument() expect(screen.getByText('workspace loading:false')).toBeInTheDocument() expect(await screen.findByText('version:1.0.0/1.0.1/cloud')).toBeInTheDocument() @@ -400,6 +418,7 @@ describe('Console bootstrap', () => { it('should fall back to placeholder values when workspace, permission, or version data is missing', async () => { mockCurrentWorkspaceQueryState.data = undefined + mockPermissionKeysState.datasetPermissionKeys = [] mockPermissionKeysState.permissionKeys = [] mockLangGeniusVersionState.data = undefined @@ -409,6 +428,7 @@ describe('Console bootstrap', () => { expect(screen.getByText(`workspace:${initialWorkspaceInfo.name}`)).toBeInTheDocument() expect(screen.getByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument() expect(screen.getByText('keys:')).toBeInTheDocument() + expect(screen.getByText('dataset keys:')).toBeInTheDocument() expect(screen.getByText('version://')).toBeInTheDocument() }) @@ -459,6 +479,31 @@ describe('Console bootstrap', () => { expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user-profile'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['current-workspace'] }) }) + + it('starts a fresh permission request without waiting for an older request', async () => { + const { queryClient } = renderConsoleBootstrap() + await screen.findByText('dataset keys:dataset.acl.edit') + const olderRequest = new Promise(() => {}) + let permissionRequestCount = 0 + mockGetPermissionKeys.mockImplementation(() => { + permissionRequestCount += 1 + if (permissionRequestCount === 1) return olderRequest + return Promise.resolve({ + workspace: { permission_keys: [] }, + app: { default_permission_keys: [], overrides: [] }, + dataset: { default_permission_keys: ['dataset.acl.readonly'], overrides: [] }, + }) + }) + + const backgroundRefresh = queryClient.refetchQueries({ + queryKey: ['current-permissions'], + }) + await waitFor(() => expect(permissionRequestCount).toBe(1)) + fireEvent.click(screen.getByRole('button', { name: /refresh permissions after denial/i })) + await waitFor(() => expect(permissionRequestCount).toBe(2)) + expect(await screen.findByText('dataset keys:dataset.acl.readonly')).toBeInTheDocument() + await backgroundRefresh + }) }) describe('External side effects', () => { diff --git a/web/context/permission-state.ts b/web/context/permission-state.ts index bd11933e7d3..bbe3e15ad4b 100644 --- a/web/context/permission-state.ts +++ b/web/context/permission-state.ts @@ -16,6 +16,26 @@ export const workspacePermissionKeysAtom = atom((get) => { ) }) +export const datasetDefaultPermissionKeysAtom = atom((get) => { + return get(workspacePermissionKeysQueryAtom).data?.dataset?.default_permission_keys ?? [] +}) + export const workspacePermissionKeysLoadingAtom = atom((get) => { return get(workspacePermissionKeysQueryAtom).isPending }) + +export const workspacePermissionKeysFetchingAtom = atom((get) => { + return get(workspacePermissionKeysQueryAtom).isFetching +}) + +export const workspacePermissionKeysErrorAtom = atom((get) => { + return get(workspacePermissionKeysQueryAtom).error +}) + +export const retryWorkspacePermissionKeysAtom = atom(null, (get) => { + return get(workspacePermissionKeysQueryAtom).refetch({ cancelRefetch: false }) +}) + +export const refreshWorkspacePermissionKeysAfterMutationDenialAtom = atom(null, (get) => { + return get(workspacePermissionKeysQueryAtom).refetch({ cancelRefetch: true }) +}) diff --git a/web/features/new-rag/README.md b/web/features/new-rag/README.md index 2ccf1402a43..1d30a942899 100644 --- a/web/features/new-rag/README.md +++ b/web/features/new-rag/README.md @@ -1,20 +1,39 @@ # New RAG -Renders the KnowledgeFS-backed knowledge list, creation entry points, and first-visit guidance. +Renders the KnowledgeFS-backed knowledge list, creation flows, sources, documents, and processing +tasks. ## Internal Modules +- `add-source-page` - `components/create-knowledge-dialog-parts` - `components/create-knowledge-exit-dialog` - `components/knowledge-space-card` - `components/knowledge-view-switcher` - `components/new-knowledge-list-states` +- `crawl-selection-form` +- `create-knowledge-page` +- `create-upload-queue` - `create-knowledge-workflow` +- `document-list` +- `document-model` +- `document-upload-policy` +- `documents-page` +- `knowledge-route-guard` +- `knowledge-space-shell` +- `new-knowledge-list` +- `processing-tasks-drawer` +- `routes` +- `services/processing-task-events` +- `sources-page` - `storage` +- `task-event-observer` +- `website-crawl-preview` ## External Modules - `app/components/base/corner-label` +- `app/components/base/loading` - `app/components/base/search-input` - `app/components/base/skeleton` - `app/components/datasets/external-api/external-api-panel` diff --git a/web/features/new-rag/__tests__/auxiliary-task-read-guard.spec.ts b/web/features/new-rag/__tests__/auxiliary-task-read-guard.spec.ts new file mode 100644 index 00000000000..fab74652567 --- /dev/null +++ b/web/features/new-rag/__tests__/auxiliary-task-read-guard.spec.ts @@ -0,0 +1,96 @@ +import { act, renderHook } from '@testing-library/react' +import { + createAuxiliaryTaskReadGuard, + useAuxiliaryTaskReadGuard, +} from '../auxiliary-task-read-guard' + +describe('createAuxiliaryTaskReadGuard', () => { + it('blocks every auxiliary reader for the denied task version', () => { + const guard = createAuxiliaryTaskReadGuard() + const olderVersion = '2026-07-20T10:01:00Z' + const deniedVersion = '2026-07-20T10:02:00Z' + const newerVersion = '2026-07-20T10:03:00Z' + + guard.block('task-1', deniedVersion) + + expect(guard.isBlocked('task-1', olderVersion)).toBe(true) + expect(guard.isBlocked('task-1', deniedVersion)).toBe(true) + expect(guard.isBlocked('task-1', newerVersion)).toBe(false) + expect(guard.isBlocked('task-2', deniedVersion)).toBe(false) + + guard.block('task-1', olderVersion) + expect(guard.isBlocked('task-1', olderVersion)).toBe(true) + expect(guard.isBlocked('task-1', deniedVersion)).toBe(true) + }) + + it('retires blocks after task or document authority changes', () => { + const guard = createAuxiliaryTaskReadGuard() + guard.block('task-1', 'version-1') + guard.block('task-2', 'version-1') + + guard.retain(new Set(['task-1'])) + expect(guard.isBlocked('task-1', 'version-1')).toBe(true) + expect(guard.isBlocked('task-2', 'version-1')).toBe(false) + + guard.clearTask('task-1') + expect(guard.isBlocked('task-1', 'version-1')).toBe(false) + + guard.block('task-1', 'version-2') + guard.clear() + expect(guard.isBlocked('task-1', 'version-2')).toBe(false) + }) + + it('retires a block only after an authoritative version catches up', () => { + const guard = createAuxiliaryTaskReadGuard() + guard.block('task-1', '2026-07-20T10:02:00Z') + + expect(guard.clearThrough('task-1', '2026-07-20T10:01:00Z')).toBe(false) + expect(guard.isBlocked('task-1', '2026-07-20T10:02:00Z')).toBe(true) + + expect(guard.clearThrough('task-1', '2026-07-20T10:02:00Z')).toBe(true) + expect(guard.isBlocked('task-1', '2026-07-20T10:02:00Z')).toBe(false) + }) + + it('restores the page after authoritative document verification without retrying the task version', async () => { + let resolveRefetch!: (result: { error: null }) => void + const refetchDocuments = vi.fn( + () => + new Promise<{ error: null }>((resolve) => { + resolveRefetch = resolve + }), + ) + const { result } = renderHook(() => + useAuxiliaryTaskReadGuard({ documentPermissionDenied: false, refetchDocuments }), + ) + + act(() => result.current.deny('task-1', 'version-1')) + expect(result.current.permissionDenied).toBe(true) + expect(result.current.guard.isBlocked('task-1', 'version-1')).toBe(true) + + await act(async () => resolveRefetch({ error: null })) + expect(result.current.permissionDenied).toBe(false) + expect(result.current.guard.isBlocked('task-1', 'version-1')).toBe(true) + }) + + it('clears local denial and version blocks after document permission returns', async () => { + const refetchDocuments = vi + .fn() + .mockResolvedValue({ error: new Response(null, { status: 403 }) }) + const { result, rerender } = renderHook( + ({ documentPermissionDenied }) => + useAuxiliaryTaskReadGuard({ documentPermissionDenied, refetchDocuments }), + { initialProps: { documentPermissionDenied: false } }, + ) + + act(() => result.current.deny('task-1', 'version-1')) + await act(async () => {}) + expect(result.current.permissionDenied).toBe(true) + + rerender({ documentPermissionDenied: true }) + rerender({ documentPermissionDenied: false }) + + expect(result.current.permissionDenied).toBe(false) + expect(result.current.guard.isBlocked('task-1', 'version-1')).toBe(false) + expect(result.current.guardRevision).toBe(1) + }) +}) diff --git a/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx b/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx index 1397163b0d9..f02c41c5d8b 100644 --- a/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx +++ b/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx @@ -9,6 +9,8 @@ const serviceMock = vi.hoisted(() => ({ create: vi.fn(), getPolicy: vi.fn(), patchPolicy: vi.fn(), + upload: vi.fn(), + uploadBulk: vi.fn(), listKey: vi.fn(() => ['console', 'knowledgeFs', 'listKnowledgeSpaces']), })) @@ -54,6 +56,8 @@ vi.mock('@/service/client', () => ({ createKnowledgeSpace: serviceMock.create, getKnowledgeSpacesByIdAccessPolicy: serviceMock.getPolicy, patchKnowledgeSpacesByIdAccessPolicy: serviceMock.patchPolicy, + postKnowledgeSpacesByIdDocuments: serviceMock.upload, + postKnowledgeSpacesByIdDocumentsBulk: serviceMock.uploadBulk, }, }, consoleQuery: { @@ -120,6 +124,14 @@ describe('CreateKnowledgePage', () => { revision: 5, visibility: 'all_members', }) + serviceMock.upload.mockResolvedValue({ + id: 'document-1', + }) + serviceMock.uploadBulk.mockResolvedValue({ + accepted: 2, + excluded: 0, + items: [], + }) permissionStateMock.keys = ['dataset.create_and_management', 'dataset.acl.access_config'] navigationMock.startMode = null vi.spyOn(globalThis.crypto, 'randomUUID').mockReturnValue( @@ -129,6 +141,7 @@ describe('CreateKnowledgePage', () => { afterEach(() => { globalThis.sessionStorage.clear() + vi.useRealTimers() vi.restoreAllMocks() }) @@ -373,24 +386,138 @@ describe('CreateKnowledgePage', () => { expect(serviceMock.patchPolicy).toHaveBeenCalledOnce() }) - it('integrates source setup while keeping the later upload flow disabled', async () => { + it('keeps every start mode interactive without simulating backend success', async () => { const user = userEvent.setup() - navigationMock.startMode = 'source' renderPage() const startEmpty = screen.getByRole('radio', { name: 'dataset.newKnowledge.startEmpty' }) - expect(startEmpty).not.toBeChecked() + expect(startEmpty).toBeChecked() expect(startEmpty).toHaveAccessibleDescription('dataset.newKnowledge.startEmptyDescription') const connectSource = screen.getByRole('radio', { name: 'dataset.newKnowledge.connectSource', }) const uploadFiles = screen.getByRole('radio', { name: 'dataset.newKnowledge.uploadFiles' }) expect(connectSource).toBeEnabled() + expect(uploadFiles).toBeEnabled() + + await user.click(connectSource) expect(connectSource).toBeChecked() expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })).toBeChecked() expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() + for (const unavailableProvider of ['Jina Reader', 'WaterCrawl']) { + await user.click(screen.getByRole('radio', { name: unavailableProvider })) + expect(screen.getByRole('radio', { name: unavailableProvider })).toBeChecked() + } + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.moreProviders' })).toBeEnabled() + expect(screen.getByText('dataset.newKnowledge.crawlOptions')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' }), + ).toBeDisabled() + expect(screen.getByText('dataset.newKnowledge.pagesAppearTitle')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.usingDefaults')).toBeInTheDocument() + const rootUrl = screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder') + const sourceName = screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder') + expect(rootUrl).toBeEnabled() + expect(sourceName).toBeEnabled() + await user.type(rootUrl, 'https://docs.dify.ai') + await user.type(sourceName, 'Dify docs') + const crawlAndPreview = screen.getByRole('button', { + name: 'dataset.newKnowledge.crawlAndPreview', + }) + expect(crawlAndPreview).toBeEnabled() + await user.click(crawlAndPreview) + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.sourceSetupBackendDependency', + ) + expect(screen.queryByText(/^dataset\.newKnowledge\.crawlingPages/)).not.toBeInTheDocument() + expect(screen.queryByText(/^dataset\.newKnowledge\.pagesCrawled/)).not.toBeInTheDocument() + const onlineDocuments = screen.getByRole('radio', { + name: 'dataset.newKnowledge.onlineDocuments', + }) + await user.click(onlineDocuments) + expect(onlineDocuments).toBeChecked() + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + expect(screen.getByText('Notion')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.notionNotConnected')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.connectNotion' })).toBeEnabled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectNotion' })) + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.sourceSetupBackendDependency', + ) + await user.click(screen.getByRole('radio', { name: 'Google Docs' })) + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() + expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toBeEnabled() + await user.click(uploadFiles) + expect(uploadFiles).toBeChecked() + const uploadInput = screen.getByLabelText('dataset.newKnowledge.uploadFiles', { + selector: 'input[type="file"]', + }) + expect(uploadInput).toBeInTheDocument() + expect(uploadInput).not.toHaveAttribute('hidden') + expect(uploadInput.nextElementSibling).toHaveClass('peer-focus-visible:ring-2') + uploadInput.focus() + expect(uploadInput).toHaveFocus() expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled() + }) + it('continues from the upload mode after real creation succeeds', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'upload' + renderPage() + + expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.uploadFiles' })).toBeChecked() + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadFiles', { + selector: 'input[type="file"]', + }), + new File(['content'], 'handbook.md', { type: 'text/markdown' }), + ) + await fillRequiredFields(user) + await choosePermission(user, 'dataset.newKnowledge.permissionOnlyMe') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + + await waitFor(() => + expect(routerMock.replace).toHaveBeenCalledWith( + '/datasets/new/e735c1dc-d2b8-4dc4-86dc-abaf2fb7d084/documents', + ), + ) + expect(serviceMock.upload).toHaveBeenCalledWith({ + body: { file: expect.objectContaining({ name: 'handbook.md' }) }, + params: { id: createdKnowledge.id }, + }) + }) + + it('queues uploads when native random UUID generation is unavailable', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'upload' + vi.restoreAllMocks() + const descriptor = Object.getOwnPropertyDescriptor(globalThis.crypto, 'randomUUID') + Object.defineProperty(globalThis.crypto, 'randomUUID', { + configurable: true, + value: undefined, + }) + + try { + renderPage() + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadFiles', { + selector: 'input[type="file"]', + }), + new File(['content'], 'handbook.md', { type: 'text/markdown' }), + ) + + expect(screen.getByText('handbook.md')).toBeInTheDocument() + } finally { + if (descriptor) Object.defineProperty(globalThis.crypto, 'randomUUID', descriptor) + else Reflect.deleteProperty(globalThis.crypto, 'randomUUID') + } + }) + + it('hands the configured website draft to the real add-source workflow', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + renderPage() + await fillRequiredFields(user) await user.type( screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder'), 'https://docs.dify.ai', @@ -399,11 +526,192 @@ describe('CreateKnowledgePage', () => { screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'), 'Dify docs', ) - expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeEnabled() - expect(uploadFiles).toBeDisabled() - expect(uploadFiles).toHaveAccessibleDescription( - 'dataset.newKnowledge.uploadFilesDescription dataset.cornerLabel.unavailable', + await user.keyboard('{Enter}') + expect(serviceMock.create).not.toHaveBeenCalled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlOptions' })) + await user.click(screen.getByRole('checkbox', { name: 'dataset.newKnowledge.includeSubpages' })) + const maxPages = screen.getByRole('spinbutton', { name: 'dataset.newKnowledge.maxPages' }) + await user.clear(maxPages) + await user.type(maxPages, '25') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + + await waitFor(() => + expect(routerMock.replace).toHaveBeenCalledWith( + '/datasets/new/e735c1dc-d2b8-4dc4-86dc-abaf2fb7d084/sources/new?type=websiteCrawl&draft=a9c36c57-2d84-44d6-a36d-841f0d92a179', + ), ) + expect( + JSON.parse( + globalThis.sessionStorage.getItem( + newKnowledgeSourceDraftStorageKey('a9c36c57-2d84-44d6-a36d-841f0d92a179'), + ) ?? '', + ), + ).toEqual({ + includeSubpages: false, + maxPages: 25, + provider: 'Firecrawl', + rootUrl: 'https://docs.dify.ai', + sourceName: 'Dify docs', + sourceType: 'websiteCrawl', + syncPolicy: 'provider', + }) + }) + + it('preserves online document configuration across the real navigation boundary', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + renderPage() + await fillRequiredFields(user) + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) + await user.click(screen.getByRole('radio', { name: 'Google Docs' })) + await user.type( + screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }), + 'Shared product docs', + ) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'daily', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + + await waitFor(() => + expect(routerMock.replace).toHaveBeenCalledWith( + '/datasets/new/e735c1dc-d2b8-4dc4-86dc-abaf2fb7d084/sources/new?type=onlineDocuments&draft=a9c36c57-2d84-44d6-a36d-841f0d92a179', + ), + ) + expect( + JSON.parse( + globalThis.sessionStorage.getItem( + newKnowledgeSourceDraftStorageKey('a9c36c57-2d84-44d6-a36d-841f0d92a179'), + ) ?? '', + ), + ).toEqual({ + provider: 'Google Docs', + sourceName: 'Shared product docs', + sourceType: 'onlineDocuments', + syncPolicy: 'daily', + }) + }) + + it('keeps each source type draft when the user switches between them', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + renderPage() + await user.type( + screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder'), + 'https://docs.dify.ai', + ) + await user.type( + screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'), + 'Website docs', + ) + + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() + await user.type( + screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }), + 'Notion docs', + ) + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })) + + expect(screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder')).toHaveValue( + 'https://docs.dify.ai', + ) + expect(screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder')).toHaveValue( + 'Website docs', + ) + }) + + it('uses the same website validation as the add-source workflow', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + renderPage() + await fillRequiredFields(user) + const rootUrl = screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder') + const sourceName = screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder') + expect(rootUrl).toHaveAttribute('maxlength', '2048') + expect(sourceName).toHaveAttribute('maxlength', '200') + + await user.type(rootUrl, 'https://user:secret@docs.dify.ai') + await user.type(sourceName, 'Dify docs') + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + expect(serviceMock.create).not.toHaveBeenCalled() + + await user.clear(rootUrl) + await user.type(rootUrl, 'https://docs.dify.ai') + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeEnabled() + }) + + it('keeps an invalid upload visible and prevents creating the knowledge space', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'upload' + renderPage() + await fillRequiredFields(user) + const oversizedFile = new File(['content'], 'oversized.pdf', { type: 'application/pdf' }) + Object.defineProperty(oversizedFile, 'size', { value: 16 * 1024 * 1024 }) + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadFiles', { + selector: 'input[type="file"]', + }), + oversizedFile, + ) + + expect(screen.getByText('oversized.pdf')).toBeInTheDocument() + expect( + screen.getByText('dataset.newKnowledge.documentUploadExclusion.fileSize'), + ).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled() + expect(screen.queryByRole('button', { name: 'dataset.newKnowledge.preview' })).toBeNull() + expect(serviceMock.create).not.toHaveBeenCalled() + }) + + it('shows the real uploading state on each valid file row', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'upload' + serviceMock.upload.mockImplementation(() => new Promise(() => {})) + renderPage() + await fillRequiredFields(user) + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadFiles', { + selector: 'input[type="file"]', + }), + new File(['content'], 'handbook.md', { type: 'text/markdown' }), + ) + const queue = screen.getByRole('list', { name: 'dataset.newKnowledge.uploadFiles' }) + const preview = within(queue).getByRole('button', { name: 'dataset.newKnowledge.preview' }) + expect(preview).toBeDisabled() + expect(preview).toHaveAccessibleDescription('dataset.newKnowledge.previewUnavailable') + expect(screen.getByText('dataset.newKnowledge.previewUnavailable')).toBeVisible() + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + + expect(await within(queue).findByText('dataset.newKnowledge.uploadingFiles')).toBeVisible() + expect(within(queue).queryByRole('button', { name: 'dataset.newKnowledge.preview' })).toBeNull() + }) + + it('retries upload without creating a duplicate knowledge space', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'upload' + serviceMock.upload.mockRejectedValueOnce(new Error('KnowledgeFS unavailable')) + renderPage() + await fillRequiredFields(user) + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadFiles', { + selector: 'input[type="file"]', + }), + new File(['content'], 'handbook.md', { type: 'text/markdown' }), + ) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentUploadFailed', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })) + + await waitFor(() => expect(routerMock.replace).toHaveBeenCalled()) + expect(serviceMock.create).toHaveBeenCalledOnce() + expect(serviceMock.upload).toHaveBeenCalledTimes(2) }) it('hands the configured source draft to the add-source workflow after creation', async () => { diff --git a/web/features/new-rag/__tests__/document-model.spec.ts b/web/features/new-rag/__tests__/document-model.spec.ts new file mode 100644 index 00000000000..7b97acd7efc --- /dev/null +++ b/web/features/new-rag/__tests__/document-model.spec.ts @@ -0,0 +1,118 @@ +import type { + DocumentProcessingTask, + LogicalDocument, +} from '@dify/contracts/knowledge-fs/types.gen' +import { + documentDisplayStatus, + newestTaskByDocument, + taskCanRetry, + taskIsActive, + taskNeedsAttention, + taskVersionIsAfter, +} from '../document-model' + +const document = (overrides: Partial = {}): LogicalDocument => ({ + active: { + contentHash: 'hash', + createdAt: '2026-07-20T10:00:00Z', + documentAssetId: 'asset-1', + documentAssetVersion: 1, + documentId: 'document-1', + knowledgeSpaceId: 'space-1', + mimeType: 'text/plain', + revision: 2, + sizeBytes: 10, + state: 'active', + }, + activeRevision: 2, + createdAt: '2026-07-20T10:00:00Z', + id: 'document-1', + knowledgeSpaceId: 'space-1', + rowVersion: 1, + status: 'ready', + title: 'Document', + updatedAt: '2026-07-20T10:00:00Z', + userMetadata: {}, + ...overrides, +}) + +const task = (overrides: Partial = {}): DocumentProcessingTask => ({ + createdAt: '2026-07-20T10:00:00Z', + documentId: 'document-1', + documentRevision: 3, + id: 'task-1', + knowledgeSpaceId: 'space-1', + progressPercent: 10, + stage: 'queued', + state: 'queued', + updatedAt: '2026-07-20T10:00:00Z', + ...overrides, +}) + +describe('new Knowledge document model', () => { + it('chooses the highest revision before a more recently updated stale task', () => { + const result = newestTaskByDocument([ + task({ documentRevision: 2, id: 'stale', updatedAt: '2026-07-20T10:10:00Z' }), + task({ documentRevision: 3, id: 'current', updatedAt: '2026-07-20T10:01:00Z' }), + ]) + + expect(result.get('document-1')?.id).toBe('current') + }) + + it('compares task versions without losing sub-millisecond precision', () => { + expect(taskVersionIsAfter('2026-07-20T10:00:00.123789Z', '2026-07-20T10:00:00.123456Z')).toBe( + true, + ) + expect(taskVersionIsAfter('2026-07-20T10:00:00.000001Z', '2026-07-20T10:00:00Z')).toBe(true) + expect(taskVersionIsAfter('2026-07-20T10:00:00Z', '2026-07-20T10:00:00.000001Z')).toBe(false) + }) + + it('selects the newest task when versions differ only after milliseconds', () => { + const result = newestTaskByDocument([ + task({ id: 'older', updatedAt: '2026-07-20T10:00:00.123456Z' }), + task({ id: 'newer', updatedAt: '2026-07-20T10:00:00.123789Z' }), + ]) + + expect(result.get('document-1')?.id).toBe('newer') + }) + + it.each([ + ['dispatch_pending', 'queued'], + ['queued', 'queued'], + ['retry_wait', 'queued'], + ['running', 'processing'], + ['failed', 'failed'], + ] as const)('maps a latest %s task to %s', (state, expected) => { + expect(documentDisplayStatus(document(), task({ state }))).toBe(expected) + }) + + it('keeps a last ready revision available after a canceled re-index', () => { + expect(documentDisplayStatus(document(), task({ state: 'canceled' }))).toBe('ready') + expect( + documentDisplayStatus( + document({ active: null, activeRevision: undefined, status: 'pending' }), + task({ state: 'canceled' }), + ), + ).toBe('failed') + }) + + it('does not let an older failed task override the active revision', () => { + expect(documentDisplayStatus(document(), task({ documentRevision: 1, state: 'failed' }))).toBe( + 'ready', + ) + }) + + it('maps a disabled source and deleting lifecycle to the designed disabled state', () => { + expect(documentDisplayStatus(document(), undefined, true)).toBe('disabled') + expect(documentDisplayStatus(document({ status: 'deleting' }))).toBe('disabled') + }) + + it('keeps task badge and actions aligned with the contract states', () => { + expect(taskIsActive(task({ state: 'running' }))).toBe(true) + expect(taskNeedsAttention(task({ state: 'failed' }))).toBe(true) + expect(taskCanRetry(task({ state: 'canceled' }))).toBe(false) + expect(taskNeedsAttention(task({ state: 'canceled' }))).toBe(false) + expect(taskNeedsAttention(task({ state: 'succeeded' }))).toBe(false) + expect(taskCanRetry(task({ state: 'superseded' }))).toBe(false) + }) +}) diff --git a/web/features/new-rag/__tests__/documents-page.spec.tsx b/web/features/new-rag/__tests__/documents-page.spec.tsx new file mode 100644 index 00000000000..034bac3812a --- /dev/null +++ b/web/features/new-rag/__tests__/documents-page.spec.tsx @@ -0,0 +1,5767 @@ +import type { + DocumentProcessingTask, + LogicalDocument, + Source, +} from '@dify/contracts/knowledge-fs/types.gen' +import { hashKey } from '@tanstack/react-query' +import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { renderWithNuqs as render } from '@/test/nuqs-testing' +import { DocumentsPage } from '../documents-page' +import { TaskEventObserver } from '../task-event-observer' + +type InfiniteOptions = { + enabled?: boolean + getNextPageParam: (lastPage: { nextCursor?: string }) => string | undefined + input: (pageParam: string | null) => unknown + initialPageParam: string | null + queryKind: 'documents' | 'sources' | 'tasks' + queryKey?: readonly unknown[] + refetchInterval?: (query: { state: { data?: unknown } }) => false | number +} + +const documentsQuery = vi.hoisted(() => ({ + data: undefined as + | { pages: Array<{ items: LogicalDocument[]; nextCursor?: string }> } + | undefined, + error: null as unknown, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetchNextPageError: false, + isFetching: false, + isFetchingNextPage: false, + isPending: false, + isRefetching: false, + refetch: vi.fn(), +})) + +const tasksQuery = vi.hoisted(() => ({ + data: undefined as + | { pages: Array<{ items: DocumentProcessingTask[]; nextCursor?: string }> } + | undefined, + dataUpdatedAt: 0, + dataUpdateCount: 0, + error: null as unknown, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetchNextPageError: false, + isFetching: false, + isFetchingNextPage: false, + isPending: false, + refetch: vi.fn(), +})) + +const sourcesQuery = vi.hoisted(() => ({ + data: undefined as { pages: Array<{ items: Source[]; nextCursor?: string }> } | undefined, + error: null as unknown, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetchNextPageError: false, + isFetching: false, + isFetchingNextPage: false, + isPending: false, + refetch: vi.fn(), +})) + +const cancelMutation = vi.hoisted(() => ({ mutateAsync: vi.fn() })) +const retryMutation = vi.hoisted(() => ({ mutateAsync: vi.fn() })) +const reindexMutation = vi.hoisted(() => ({ mutateAsync: vi.fn() })) +const uploadMutation = vi.hoisted(() => ({ mutateAsync: vi.fn() })) +const bulkUploadMutation = vi.hoisted(() => ({ mutateAsync: vi.fn() })) +const queryCacheListeners = vi.hoisted( + () => + new Set< + (event: { action: { type: string }; query: { queryHash: string }; type: string }) => void + >(), +) +const queryClient = vi.hoisted(() => ({ + cancelQueries: vi.fn(), + getQueryCache: vi.fn(() => ({ + subscribe: ( + listener: (event: { + action: { type: string } + query: { queryHash: string } + type: string + }) => void, + ) => { + queryCacheListeners.add(listener) + return () => queryCacheListeners.delete(listener) + }, + })), + getQueryState: vi.fn(() => ({ dataUpdateCount: tasksQuery.dataUpdateCount })), + invalidateQueries: vi.fn(), +})) +const streamProcessingTaskEvents = vi.hoisted(() => vi.fn()) +const getTaskSnapshot = vi.hoisted(() => vi.fn()) +const permissionStateMock = vi.hoisted(() => ({ + datasetAtom: Symbol('datasetDefaultPermissionKeysAtom'), + datasetKeys: ['dataset.acl.edit'], + error: null as unknown, + errorAtom: Symbol('workspacePermissionKeysErrorAtom'), + fetching: false, + fetchingAtom: Symbol('workspacePermissionKeysFetchingAtom'), + loading: false, + loadingAtom: Symbol('workspacePermissionKeysLoadingAtom'), + retry: vi.fn(), + retryAtom: Symbol('retryWorkspacePermissionKeysAtom'), + refreshAfterDenial: vi.fn(), + refreshAfterDenialAtom: Symbol('refreshWorkspacePermissionKeysAfterMutationDenialAtom'), +})) +const toastMock = vi.hoisted(() => ({ + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warning: vi.fn(), +})) + +vi.mock('@/context/permission-state', () => ({ + datasetDefaultPermissionKeysAtom: permissionStateMock.datasetAtom, + refreshWorkspacePermissionKeysAfterMutationDenialAtom: permissionStateMock.refreshAfterDenialAtom, + retryWorkspacePermissionKeysAtom: permissionStateMock.retryAtom, + workspacePermissionKeysErrorAtom: permissionStateMock.errorAtom, + workspacePermissionKeysFetchingAtom: permissionStateMock.fetchingAtom, + workspacePermissionKeysLoadingAtom: permissionStateMock.loadingAtom, +})) + +vi.mock('jotai', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + useAtomValue: (atom: unknown) => { + if (atom === permissionStateMock.datasetAtom) return permissionStateMock.datasetKeys + if (atom === permissionStateMock.errorAtom) return permissionStateMock.error + if (atom === permissionStateMock.fetchingAtom) return permissionStateMock.fetching + if (atom === permissionStateMock.loadingAtom) return permissionStateMock.loading + return original.useAtomValue(atom as Parameters[0]) + }, + useSetAtom: (atom: unknown) => + atom === permissionStateMock.retryAtom + ? permissionStateMock.retry + : atom === permissionStateMock.refreshAfterDenialAtom + ? permissionStateMock.refreshAfterDenial + : original.useSetAtom(atom as Parameters[0]), + } +}) + +vi.mock('@langgenius/dify-ui/toast', () => ({ toast: toastMock })) + +const documentsInfiniteOptions = vi.hoisted(() => + vi.fn((options: Omit) => ({ ...options, queryKind: 'documents' })), +) +const tasksInfiniteOptions = vi.hoisted(() => + vi.fn((options: Omit) => ({ + ...options, + queryKey: ['knowledge-fs', 'tasks'], + queryKind: 'tasks', + })), +) +const sourcesInfiniteOptions = vi.hoisted(() => + vi.fn((options: Omit) => ({ ...options, queryKind: 'sources' })), +) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + useInfiniteQuery: (options: InfiniteOptions) => { + if (options.queryKind === 'documents') return documentsQuery + if (options.queryKind === 'sources') return sourcesQuery + return tasksQuery + }, + useMutation: (options: { + mutationKind: 'bulk-upload' | 'cancel' | 'reindex' | 'retry' | 'upload' + }) => { + if (options.mutationKind === 'cancel') return cancelMutation + if (options.mutationKind === 'retry') return retryMutation + if (options.mutationKind === 'reindex') return reindexMutation + if (options.mutationKind === 'bulk-upload') return bulkUploadMutation + return uploadMutation + }, + useQueryClient: () => queryClient, + } +}) + +vi.mock('@/service/client', () => ({ + consoleClient: { + knowledgeFs: { + getKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskId: getTaskSnapshot, + }, + }, + consoleQuery: { + knowledgeFs: { + deleteKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskId: { + mutationOptions: () => ({ mutationKind: 'cancel' }), + }, + getKnowledgeSpacesByIdLogicalDocuments: { + infiniteOptions: documentsInfiniteOptions, + key: () => ['knowledge-fs', 'documents'], + }, + getKnowledgeSpacesByIdProcessingTasks: { + infiniteOptions: tasksInfiniteOptions, + key: () => ['knowledge-fs', 'tasks'], + }, + getKnowledgeSpacesByIdSources: { + infiniteOptions: sourcesInfiniteOptions, + key: () => ['knowledge-fs', 'sources'], + }, + postKnowledgeSpacesByIdDocuments: { + mutationOptions: () => ({ mutationKind: 'upload' }), + }, + postKnowledgeSpacesByIdDocumentsBulk: { + mutationOptions: () => ({ mutationKind: 'bulk-upload' }), + }, + postKnowledgeSpacesByIdDocumentsBulkReindex: { + mutationOptions: () => ({ mutationKind: 'reindex' }), + }, + postKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskIdRetry: { + mutationOptions: () => ({ mutationKind: 'retry' }), + }, + }, + }, +})) + +vi.mock('../services/processing-task-events', () => ({ streamProcessingTaskEvents })) + +const document = (overrides: Partial = {}): LogicalDocument => ({ + active: { + activatedAt: '2026-07-20T10:01:00Z', + contentHash: 'hash', + createdAt: '2026-07-20T10:00:00Z', + documentAssetId: 'asset-1', + documentAssetVersion: 1, + documentId: 'document-1', + knowledgeSpaceId: 'space-1', + mimeType: 'application/pdf', + revision: 2, + sizeBytes: 1024, + state: 'active', + }, + activeRevision: 2, + createdAt: '2026-07-20T10:00:00Z', + id: 'document-1', + knowledgeSpaceId: 'space-1', + rowVersion: 1, + sourceId: 'source-1', + status: 'ready', + title: 'sso-enterprise.pdf', + updatedAt: '2026-07-20T10:01:00Z', + userMetadata: { sourceName: 'Notion support SOP' }, + ...overrides, +}) + +const task = (overrides: Partial = {}): DocumentProcessingTask => ({ + createdAt: '2026-07-20T10:00:00Z', + documentId: 'document-1', + documentRevision: 2, + id: 'task-1', + knowledgeSpaceId: 'space-1', + progressPercent: 45, + stage: 'parsed', + state: 'running', + updatedAt: '2026-07-20T10:01:00Z', + ...overrides, +}) + +function streamFailedTaskThenWait(taskId: string) { + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 80, + stage: 'parsed' as const, + state: 'failed' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: `${taskId}:2026-07-20T10:03:00Z`, + } + yield { + data: { errorCode: 'PARSER_FAILED', state: 'failed' as const }, + event: 'terminal' as const, + id: `${taskId}:terminal`, + } + return + } + await new Promise(() => {}) + }) +} + +function notifyTaskQuerySuccess() { + for (const listener of queryCacheListeners) + listener({ + action: { type: 'success' }, + query: { queryHash: hashKey(['knowledge-fs', 'tasks']) }, + type: 'updated', + }) +} + +const source = (overrides: Partial = {}): Source => ({ + createdAt: '2026-07-20T10:00:00Z', + id: 'source-1', + knowledgeSpaceId: 'space-1', + metadata: {}, + name: 'Notion support SOP', + status: 'active', + type: 'connector', + updatedAt: '2026-07-20T10:00:00Z', + uri: 'notion://support', + ...overrides, +}) + +describe('DocumentsPage', () => { + beforeEach(() => { + vi.clearAllMocks() + queryCacheListeners.clear() + queryClient.cancelQueries.mockResolvedValue(undefined) + queryClient.invalidateQueries.mockResolvedValue(undefined) + documentsQuery.data = { pages: [{ items: [] }] } + documentsQuery.error = null + documentsQuery.hasNextPage = false + documentsQuery.isFetchNextPageError = false + documentsQuery.isFetching = false + documentsQuery.isFetchingNextPage = false + documentsQuery.isPending = false + documentsQuery.isRefetching = false + documentsQuery.refetch.mockResolvedValue({ error: null }) + tasksQuery.data = { pages: [{ items: [] }] } + tasksQuery.dataUpdatedAt = 0 + tasksQuery.dataUpdateCount = 0 + tasksQuery.error = null + tasksQuery.hasNextPage = false + tasksQuery.isFetchNextPageError = false + tasksQuery.isFetching = false + tasksQuery.isFetchingNextPage = false + tasksQuery.isPending = false + tasksQuery.refetch.mockResolvedValue({ error: null }) + sourcesQuery.data = { pages: [{ items: [source()] }] } + sourcesQuery.error = null + sourcesQuery.hasNextPage = false + sourcesQuery.isFetchNextPageError = false + sourcesQuery.isFetching = false + sourcesQuery.isFetchingNextPage = false + sourcesQuery.isPending = false + sourcesQuery.refetch.mockResolvedValue({ error: null }) + permissionStateMock.datasetKeys = ['dataset.acl.edit'] + permissionStateMock.error = null + permissionStateMock.fetching = false + permissionStateMock.loading = false + permissionStateMock.retry.mockResolvedValue({ + data: { + dataset: { default_permission_keys: ['dataset.acl.edit'] }, + }, + error: null, + }) + permissionStateMock.refreshAfterDenial.mockResolvedValue({ + data: { + dataset: { default_permission_keys: ['dataset.acl.edit'] }, + }, + error: null, + }) + getTaskSnapshot.mockResolvedValue(task({ state: 'succeeded' })) + cancelMutation.mutateAsync.mockResolvedValue(task({ state: 'canceled' })) + retryMutation.mutateAsync.mockResolvedValue(task({ state: 'queued' })) + reindexMutation.mutateAsync.mockResolvedValue({ + bulkJobId: 'reindex-1', + items: [ + { + asset: { + createdAt: '2026-07-20T10:00:00Z', + filename: 'one.pdf', + id: 'asset-1', + knowledgeSpaceId: 'space-1', + mimeType: 'application/pdf', + objectKey: 'documents/one.pdf', + parserStatus: 'pending', + sha256: 'sha', + sizeBytes: 1024, + version: 1, + }, + compilationJob: { id: 'job-1', stage: 'queued' }, + status: 'queued', + statusUrl: '/tasks/job-1', + }, + ], + total: 1, + }) + uploadMutation.mutateAsync.mockResolvedValue({ + asset: { + createdAt: '2026-07-20T10:00:00Z', + filename: 'one.md', + id: 'asset-1', + knowledgeSpaceId: 'space-1', + mimeType: 'text/markdown', + objectKey: 'documents/one.md', + parserStatus: 'pending', + sha256: 'sha', + sizeBytes: 3, + version: 1, + }, + compilationJob: { id: 'job-1', stage: 'queued' }, + documentRevision: 1, + logicalDocument: { id: 'document-1', revision: 1 }, + logicalDocumentId: 'document-1', + statusUrl: '/tasks/job-1', + }) + bulkUploadMutation.mutateAsync.mockResolvedValue({ + accepted: 2, + bulkJobId: 'upload-1', + excluded: 0, + items: [], + total: 2, + }) + streamProcessingTaskEvents.mockImplementation(async function* () {}) + }) + + it('loads real logical documents and processing tasks through generated contracts', () => { + documentsQuery.isPending = true + tasksQuery.isPending = true + + render() + + const documentOptions = documentsInfiniteOptions.mock.lastCall?.[0] + const taskOptions = tasksInfiniteOptions.mock.lastCall?.[0] + const sourceOptions = sourcesInfiniteOptions.mock.lastCall?.[0] + expect(documentOptions?.input(null)).toEqual({ + params: { id: 'space-1' }, + query: { limit: 50 }, + }) + expect(documentOptions?.input('next')).toEqual({ + params: { id: 'space-1' }, + query: { cursor: 'next', limit: 50 }, + }) + expect(documentOptions?.getNextPageParam({ nextCursor: 'next' })).toBe('next') + expect(taskOptions?.input(null)).toEqual({ + params: { id: 'space-1' }, + query: { limit: 100 }, + }) + expect(taskOptions?.getNextPageParam({ nextCursor: 'next' })).toBe('next') + expect(sourceOptions?.input(null)).toEqual({ + params: { id: 'space-1' }, + query: { limit: 100 }, + }) + expect(sourceOptions?.getNextPageParam({ nextCursor: 'next' })).toBe('next') + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + }) + + it('restores document search and status filters from the URL', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [ + { + items: [ + document({ id: 'ready-document', title: 'Ready handbook.pdf' }), + document({ id: 'failed-document', title: 'Failed report.pdf' }), + ], + }, + ], + } + tasksQuery.data = { + pages: [ + { + items: [task({ documentId: 'failed-document', id: 'failed-task', state: 'failed' })], + }, + ], + } + + render(, { + searchParams: '?query=report&status=failed', + }) + + const metadata = screen.getByRole('button', { name: 'dataset.newKnowledge.metadata' }) + expect(metadata).toBeEnabled() + await user.click(metadata) + expect(toastMock.info).toHaveBeenCalledWith('dataset.newKnowledge.filtersUnavailable') + const rowActions = screen.getByRole('button', { + name: /dataset\.newKnowledge\.documentActions/, + }) + expect(rowActions).toBeEnabled() + await user.click(rowActions) + await user.click( + await screen.findByRole('menuitem', { name: 'dataset.newKnowledge.downloadDocuments' }), + ) + expect(toastMock.info).toHaveBeenCalledWith('dataset.newKnowledge.documentActionsUnavailable') + + expect(screen.getByRole('searchbox')).toHaveValue('report') + expect(screen.getByRole('combobox')).toHaveValue('failed') + expect(screen.getByText('Failed report.pdf')).toBeInTheDocument() + expect(screen.queryByText('Ready handbook.pdf')).not.toBeInTheDocument() + }) + + it('renders revisions, sources, stable cursor-ordered rows, and every designed status', () => { + documentsQuery.data = { + pages: [ + { + items: [document({ id: 'ready', title: 'Ready.pdf', userMetadata: {} })], + }, + { + items: [ + document({ + active: null, + activeRevision: undefined, + id: 'queued', + status: 'pending', + title: 'Queued.docx', + updatedAt: '2026-07-20T10:05:00Z', + }), + document({ id: 'failed', status: 'failed', title: 'Failed.html' }), + document({ + id: 'disabled', + sourceId: 'disabled-source', + title: 'Disabled.xlsx', + userMetadata: { sourceName: 'Archive' }, + }), + document({ id: 'processing', status: 'pending', title: 'Processing.md' }), + ], + }, + ], + } + tasksQuery.data = { + pages: [ + { + items: [ + task({ documentId: 'processing', id: 'processing-task' }), + task({ documentId: 'queued', id: 'queued-task', state: 'queued' }), + ], + }, + ], + } + sourcesQuery.data = { + pages: [ + { + items: [source(), source({ id: 'disabled-source', name: 'Archive', status: 'disabled' })], + }, + ], + } + + render() + + const rows = screen.getAllByRole('row').slice(1) + expect(within(rows[0]!).getByText('Ready.pdf')).toBeInTheDocument() + expect(screen.getAllByText('Notion support SOP').length).toBeGreaterThan(0) + expect(screen.getAllByText('v2').length).toBeGreaterThan(0) + for (const status of ['ready', 'queued', 'processing', 'failed', 'disabled']) + expect( + screen.getAllByText(`dataset.newKnowledge.documentStatus.${status}`).length, + ).toBeGreaterThan(0) + expect(screen.getByText('dataset.newKnowledge.lastReadyRevisionHint')).toBeInTheDocument() + }) + + it('treats a document with an unresolved source as unavailable for writes', () => { + documentsQuery.data = { + pages: [{ items: [document({ sourceId: 'missing-source', title: 'Orphaned.pdf' })] }], + } + sourcesQuery.data = { pages: [{ items: [] }] } + + render() + + expect(screen.getAllByText('dataset.newKnowledge.documentStatus.disabled')).toHaveLength(2) + expect(screen.getByRole('checkbox', { name: 'Orphaned.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + }) + + it('renders the designed empty state with an available upload action', () => { + render() + + const emptyState = screen.getByText('dataset.newKnowledge.documentsEmptyTitle').parentElement + expect(emptyState).not.toBeNull() + expect(screen.getByText('dataset.newKnowledge.documentsEmptyDescription')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.documentsDropHint')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' })).toBeEnabled() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.tasks' }), + ).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.metadata' }), + ).not.toBeInTheDocument() + const dataTransfer = { dropEffect: 'none' } + const dragOver = new Event('dragover', { bubbles: true, cancelable: true }) + Object.defineProperty(dragOver, 'dataTransfer', { value: dataTransfer }) + expect(fireEvent(emptyState!, dragOver)).toBe(false) + expect(dataTransfer.dropEffect).toBe('copy') + }) + + it('removes the empty-state drop affordance when uploads are unavailable', () => { + permissionStateMock.datasetKeys = ['dataset.acl.readonly'] + + render() + + const emptyState = screen.getByText('dataset.newKnowledge.documentsEmptyTitle').parentElement + expect(emptyState).not.toBeNull() + expect(screen.queryByText('dataset.newKnowledge.documentsDropHint')).not.toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' }), + ).toHaveAttribute('aria-describedby', 'documents-readonly-reason') + const dataTransfer = { + dropEffect: 'copy', + files: [new File(['one'], 'one.md', { type: 'text/markdown' })], + } + const dragOver = new Event('dragover', { bubbles: true, cancelable: true }) + Object.defineProperty(dragOver, 'dataTransfer', { value: dataTransfer }) + expect(fireEvent(emptyState!, dragOver)).toBe(false) + expect(dataTransfer.dropEffect).toBe('none') + expect( + fireEvent.drop(emptyState!, { + dataTransfer, + }), + ).toBe(false) + expect(uploadMutation.mutateAsync).not.toHaveBeenCalled() + }) + + it('keeps processing tasks reachable while the document list is empty', async () => { + const user = userEvent.setup() + tasksQuery.data = { pages: [{ items: [task({ id: 'orphaned-running-task' })] }] } + + render() + + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + }) + + it('keeps every write action unavailable for read-only users', async () => { + const user = userEvent.setup() + permissionStateMock.datasetKeys = ['dataset.acl.readonly'] + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + + render() + + expect(screen.getByText('dataset.newKnowledge.documentPermissionRestricted')).toBeVisible() + expect(screen.queryByLabelText('dataset.newKnowledge.uploadDocuments')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' })).toBeDisabled() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-describedby', + 'documents-readonly-reason', + ) + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + expect( + within(screen.getByRole('dialog')).getByText( + 'dataset.newKnowledge.documentPermissionRestricted', + ), + ).toBeInTheDocument() + }) + + it('keeps permission lookup failures distinct from read-only access and retries them', async () => { + const user = userEvent.setup() + permissionStateMock.error = new Error('permission service unavailable') + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + + render() + + expect(screen.getByRole('alert')).toHaveTextContent('dataset.newKnowledge.permissionLoadFailed') + expect( + screen.queryByText('dataset.newKnowledge.documentPermissionRestricted'), + ).not.toBeInTheDocument() + const addDocument = screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' }) + expect(addDocument).toBeDisabled() + expect(addDocument).toHaveAccessibleDescription('dataset.newKnowledge.permissionLoadFailed') + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(within(screen.getByRole('dialog')).getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.permissionLoadFailed', + ) + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.permissionLoadFailed', + }), + ) + expect(permissionStateMock.retry).toHaveBeenCalledOnce() + }) + + it('explains pending permission checks inside the task drawer', async () => { + const user = userEvent.setup() + permissionStateMock.loading = true + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + + render() + const pendingPermission = screen.getByText('dataset.newKnowledge.permission · common.loading') + expect(pendingPermission).toHaveTextContent('dataset.newKnowledge.permission · common.loading') + expect(pendingPermission).toHaveAttribute('role', 'status') + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' }), + ).toHaveAttribute('aria-describedby', 'documents-permission-pending') + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-describedby', + 'documents-permission-pending', + ) + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + + expect( + within(screen.getByRole('dialog')).getByText( + 'dataset.newKnowledge.permission · common.loading', + ), + ).toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + }) + + it('uploads one or multiple files through the pinned document contracts', async () => { + const user = userEvent.setup() + render() + const input = screen.getByLabelText('dataset.newKnowledge.uploadDocuments') + expect(input).toHaveAttribute('hidden') + expect(input).toHaveAttribute('tabindex', '-1') + expect(input).toHaveAttribute( + 'accept', + '.csv,.doc,.docx,.epub,.htm,.html,.json,.jsonl,.md,.pdf,.ppt,.pptx,.rtf,.text,.txt,.xls,.xlsx', + ) + + await user.upload(input, new File(['one'], 'one.md', { type: 'text/markdown' })) + expect(uploadMutation.mutateAsync).toHaveBeenCalledWith({ + body: { file: expect.any(File) }, + params: { id: 'space-1' }, + }) + + await user.upload(input, [ + new File(['two'], 'two.md', { type: 'text/markdown' }), + new File(['three'], 'three.txt', { type: 'text/plain' }), + ]) + expect(bulkUploadMutation.mutateAsync).toHaveBeenCalledWith({ + body: { files: [expect.any(File), expect.any(File)] }, + params: { id: 'space-1' }, + }) + expect(queryClient.invalidateQueries).toHaveBeenCalled() + const documentInvalidation = queryClient.invalidateQueries.mock.calls.find( + ([options]) => options.queryKey[1] === 'documents', + )?.[0] + expect( + documentInvalidation?.predicate({ + queryKey: [ + ['console', 'knowledgeFs', 'getKnowledgeSpacesByIdLogicalDocuments'], + { input: { params: { id: 'space-1' } }, type: 'infinite' }, + ], + }), + ).toBe(true) + expect( + documentInvalidation?.predicate({ + queryKey: [ + ['console', 'knowledgeFs', 'getKnowledgeSpacesByIdLogicalDocuments'], + { input: { params: { id: 'space-2' } }, type: 'infinite' }, + ], + }), + ).toBe(false) + }) + + it('excludes unsupported files locally while uploading valid files', async () => { + render() + const validFile = new File(['one'], 'one.md', { type: 'text/markdown' }) + const unsupportedFile = new File(['two'], 'two.exe', { + type: 'application/octet-stream', + }) + + fireEvent.change(screen.getByLabelText('dataset.newKnowledge.uploadDocuments'), { + target: { files: [validFile, unsupportedFile] }, + }) + + await waitFor(() => + expect(uploadMutation.mutateAsync).toHaveBeenCalledWith({ + body: { file: validFile }, + params: { id: 'space-1' }, + }), + ) + expect(bulkUploadMutation.mutateAsync).not.toHaveBeenCalled() + expect(toastMock.warning).toHaveBeenCalledWith( + 'dataset.newKnowledge.documentUploadPartial:{"accepted":1,"details":"two.exe (dataset.newKnowledge.documentUploadExclusion.fileType)","excluded":1}', + ) + }) + + it('rejects oversized files before invoking an upload contract', async () => { + render() + const oversizedFile = new File(['one'], 'oversized.pdf', { type: 'application/pdf' }) + Object.defineProperty(oversizedFile, 'size', { value: 16 * 1024 * 1024 }) + + fireEvent.change(screen.getByLabelText('dataset.newKnowledge.uploadDocuments'), { + target: { files: [oversizedFile] }, + }) + + expect(uploadMutation.mutateAsync).not.toHaveBeenCalled() + expect(bulkUploadMutation.mutateAsync).not.toHaveBeenCalled() + expect(toastMock.error).toHaveBeenCalledWith( + 'dataset.newKnowledge.documentUploadRejected:{"details":"oversized.pdf (dataset.newKnowledge.documentUploadExclusion.fileSize)"}', + ) + }) + + it('reports partial and fully excluded bulk uploads from the contract result', async () => { + const user = userEvent.setup() + bulkUploadMutation.mutateAsync + .mockResolvedValueOnce({ + accepted: 1, + bulkJobId: 'upload-partial', + excluded: 1, + items: [ + { + filename: 'too-large.pdf', + index: 1, + mimeType: 'application/pdf', + reason: 'file_too_large', + sizeBytes: 10_000, + status: 'excluded', + }, + ], + total: 2, + }) + .mockResolvedValueOnce({ + accepted: 0, + bulkJobId: 'upload-rejected', + excluded: 2, + items: [ + { + filename: 'one.md', + index: 0, + mimeType: 'text/markdown', + reason: 'quota_exceeded', + sizeBytes: 3, + status: 'excluded', + }, + { + filename: 'two.md', + index: 1, + mimeType: 'text/markdown', + reason: 'quota_exceeded', + sizeBytes: 3, + status: 'excluded', + }, + ], + total: 2, + }) + render() + const input = screen.getByLabelText('dataset.newKnowledge.uploadDocuments') + + await user.upload(input, [ + new File(['one'], 'one.md', { type: 'text/markdown' }), + new File(['large'], 'too-large.pdf', { type: 'application/pdf' }), + ]) + expect(toastMock.warning).toHaveBeenCalledWith( + 'dataset.newKnowledge.documentUploadPartial:{"accepted":1,"details":"too-large.pdf (dataset.newKnowledge.documentUploadExclusion.fileSize)","excluded":1}', + ) + + queryClient.invalidateQueries.mockClear() + await user.upload(input, [ + new File(['one'], 'one.md', { type: 'text/markdown' }), + new File(['two'], 'two.md', { type: 'text/markdown' }), + ]) + expect(toastMock.error).toHaveBeenCalledWith( + 'dataset.newKnowledge.documentUploadRejected:{"details":"one.md (dataset.newKnowledge.documentUploadExclusion.quota); two.md (dataset.newKnowledge.documentUploadExclusion.quota)"}', + ) + expect(queryClient.invalidateQueries).not.toHaveBeenCalled() + }) + + it('distinguishes a permission failure and exposes retry for other load errors', async () => { + const user = userEvent.setup() + documentsQuery.data = undefined + documentsQuery.error = { status: 403 } + + const { rerender } = render() + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsPermissionDescription', + ) + expect(screen.queryByRole('button', { name: 'common.operation.retry' })).not.toBeInTheDocument() + + documentsQuery.error = new Error('temporary') + rerender() + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ) + expect(documentsQuery.refetch).toHaveBeenCalledOnce() + }) + + it('reports a failed background document refresh while preserving cached rows', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const rendered = render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + + documentsQuery.error = new Error('background refresh failed') + rendered.rerender() + + expect(screen.getByText('sso-enterprise.pdf')).toBeInTheDocument() + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsErrorDescription', + ) + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + const actions = screen.getByRole('group', { + name: 'dataset.newKnowledge.bulkDocumentActions', + }) + expect( + within(actions).getByRole('button', { + name: 'dataset.newKnowledge.reindexDocuments', + }), + ).toHaveAttribute('aria-describedby', 'document-reindex-unavailable') + expect( + within(actions).getByText( + 'dataset.newKnowledge.reindexDocuments · dataset.newKnowledge.documentsErrorDescription', + ), + ).toBeVisible() + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ) + expect(documentsQuery.refetch).toHaveBeenCalledOnce() + }) + + it('blocks cached document content when a background refresh loses permission', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.error = { status: 403 } + + render() + + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsPermissionDescription', + ) + expect(screen.queryByText('sso-enterprise.pdf')).not.toBeInTheDocument() + expect(screen.queryByLabelText('dataset.newKnowledge.uploadDocuments')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.tasks' }), + ).not.toBeInTheDocument() + }) + + it('keeps cached refresh retries busy while their queries are fetching', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.error = new Error('document refresh failed') + documentsQuery.isRefetching = true + tasksQuery.error = new Error('task refresh failed') + tasksQuery.isFetching = true + + render() + + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ).toHaveAttribute('aria-disabled', 'true') + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ).toHaveAttribute('aria-disabled', 'true') + }) + + it('keeps permission retries busy while permission keys are refetching', () => { + permissionStateMock.error = new Error('permission refresh failed') + permissionStateMock.fetching = true + + render() + + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.permissionLoadFailed', + }), + ).toHaveAttribute('aria-disabled', 'true') + }) + + it('does not disable a failed dependency retry for an unrelated background refresh', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.error = new Error('task refresh failed') + sourcesQuery.isFetching = true + + render() + + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ).not.toHaveAttribute('aria-disabled') + }) + + it('keeps a blocking dependency retry stable while its first page refetches', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = undefined + tasksQuery.error = new Error('task first page failed') + sourcesQuery.error = new Error('source background refresh failed') + const { rerender } = render() + const retry = screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }) + + await user.click(retry) + expect(tasksQuery.refetch).toHaveBeenCalledOnce() + tasksQuery.error = null + tasksQuery.isFetching = true + tasksQuery.isPending = true + rerender() + + expect(retry).toBeInTheDocument() + expect(retry).toHaveFocus() + expect(retry).toHaveAttribute('aria-disabled', 'true') + + tasksQuery.data = { pages: [{ items: [] }] } + tasksQuery.isFetching = false + tasksQuery.isPending = false + rerender() + expect(screen.getByText('sso-enterprise.pdf')).toBeInTheDocument() + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.sourcesErrorDescription', + }), + ).toHaveFocus(), + ) + + sourcesQuery.error = null + rerender() + await waitFor(() => + expect(screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' })).toHaveFocus(), + ) + }) + + it('closes a task drawer permanently and restores focus when document permission is revoked', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.getByRole('dialog')).toBeInTheDocument() + + documentsQuery.error = { status: 403 } + rerender() + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + const permissionAlert = screen.getByRole('alert') + expect(permissionAlert).toHaveFocus() + expect(tasksInfiniteOptions.mock.lastCall?.[0].enabled).toBe(false) + expect(sourcesInfiniteOptions.mock.lastCall?.[0].enabled).toBe(false) + expect(queryClient.cancelQueries).toHaveBeenCalledWith({ + predicate: expect.any(Function), + queryKey: ['knowledge-fs', 'tasks'], + }) + expect(queryClient.cancelQueries).toHaveBeenCalledWith({ + predicate: expect.any(Function), + queryKey: ['knowledge-fs', 'sources'], + }) + const taskCancellation = queryClient.cancelQueries.mock.calls.find( + ([options]) => options.queryKey[1] === 'tasks', + )?.[0] + expect( + taskCancellation?.predicate({ + queryKey: [ + ['console', 'knowledgeFs', 'getKnowledgeSpacesByIdProcessingTasks'], + { input: { params: { id: 'space-1' } }, type: 'infinite' }, + ], + }), + ).toBe(true) + expect( + taskCancellation?.predicate({ + queryKey: [ + ['console', 'knowledgeFs', 'getKnowledgeSpacesByIdProcessingTasks'], + { input: { params: { id: 'space-2' } }, type: 'infinite' }, + ], + }), + ).toBe(false) + + documentsQuery.error = null + tasksQuery.refetch.mockClear() + sourcesQuery.refetch.mockClear() + rerender() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(tasksQuery.refetch).toHaveBeenCalledWith({ cancelRefetch: false }) + expect(sourcesQuery.refetch).toHaveBeenCalledWith({ cancelRefetch: false }) + }) + + it('does not repeat the child query that already recovered from permission denial', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [] }] } + tasksQuery.error = { status: 403 } + const rendered = render() + + tasksQuery.error = null + tasksQuery.refetch.mockClear() + sourcesQuery.refetch.mockClear() + rendered.rerender() + + expect(tasksQuery.refetch).not.toHaveBeenCalled() + expect(sourcesQuery.refetch).toHaveBeenCalledWith({ cancelRefetch: false }) + }) + + it('moves focus from document controls to the permission alert after dynamic revocation', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const { rerender } = render() + const searchbox = screen.getByRole('searchbox', { + name: 'dataset.newKnowledge.searchDocuments', + }) + await user.click(searchbox) + expect(searchbox).toHaveFocus() + + documentsQuery.error = { status: 403 } + rerender() + + await waitFor(() => expect(screen.getByRole('alert')).toHaveFocus()) + + documentsQuery.error = null + rerender() + await waitFor(() => + expect(screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' })).toHaveFocus(), + ) + }) + + it('moves focus from bulk actions to the permission alert after dynamic revocation', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const { rerender } = render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + const reindex = within( + screen.getByRole('group', { name: 'dataset.newKnowledge.bulkDocumentActions' }), + ).getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' }) + act(() => reindex.focus()) + + documentsQuery.error = { status: 403 } + rerender() + + await waitFor(() => expect(screen.getByRole('alert')).toHaveFocus()) + }) + + it('stops background pagination and failed-task polling while document permission is denied', async () => { + vi.useFakeTimers() + documentsQuery.data = { + pages: [{ items: [document({ sourceId: 'unresolved-source' })], nextCursor: 'next' }], + } + documentsQuery.error = { status: 403 } + documentsQuery.hasNextPage = true + tasksQuery.data = { + pages: [{ items: [task({ state: 'failed' })], nextCursor: 'task-next' }], + } + tasksQuery.hasNextPage = true + sourcesQuery.data = { pages: [{ items: [], nextCursor: 'source-next' }] } + sourcesQuery.hasNextPage = true + + const rendered = render(, { + searchParams: '?query=sso', + }) + try { + await act(async () => vi.advanceTimersByTime(10_000)) + + expect(documentsQuery.fetchNextPage).not.toHaveBeenCalled() + expect(tasksQuery.fetchNextPage).not.toHaveBeenCalled() + expect(sourcesQuery.fetchNextPage).not.toHaveBeenCalled() + expect(getTaskSnapshot).not.toHaveBeenCalled() + expect(streamProcessingTaskEvents).not.toHaveBeenCalled() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('filters and searches documents while continuing through cursor pages', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document({ title: 'Product handbook.pdf' })], nextCursor: 'next' }], + } + documentsQuery.hasNextPage = true + + render() + await user.type( + screen.getByRole('searchbox', { name: 'dataset.newKnowledge.searchDocuments' }), + 'later page', + ) + + expect(documentsQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(screen.queryByText('dataset.newKnowledge.noMatchingDocuments')).not.toBeInTheDocument() + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.partialDocumentResults')).toBeInTheDocument() + }) + + it('blocks selection until filtered cursor pages are complete', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document({ title: 'Product handbook.pdf' })], nextCursor: 'next' }], + } + documentsQuery.hasNextPage = true + + render() + await user.type( + screen.getByRole('searchbox', { name: 'dataset.newKnowledge.searchDocuments' }), + 'product', + ) + + expect(screen.getByRole('checkbox', { name: 'Product handbook.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect( + screen.getByRole('checkbox', { name: 'dataset.newKnowledge.selectAllDocuments' }), + ).toHaveAttribute('aria-disabled', 'true') + }) + + it('surfaces dependent source pagination failures and retries the failed page', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + sourcesQuery.isFetchNextPageError = true + + render() + + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.sourcesErrorDescription', + ) + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.sourcesErrorDescription', + }), + ) + expect(sourcesQuery.fetchNextPage).toHaveBeenCalledOnce() + const documentRow = screen.getByRole('row', { name: /sso-enterprise\.pdf/ }) + expect( + within(documentRow).getByText('dataset.newKnowledge.documentStatus.ready'), + ).toBeInTheDocument() + expect(screen.getByRole('combobox')).toBeEnabled() + }) + + it('retries a failed task cursor page from the task drawer', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.isFetchNextPageError = true + + render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + await user.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ) + + expect(tasksQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(tasksQuery.refetch).not.toHaveBeenCalled() + }) + + it('returns focus to the drawer close button after a query retry succeeds', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.error = new Error('task refresh failed') + const { rerender } = render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + const panel = screen.getByRole('dialog') + await user.click( + within(panel).getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ) + + tasksQuery.error = null + rerender() + + await waitFor(() => + expect(within(panel).getByRole('button', { name: 'common.operation.close' })).toHaveFocus(), + ) + }) + + it('moves focus between drawer query retries as each error recovers', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document()], nextCursor: 'document-next' }], + } + documentsQuery.hasNextPage = true + documentsQuery.isFetchNextPageError = true + tasksQuery.data = { + pages: [{ items: [task({ documentId: 'missing-document' })] }], + } + tasksQuery.error = new Error('task refresh failed') + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + const panel = screen.getByRole('dialog') + await user.click( + within(panel).getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ) + + tasksQuery.error = null + rerender() + const documentRetry = within(panel).getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }) + await waitFor(() => expect(documentRetry).toHaveFocus()) + await user.click(documentRetry) + + documentsQuery.isFetchNextPageError = false + rerender() + await waitFor(() => + expect(within(panel).getByRole('button', { name: 'common.operation.close' })).toHaveFocus(), + ) + }) + + it('continues remote task pagination from an empty cached drawer after the automatic cap', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: Array.from({ length: 20 }, () => ({ + items: [], + nextCursor: 'next', + })), + } + tasksQuery.hasNextPage = true + + render() + const taskTrigger = screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":0} · dataset.newKnowledge.taskHistoryIncomplete', + }) + expect(taskTrigger).toHaveTextContent('0+') + await user.click(taskTrigger) + expect( + within(screen.getByRole('dialog')).queryByText('dataset.newKnowledge.noBackgroundTasks'), + ).not.toBeInTheDocument() + await user.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'dataset.newKnowledge.loadMore', + }), + ) + + expect(tasksQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('loads document titles for tasks whose documents are on a later cursor page', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: Array.from({ length: 20 }, (_, index) => ({ + items: index === 0 ? [document()] : [], + nextCursor: 'next', + })), + } + documentsQuery.hasNextPage = true + tasksQuery.data = { + pages: [ + { + items: [task({ documentId: 'later-document', id: 'later-task', state: 'succeeded' })], + }, + ], + } + + render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + + const panel = screen.getByRole('dialog') + expect(within(panel).queryByText('later-document')).not.toBeInTheDocument() + expect( + within(panel).getByText( + 'dataset.newKnowledge.processDocument:{"name":"dataset.newKnowledge.documentColumn"}', + ), + ).toBeInTheDocument() + await user.click(within(panel).getByRole('button', { name: 'dataset.newKnowledge.loadMore' })) + expect(documentsQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('defers task title pagination until the task drawer is opened', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document()], nextCursor: 'next' }], + } + documentsQuery.hasNextPage = true + tasksQuery.data = { + pages: [ + { + items: [task({ documentId: 'later-document', id: 'later-task', state: 'succeeded' })], + }, + ], + } + + render() + expect(documentsQuery.fetchNextPage).not.toHaveBeenCalled() + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + await waitFor(() => expect(documentsQuery.fetchNextPage).toHaveBeenCalledOnce()) + }) + + it('reports and retries task title pagination failures inside the drawer', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document()], nextCursor: 'next' }], + } + documentsQuery.hasNextPage = true + documentsQuery.isFetchNextPageError = true + tasksQuery.data = { + pages: [ + { + items: [task({ documentId: 'later-document', id: 'later-task', state: 'succeeded' })], + }, + ], + } + + render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + + const panel = screen.getByRole('dialog') + expect(within(panel).getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsErrorDescription', + ) + await user.click( + within(panel).getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ) + expect(documentsQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('keeps an unresolved source pending when a background source refresh fails', () => { + documentsQuery.data = { + pages: [{ items: [document({ sourceId: 'unresolved-source' })] }], + } + sourcesQuery.error = new Error('source refresh failed') + + render() + + const documentRow = screen.getByRole('row', { name: /sso-enterprise\.pdf/ }) + expect( + within(documentRow).queryByText('dataset.newKnowledge.documentStatus.disabled'), + ).not.toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + }) + + it('keeps loaded task actions available when background task pagination fails', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + tasksQuery.error = new Error('poll failed') + tasksQuery.isFetchingNextPage = true + + render() + + expect(screen.getByText('sso-enterprise.pdf')).toBeInTheDocument() + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.tasksErrorDescription', + ) + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ) + expect(tasksQuery.refetch).toHaveBeenCalledOnce() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + }) + + it('reports and retries cached task refresh failures from the document empty state', async () => { + const user = userEvent.setup() + tasksQuery.error = new Error('task refresh failed') + + render() + + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.tasksErrorDescription', + ) + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.tasksErrorDescription', + }), + ) + expect(tasksQuery.refetch).toHaveBeenCalledOnce() + }) + + it('renders document identity while dependency cursor pages continue loading', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [], nextCursor: 'task-next' }] } + tasksQuery.hasNextPage = true + sourcesQuery.data = { pages: [{ items: [], nextCursor: 'source-next' }] } + sourcesQuery.hasNextPage = true + + render() + + expect(screen.getByText('sso-enterprise.pdf')).toBeInTheDocument() + expect(screen.getByRole('combobox')).toBeDisabled() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(tasksQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(sourcesQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('keeps document status and selection pending until the first task page arrives', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = undefined + tasksQuery.isPending = true + + render() + + const documentRow = screen.getByRole('row', { name: /sso-enterprise\.pdf/ }) + expect( + within(documentRow).queryByText('dataset.newKnowledge.documentStatus.ready'), + ).not.toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(screen.getByRole('region', { name: 'dataset.newKnowledge.documents' })).toHaveAttribute( + 'aria-busy', + 'true', + ) + }) + + it('keeps document state pending while the next task cursor page is loading', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [], nextCursor: 'next' }] } + tasksQuery.hasNextPage = true + tasksQuery.isFetchingNextPage = true + + render() + + const documentRow = screen.getByRole('row', { name: /sso-enterprise\.pdf/ }) + expect( + within(documentRow).queryByText('dataset.newKnowledge.documentStatus.ready'), + ).not.toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + }) + + it('keeps selection disabled after a filtered document page fails', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document({ title: 'Product handbook.pdf' })], nextCursor: 'next' }], + } + documentsQuery.hasNextPage = true + documentsQuery.isFetchNextPageError = true + + render() + await user.type( + screen.getByRole('searchbox', { name: 'dataset.newKnowledge.searchDocuments' }), + 'product', + ) + + expect(screen.getByRole('checkbox', { name: 'Product handbook.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect( + screen.getByRole('checkbox', { name: 'dataset.newKnowledge.selectAllDocuments' }), + ).toHaveAttribute('aria-disabled', 'true') + await user.click( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ) + expect(documentsQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('keeps a failed document page retry busy while the next page is fetching', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document()], nextCursor: 'next' }], + } + documentsQuery.hasNextPage = true + documentsQuery.isFetchNextPageError = true + documentsQuery.isFetchingNextPage = true + + render() + await user.type( + screen.getByRole('searchbox', { name: 'dataset.newKnowledge.searchDocuments' }), + 'sso', + ) + + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ).toHaveAttribute('aria-disabled', 'true') + }) + + it('does not disable a document page retry for unrelated dependency pagination', () => { + documentsQuery.data = { pages: [{ items: [document()], nextCursor: 'next' }] } + documentsQuery.hasNextPage = true + documentsQuery.isFetchNextPageError = true + tasksQuery.data = { + pages: Array.from({ length: 20 }, () => ({ items: [], nextCursor: 'task-next' })), + } + tasksQuery.hasNextPage = true + tasksQuery.isFetchingNextPage = true + + render() + + expect( + screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }), + ).not.toHaveAttribute('aria-disabled') + }) + + it('moves pagination focus from load more to retry and then to final results', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()], nextCursor: 'next' }] } + documentsQuery.hasNextPage = true + const { rerender } = render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.loadMore' })) + + documentsQuery.isFetchNextPageError = true + rerender() + const retry = screen.getByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsErrorDescription', + }) + await waitFor(() => expect(retry).toHaveFocus()) + await user.click(retry) + + documentsQuery.data = { + pages: [ + { items: [document()] }, + { items: [document({ id: 'last-document', title: 'Last.pdf' })] }, + ], + } + documentsQuery.hasNextPage = false + documentsQuery.isFetchNextPageError = false + rerender() + + await waitFor(() => expect(screen.getByRole('table').parentElement).toHaveFocus()) + }) + + it('re-indexes selected documents and disables backend-dependent actions', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [{ items: [document({ id: 'one', title: 'One.pdf' }), document({ id: 'two' })] }], + } + + render() + await user.click(screen.getByRole('checkbox', { name: 'One.pdf' })) + + const actions = screen.getByRole('group', { + name: 'dataset.newKnowledge.bulkDocumentActions', + }) + expect( + screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' }).closest('section'), + ).toHaveClass('pb-[calc(7rem+env(safe-area-inset-bottom,0px))]') + expect( + within(actions).getByText('dataset.newKnowledge.documentsSelected:{"count":1}'), + ).toBeInTheDocument() + const reindex = within(actions).getByRole('button', { + name: 'dataset.newKnowledge.reindexDocuments', + }) + expect(reindex).toBeEnabled() + const orderedActions = within(actions).getAllByRole('button') + expect(orderedActions[0]).toHaveAccessibleName('dataset.newKnowledge.reindexDocuments') + expect(orderedActions[1]).toHaveAccessibleName('dataset.newKnowledge.clearDocumentSelection') + expect(actions.firstElementChild).toBe(orderedActions[0]) + expect(orderedActions[1]!.nextElementSibling).toHaveTextContent( + 'dataset.newKnowledge.documentsSelected:{"count":1}', + ) + expect( + within(actions).getByText( + 'dataset.newKnowledge.downloadDocuments / dataset.newKnowledge.deleteDocuments · dataset.cornerLabel.unavailable', + ), + ).toBeVisible() + const download = within(actions).getByRole('button', { + name: 'dataset.newKnowledge.downloadDocuments', + }) + const remove = within(actions).getByRole('button', { + name: 'dataset.newKnowledge.deleteDocuments', + }) + expect(download).toBeDisabled() + expect(remove).toBeDisabled() + expect(toastMock.info).not.toHaveBeenCalled() + await user.dblClick(reindex) + expect(reindexMutation.mutateAsync).toHaveBeenCalledOnce() + expect(reindexMutation.mutateAsync).toHaveBeenCalledWith({ + body: { documentIds: ['one'] }, + params: { id: 'space-1' }, + }) + await waitFor(() => + expect(screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' })).toHaveFocus(), + ) + }) + + it('explains why re-indexing becomes unavailable while results refresh', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const rendered = render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + + tasksQuery.data = undefined + tasksQuery.isPending = true + rendered.rerender() + + const actions = screen.getByRole('group', { + name: 'dataset.newKnowledge.bulkDocumentActions', + }) + expect( + within(actions).getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' }), + ).toHaveAttribute('aria-describedby', 'document-reindex-unavailable') + expect( + within(actions).getByText('dataset.newKnowledge.reindexDocuments · common.loading'), + ).toBeVisible() + + tasksQuery.data = { pages: [{ items: [] }] } + tasksQuery.isPending = false + sourcesQuery.data = { pages: [{ items: [] }] } + sourcesQuery.error = new Error('source refresh failed') + rendered.rerender() + + expect( + within(actions).getByText( + 'dataset.newKnowledge.reindexDocuments · dataset.newKnowledge.sourcesErrorDescription', + ), + ).toBeVisible() + }) + + it('keeps missing documents selected after a partial bulk re-index result', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [ + { + items: [ + document({ id: 'one', title: 'One.pdf' }), + document({ id: 'missing', title: 'Missing.pdf' }), + ], + }, + ], + } + reindexMutation.mutateAsync.mockResolvedValue({ + bulkJobId: 'reindex-partial', + items: [ + { + asset: { + createdAt: '2026-07-20T10:00:00Z', + filename: 'one.pdf', + id: 'asset-1', + knowledgeSpaceId: 'space-1', + mimeType: 'application/pdf', + objectKey: 'documents/one.pdf', + parserStatus: 'pending', + sha256: 'sha', + sizeBytes: 1024, + version: 1, + }, + compilationJob: { id: 'job-1', stage: 'queued' }, + status: 'queued', + statusUrl: '/tasks/job-1', + }, + { documentId: 'missing', status: 'not_found' }, + ], + total: 2, + }) + + render() + await user.click(screen.getByRole('checkbox', { name: 'One.pdf' })) + await user.click(screen.getByRole('checkbox', { name: 'Missing.pdf' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' })) + + expect(screen.getByRole('checkbox', { name: 'One.pdf' })).not.toBeChecked() + expect(screen.getByRole('checkbox', { name: 'Missing.pdf' })).toBeChecked() + expect(toastMock.warning).toHaveBeenCalledWith( + 'dataset.newKnowledge.documentsReindexPartial:{"missing":1,"queued":1}', + ) + }) + + it('clears stale selection and refreshes after every re-index target is missing', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({ id: 'missing' })] }] } + reindexMutation.mutateAsync.mockResolvedValue({ + bulkJobId: 'reindex-missing', + items: [{ documentId: 'missing', status: 'not_found' }], + total: 1, + }) + + render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' })) + + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).not.toBeChecked() + expect( + screen.queryByRole('group', { name: 'dataset.newKnowledge.bulkDocumentActions' }), + ).not.toBeInTheDocument() + expect(queryClient.invalidateQueries).toHaveBeenCalled() + expect(toastMock.error).toHaveBeenCalledWith( + 'dataset.newKnowledge.documentsReindexPartial:{"missing":1,"queued":0}', + ) + }) + + it('counts only attention tasks and opens the designed task panel', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ id: 'running' }), + task({ id: 'failed', state: 'failed', errorMessage: 'Parser failed' }), + task({ id: 'done', state: 'succeeded' }), + ], + }, + ], + } + + render() + + const trigger = screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":2}', + }) + expect(trigger).toHaveTextContent('2') + expect(trigger).toHaveAttribute('data-has-error', 'true') + await user.click(trigger) + const panel = screen.getByRole('dialog', { name: 'dataset.newKnowledge.backgroundTasks' }) + expect( + within(panel).getByText('dataset.newKnowledge.backgroundTasksDescription'), + ).toBeInTheDocument() + expect(within(panel).getByText('Parser failed')).toBeInTheDocument() + expect( + within(panel).getByText((_, element) => + Boolean( + element?.tagName === 'P' && + element.textContent?.includes('dataset.newKnowledge.processingTaskState.running') && + element.textContent.includes('·'), + ), + ), + ).toBeInTheDocument() + }) + + it('gives duplicate task actions distinct accessible names', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [ + { + items: [ + document({ id: 'document-a', title: 'Alpha.pdf' }), + document({ id: 'document-b', title: 'Beta.pdf' }), + ], + }, + ], + } + tasksQuery.data = { + pages: [ + { + items: [ + task({ documentId: 'document-a', id: 'task-a' }), + task({ documentId: 'document-b', id: 'task-b' }), + ], + }, + ], + } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":2}', + }), + ) + + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.interruptTask · Alpha.pdf · task-a', + }), + ).toBeInTheDocument() + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.interruptTask · Beta.pdf · task-b', + }), + ).toBeInTheDocument() + }) + + it('shows task loading instead of an empty result while the task query is pending', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const { rerender } = render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + + tasksQuery.isPending = true + rerender() + + const panel = screen.getByRole('dialog', { name: 'dataset.newKnowledge.backgroundTasks' }) + expect(within(panel).getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + expect( + within(panel).queryByText('dataset.newKnowledge.noBackgroundTasks'), + ).not.toBeInTheDocument() + }) + + it('prevents duplicate interrupt requests and refreshes both resources', async () => { + const user = userEvent.setup() + let resolveCancel: ((value: DocumentProcessingTask) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveCancel = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'running' })] }] } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + const interrupt = screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }) + await user.dblClick(interrupt) + + expect(cancelMutation.mutateAsync).toHaveBeenCalledOnce() + expect(cancelMutation.mutateAsync).toHaveBeenCalledWith({ + params: { documentId: 'document-1', id: 'space-1', taskId: 'running' }, + }) + await act(async () => resolveCancel?.(task({ id: 'running', state: 'canceled' }))) + expect(queryClient.invalidateQueries).toHaveBeenCalled() + expect(screen.getByRole('button', { name: 'common.operation.close' })).toHaveFocus() + }) + + it('locks cancel and retry together while either task mutation is pending', async () => { + const user = userEvent.setup() + let resolveCancel: ((value: DocumentProcessingTask) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveCancel = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'shared-task' })] }] } + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + + tasksQuery.data = { + pages: [{ items: [task({ id: 'shared-task', state: 'failed' })] }], + } + rerender() + const retry = screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' }) + expect(retry).toHaveAttribute('aria-disabled', 'true') + await user.click(retry) + expect(retryMutation.mutateAsync).not.toHaveBeenCalled() + + await act(async () => resolveCancel?.(task({ id: 'shared-task', state: 'canceled' }))) + }) + + it('retries a failed task once and reports mutation errors in the panel', async () => { + const user = userEvent.setup() + retryMutation.mutateAsync.mockRejectedValueOnce(new Error('retry failed')) + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'failed', state: 'failed' })] }] } + + const rendered = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + expect(retryMutation.mutateAsync).toHaveBeenCalledOnce() + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.taskActionFailed', + ) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'failed', + state: 'running', + }), + ], + }, + ], + } + rendered.rerender() + expect(screen.queryByText('dataset.newKnowledge.taskActionFailed')).not.toBeInTheDocument() + }) + + it('does not restore an action failure after its drawer cycle closes', async () => { + const user = userEvent.setup() + let rejectCancel: ((reason?: unknown) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((_resolve, reject) => { + rejectCancel = reject + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + await user.click(screen.getByRole('button', { name: 'common.operation.close' })) + await act(async () => rejectCancel?.(new Error('cancel failed after close'))) + + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.queryByText('dataset.newKnowledge.taskActionFailed')).not.toBeInTheDocument() + }) + + it('does not move focus when an action from an older drawer cycle succeeds', async () => { + const user = userEvent.setup() + let resolveCancel: ((value: DocumentProcessingTask) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveCancel = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'old-action' })] }] } + + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + await user.click(screen.getByRole('button', { name: 'common.operation.close' })) + + tasksQuery.data = { + pages: [{ items: [task({ id: 'old-action' }), task({ id: 'new-action' })] }], + } + rerender() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":2}', + }), + ) + const nextAction = screen.getByRole('button', { + name: 'dataset.newKnowledge.interruptTask · sso-enterprise.pdf · new-action', + }) + act(() => nextAction.focus()) + expect(nextAction).toHaveFocus() + + await act(async () => resolveCancel?.(task({ id: 'old-action', state: 'canceled' }))) + expect(nextAction).toHaveFocus() + }) + + it('does not move focus when the user leaves a pending task action', async () => { + const user = userEvent.setup() + let resolveCancel: ((value: DocumentProcessingTask) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveCancel = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'pending-action' }), task({ id: 'focus-target' })] }], + } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":2}', + }), + ) + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.interruptTask · sso-enterprise.pdf · pending-action', + }), + ) + const nextAction = screen.getByRole('button', { + name: 'dataset.newKnowledge.interruptTask · sso-enterprise.pdf · focus-target', + }) + act(() => nextAction.focus()) + + await act(async () => resolveCancel?.(task({ id: 'pending-action', state: 'canceled' }))) + expect(nextAction).toHaveFocus() + }) + + it('ignores a delayed action success after the task lifecycle advances', async () => { + const user = userEvent.setup() + let resolveRetry: ((value: DocumentProcessingTask) => void) | undefined + retryMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveRetry = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'advanced-success', state: 'failed' })] }] } + + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + tasksQuery.data = { + pages: [{ items: [task({ id: 'advanced-success', state: 'running' })] }], + } + rerender() + await act(async () => resolveRetry?.(task({ id: 'advanced-success', state: 'queued' }))) + + expect( + screen.getByText(/dataset\.newKnowledge\.processingTaskState\.running/), + ).toBeInTheDocument() + }) + + it('ignores a delayed task action after document permission is revoked', async () => { + const user = userEvent.setup() + let resolveCancel: ((value: DocumentProcessingTask) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveCancel = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'revoked-action' })] }] } + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + + queryClient.invalidateQueries.mockClear() + documentsQuery.error = { status: 403 } + rerender() + await act(async () => resolveCancel?.(task({ id: 'revoked-action', state: 'canceled' }))) + expect(queryClient.invalidateQueries).not.toHaveBeenCalled() + + documentsQuery.error = null + rerender() + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ).toBeInTheDocument() + }) + + it('keeps a successful task action across a transient permission query failure', async () => { + const user = userEvent.setup() + let resolveCancel: ((value: DocumentProcessingTask) => void) | undefined + cancelMutation.mutateAsync.mockImplementation( + () => + new Promise((resolve) => { + resolveCancel = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'permission-query-action' })] }] } + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + + permissionStateMock.error = new Error('permission query unavailable') + rerender() + queryClient.invalidateQueries.mockClear() + await act(async () => + resolveCancel?.(task({ id: 'permission-query-action', state: 'canceled' })), + ) + + expect(queryClient.invalidateQueries).toHaveBeenCalled() + expect( + screen.getByText(/dataset\.newKnowledge\.processingTaskState\.canceled/), + ).toBeInTheDocument() + }) + + it('drops a delayed action failure after the task lifecycle advances', async () => { + const user = userEvent.setup() + let rejectRetry: ((reason?: unknown) => void) | undefined + retryMutation.mutateAsync.mockImplementation( + () => + new Promise((_resolve, reject) => { + rejectRetry = reject + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'advanced', state: 'failed' })] }] } + + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + tasksQuery.data = { pages: [{ items: [task({ id: 'advanced', state: 'running' })] }] } + rerender() + await act(async () => rejectRetry?.(new Error('late retry failure'))) + tasksQuery.data = { pages: [{ items: [task({ id: 'advanced', state: 'failed' })] }] } + rerender() + + expect(screen.queryByText('dataset.newKnowledge.taskActionFailed')).not.toBeInTheDocument() + }) + + it('does not offer retry for a canceled task', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'canceled', state: 'canceled' })] }] } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasks', + }), + ) + + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).not.toBeInTheDocument() + }) + + it('clears an old failure across retry and shows the next terminal error', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ id: 'retried-failure', state: 'failed', errorMessage: 'Old parser error' }), + ], + }, + ], + } + retryMutation.mutateAsync.mockResolvedValue( + task({ + id: 'retried-failure', + progressPercent: 0, + state: 'queued', + updatedAt: '2026-07-20T10:02:00Z', + }), + ) + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 20, + stage: 'parsed' as const, + state: 'failed' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: 'retried-failure:2026-07-20T10:03:00Z', + } + yield { + data: { errorCode: 'NEW_PARSER_ERROR', state: 'failed' as const }, + event: 'terminal' as const, + id: 'retried-failure:terminal', + } + }) + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + await waitFor(() => expect(screen.queryByText('Old parser error')).not.toBeInTheDocument()) + expect(await screen.findByText('NEW_PARSER_ERROR')).toBeInTheDocument() + }) + + it('ignores a delayed retry response older than the current failed list snapshot', async () => { + const user = userEvent.setup() + let resolveRetry: ((snapshot: DocumentProcessingTask) => void) | undefined + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ + errorCode: 'OLD_FAILURE', + id: 'delayed-local-retry', + state: 'failed', + updatedAt: '2026-07-20T10:01:00Z', + }), + ], + }, + ], + } + retryMutation.mutateAsync.mockReturnValue( + new Promise((resolve) => { + resolveRetry = resolve + }), + ) + + const rendered = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + errorCode: 'NEWER_FAILURE', + id: 'delayed-local-retry', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ], + }, + ], + } + rendered.rerender() + await act(async () => + resolveRetry?.( + task({ + id: 'delayed-local-retry', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:02:00Z', + }), + ), + ) + + expect(await screen.findByText('NEWER_FAILURE')).toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + }) + + it('accepts an exact active retry snapshot with the terminal timestamp', async () => { + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'same-time-retry' })] }] } + getTaskSnapshot.mockResolvedValue( + task({ + id: 'same-time-retry', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 80, + stage: 'parsed' as const, + state: 'failed' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: 'same-time-retry:2026-07-20T10:03:00Z', + } + yield { + data: { errorCode: 'PARSER_FAILED', state: 'failed' as const }, + event: 'terminal' as const, + id: 'same-time-retry:terminal', + } + return + } + await new Promise(() => {}) + }) + + render() + + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ).toHaveTextContent('1'), + ) + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2)) + expect(getTaskSnapshot).toHaveBeenCalledWith( + { + params: { documentId: 'document-1', id: 'space-1', taskId: 'same-time-retry' }, + }, + expect.objectContaining({ signal: expect.any(AbortSignal) }), + ) + }) + + it('restarts an aborted equal-timestamp reconciliation after permission returns', async () => { + const signals: AbortSignal[] = [] + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'permission-reconciliation' })] }] } + getTaskSnapshot.mockImplementation( + (_input: unknown, options: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signals.push(options.signal) + options.signal.addEventListener( + 'abort', + () => reject(new DOMException('Aborted', 'AbortError')), + { once: true }, + ) + }), + ) + streamFailedTaskThenWait('permission-reconciliation') + + const { rerender } = render() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'permission-reconciliation', updatedAt: '2026-07-20T10:03:00Z' })], + }, + ], + } + rerender() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(2)) + + documentsQuery.error = { status: 403 } + rerender() + await waitFor(() => expect(signals[1]?.aborted).toBe(true)) + documentsQuery.error = null + rerender() + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(3)) + expect(signals[2]?.aborted).toBe(false) + }) + + it('restores complete terminal error details from an equal list snapshot', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'listed-terminal-error' })] }] } + getTaskSnapshot.mockRejectedValue(new Response(null, { status: 403 })) + streamFailedTaskThenWait('listed-terminal-error') + + const rendered = render() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + errorCode: 'PARSER_FAILED', + errorMessage: 'Parser failed at line 400 with complete server context', + id: 'listed-terminal-error', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ], + }, + ], + } + rendered.rerender() + + const error = await screen.findByText('Parser failed at line 400 with complete server context') + expect(error).toHaveClass('whitespace-pre-wrap', 'break-words') + expect(error).not.toHaveClass('truncate') + }) + + it('hides cached documents and closes tasks when terminal reconciliation loses read access', async () => { + const user = userEvent.setup() + let releaseStream!: () => void + const streamGate = new Promise((resolve) => { + releaseStream = resolve + }) + let resolveDocumentsRefetch!: (result: { error: null }) => void + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch.mockImplementation( + () => + new Promise((resolve) => { + resolveDocumentsRefetch = resolve + }), + ) + tasksQuery.data = { pages: [{ items: [task({ id: 'forbidden-reconciliation' })] }] } + getTaskSnapshot.mockRejectedValue(new Response(null, { status: 403 })) + streamProcessingTaskEvents.mockImplementation(async function* () { + await streamGate + yield { + data: { + progressPercent: 80, + stage: 'parsed' as const, + state: 'failed' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: 'forbidden-reconciliation:progress', + } + yield { + data: { errorCode: 'PARSER_FAILED', state: 'failed' as const }, + event: 'terminal' as const, + id: 'forbidden-reconciliation:terminal', + } + }) + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.getByRole('dialog')).toBeInTheDocument() + + await act(async () => releaseStream()) + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + await waitFor(() => + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsPermissionDescription', + ), + ) + expect(screen.queryByText('sso-enterprise.pdf')).not.toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(documentsQuery.refetch).toHaveBeenCalledWith({ cancelRefetch: true }) + + await act(async () => resolveDocumentsRefetch({ error: null })) + expect(await screen.findByText('sso-enterprise.pdf')).toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it('blocks failed polling for a task version denied during terminal reconciliation', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'denied-terminal-version' })] }] } + getTaskSnapshot.mockRejectedValue(new Response(null, { status: 403 })) + streamFailedTaskThenWait('denied-terminal-version') + + const rendered = render() + try { + await act(async () => {}) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('hides cached documents and closes tasks when the task event stream loses read access', async () => { + const user = userEvent.setup() + let rejectStream!: () => void + const streamGate = new Promise((_resolve, reject) => { + rejectStream = () => reject(new Response(null, { status: 403 })) + }) + let resolveDocumentsRefetch!: (result: { error: null }) => void + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch.mockImplementation( + () => + new Promise((resolve) => { + resolveDocumentsRefetch = resolve + }), + ) + tasksQuery.data = { pages: [{ items: [task({ id: 'forbidden-stream' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () { + await streamGate + }) + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.getByRole('dialog')).toBeInTheDocument() + + await act(async () => rejectStream()) + + await waitFor(() => + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsPermissionDescription', + ), + ) + expect(screen.queryByText('sso-enterprise.pdf')).not.toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(documentsQuery.refetch).toHaveBeenCalledWith({ cancelRefetch: true }) + + await act(async () => resolveDocumentsRefetch({ error: null })) + expect(await screen.findByText('sso-enterprise.pdf')).toBeInTheDocument() + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + }) + + it('does not restart a denied stream version while the task list lags behind progress', async () => { + let resolveDocumentsRefetch!: (result: { error: null }) => void + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch.mockImplementation( + () => + new Promise((resolve) => { + resolveDocumentsRefetch = resolve + }), + ) + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'lagging-denied-stream', updatedAt: '2026-07-20T10:01:00Z' })], + }, + ], + } + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'lagging-denied-stream:version-2', + } + throw new Response(null, { status: 403 }) + }) + + render() + await waitFor(() => + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsPermissionDescription', + ), + ) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + + await act(async () => resolveDocumentsRefetch({ error: null })) + expect(await screen.findByText('sso-enterprise.pdf')).toBeInTheDocument() + await act(async () => {}) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + }) + + it('observes a same-version retry after the task list confirms a denied stream terminal', async () => { + let resolveDocumentsRefetch!: (result: { error: null }) => void + const deniedVersion = '2026-07-20T10:02:00Z' + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch.mockImplementation( + () => + new Promise((resolve) => { + resolveDocumentsRefetch = resolve + }), + ) + tasksQuery.dataUpdateCount = 1 + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'same-version-terminal-retry' })], + }, + ], + } + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: deniedVersion, + }, + event: 'progress' as const, + id: 'same-version-terminal-retry:version-2', + } + throw new Response(null, { status: 403 }) + } + await new Promise(() => {}) + }) + + render() + await waitFor(() => expect(documentsQuery.refetch).toHaveBeenCalled()) + await act(async () => resolveDocumentsRefetch({ error: null })) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'same-version-terminal-retry', + state: 'failed', + updatedAt: deniedVersion, + }), + ], + }, + ], + } + tasksQuery.dataUpdateCount = 2 + act(() => notifyTaskQuerySuccess()) + await act(async () => {}) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'same-version-terminal-retry', + state: 'running', + updatedAt: deniedVersion, + }), + ], + }, + ], + } + tasksQuery.dataUpdateCount = 3 + act(() => notifyTaskQuerySuccess()) + + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2)) + }) + + it('restarts an active override after a structurally shared terminal confirmation', async () => { + const user = userEvent.setup() + let resolveDocumentsRefetch!: (result: { error: null }) => void + const taskVersion = '2026-07-20T10:02:00Z' + const sharedTerminalData = { + pages: [ + { + items: [ + task({ + id: 'shared-terminal-confirmation', + state: 'failed', + updatedAt: taskVersion, + }), + ], + }, + ], + } + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch.mockImplementation( + () => + new Promise((resolve) => { + resolveDocumentsRefetch = resolve + }), + ) + tasksQuery.data = sharedTerminalData + tasksQuery.dataUpdateCount = 1 + retryMutation.mutateAsync.mockResolvedValue( + task({ + id: 'shared-terminal-confirmation', + state: 'dispatch_pending', + updatedAt: taskVersion, + }), + ) + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) throw new Response(null, { status: 403 }) + await new Promise(() => {}) + }) + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + await waitFor(() => expect(documentsQuery.refetch).toHaveBeenCalled()) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + await act(async () => resolveDocumentsRefetch({ error: null })) + + tasksQuery.data = sharedTerminalData + tasksQuery.dataUpdateCount = 2 + act(() => notifyTaskQuerySuccess()) + + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2)) + }) + + it('single-flights backed-off recovery polling until a denied stream gets a newer version', async () => { + vi.useFakeTimers() + let resolveDocumentsRefetch!: (result: { error: null }) => void + let resolveTaskRefetch!: (result: { error: null }) => void + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch.mockImplementation( + () => + new Promise((resolve) => { + resolveDocumentsRefetch = resolve + }), + ) + tasksQuery.dataUpdateCount = 1 + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'recovering-stream', updatedAt: '2026-07-20T10:01:00Z' })], + }, + ], + } + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'recovering-stream:version-2', + } + throw new Response(null, { status: 403 }) + } + await new Promise(() => {}) + }) + + const rendered = render() + try { + await act(async () => {}) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + await act(async () => resolveDocumentsRefetch({ error: null })) + tasksQuery.refetch.mockClear() + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'recovering-stream', updatedAt: '2026-07-20T10:02:00Z' })], + }, + ], + } + tasksQuery.dataUpdateCount = 2 + act(() => notifyTaskQuerySuccess()) + await act(async () => {}) + tasksQuery.refetch + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveTaskRefetch = resolve + }), + ) + .mockResolvedValue({ error: null }) + + await act(async () => vi.advanceTimersByTime(5000)) + expect(tasksQuery.refetch).toHaveBeenCalledWith({ cancelRefetch: false }) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + await act(async () => vi.advanceTimersByTime(120000)) + expect(tasksQuery.refetch).toHaveBeenCalledOnce() + + await act(async () => resolveTaskRefetch({ error: null })) + for (const delay of [10000, 20000, 30000, 30000, 30000, 30000]) + await act(async () => vi.advanceTimersByTime(delay)) + expect(tasksQuery.refetch).toHaveBeenCalledTimes(7) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'recovering-stream', updatedAt: '2026-07-20T10:03:00Z' })], + }, + ], + } + tasksQuery.dataUpdateCount = 3 + act(() => notifyTaskQuerySuccess()) + await act(async () => {}) + + expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2) + tasksQuery.refetch.mockClear() + await act(async () => vi.advanceTimersByTime(60000)) + expect(tasksQuery.refetch).not.toHaveBeenCalled() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('focuses the permission alert when an auxiliary retry becomes a document denial', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + documentsQuery.refetch + .mockResolvedValueOnce({ error: new Error('verification unavailable') }) + .mockImplementationOnce(() => new Promise(() => {})) + tasksQuery.data = { pages: [{ items: [task({ id: 'retry-document-denial' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () { + throw new Response(null, { status: 403 }) + }) + + const rendered = render() + const retry = await screen.findByRole('button', { + name: 'common.operation.retry · dataset.newKnowledge.documentsPermissionDescription', + }) + await user.click(retry) + + documentsQuery.error = { status: 403 } + rendered.rerender() + + await waitFor(() => expect(screen.getByRole('alert')).toHaveFocus()) + expect(screen.queryByText('sso-enterprise.pdf')).not.toBeInTheDocument() + }) + + it('replaces an in-flight terminal reconciliation and aborts it on unmount', async () => { + vi.useFakeTimers() + const signals: AbortSignal[] = [] + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'coalesced-reconciliation' })] }] } + getTaskSnapshot.mockImplementation( + (_input: unknown, options: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signals.push(options.signal) + options.signal.addEventListener( + 'abort', + () => reject(new DOMException('Aborted', 'AbortError')), + { once: true }, + ) + }), + ) + streamFailedTaskThenWait('coalesced-reconciliation') + + const rendered = render() + try { + await act(async () => {}) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'coalesced-reconciliation', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ], + }, + ], + } + rendered.rerender() + await act(async () => {}) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + expect(signals[0]?.aborted).toBe(true) + expect(signals[1]?.aborted).toBe(false) + rendered.unmount() + expect(signals[1]?.aborted).toBe(true) + await act(async () => vi.advanceTimersByTime(60000)) + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('retries a terminal reconciliation after its request deadline', async () => { + vi.useFakeTimers() + const signals: AbortSignal[] = [] + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'timed-reconciliation' })] }] } + getTaskSnapshot + .mockImplementationOnce( + (_input: unknown, options: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signals.push(options.signal) + options.signal.addEventListener( + 'abort', + () => reject(new DOMException('Aborted', 'AbortError')), + { once: true }, + ) + }), + ) + .mockResolvedValueOnce( + task({ + errorCode: 'TERMINAL_CONFIRMED', + id: 'timed-reconciliation', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamFailedTaskThenWait('timed-reconciliation') + + const rendered = render() + try { + await act(async () => {}) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + await act(async () => vi.advanceTimersByTime(3000)) + expect(signals[0]?.aborted).toBe(true) + await act(async () => vi.advanceTimersByTime(1000)) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('replaces an older reconciliation backoff with the latest generation retry', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'latest-reconciliation' })] }] } + getTaskSnapshot + .mockRejectedValueOnce(new Error('first snapshot unavailable')) + .mockRejectedValueOnce(new Error('second snapshot unavailable')) + .mockResolvedValueOnce( + task({ + id: 'latest-reconciliation', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamFailedTaskThenWait('latest-reconciliation') + + const rendered = render() + try { + await act(async () => {}) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'latest-reconciliation', updatedAt: '2026-07-20T10:03:00Z' })], + }, + ], + } + tasksQuery.dataUpdateCount += 1 + rendered.rerender() + await act(async () => {}) + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + + await act(async () => vi.advanceTimersByTime(1000)) + expect(getTaskSnapshot).toHaveBeenCalledTimes(3) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('accepts a later active list snapshot after exact reconciliation saw the terminal state', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'later-same-time-retry' })] }] } + getTaskSnapshot + .mockResolvedValueOnce( + task({ + errorCode: 'PARSER_FAILED', + id: 'later-same-time-retry', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + .mockResolvedValueOnce( + task({ + id: 'later-same-time-retry', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamFailedTaskThenWait('later-same-time-retry') + + const { rerender } = render() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'later-same-time-retry', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ], + }, + ], + } + rerender() + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(2)) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + expect(screen.queryByText('PARSER_FAILED')).not.toBeInTheDocument() + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2)) + }) + + it('uses a later active list snapshot when exact reconciliation fails', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'failed-reconciliation-retry' })] }] } + getTaskSnapshot.mockRejectedValueOnce(new Error('snapshot unavailable')).mockResolvedValueOnce( + task({ + id: 'failed-reconciliation-retry', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamFailedTaskThenWait('failed-reconciliation-retry') + + const { rerender } = render() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'failed-reconciliation-retry', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ], + }, + ], + } + rerender() + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(2)) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + expect(screen.queryByText('PARSER_FAILED')).not.toBeInTheDocument() + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2)) + }) + + it('does not treat a stale active row from a completed cursor page as a retry', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'stale-cursor-active', updatedAt: '2026-07-20T10:03:00Z' })], + }, + ], + } + getTaskSnapshot.mockResolvedValue( + task({ + errorCode: 'PARSER_FAILED', + id: 'stale-cursor-active', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamFailedTaskThenWait('stale-cursor-active') + + const { rerender } = render() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'stale-cursor-active', updatedAt: '2026-07-20T10:03:00Z' })], + }, + { items: [task({ id: 'another-page', state: 'succeeded' })] }, + ], + } + rerender() + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(2)) + expect(screen.getByText('PARSER_FAILED')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).toBeInTheDocument() + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + }) + + it('ignores a terminal reconciliation that resolves after a local retry', async () => { + const user = userEvent.setup() + let resolveSnapshot: ((snapshot: DocumentProcessingTask) => void) | undefined + let reconciliationSignal: AbortSignal | undefined + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'local-retry-race' })] }] } + getTaskSnapshot.mockImplementation( + (_input: unknown, options: { signal: AbortSignal }) => + new Promise((resolve) => { + reconciliationSignal = options.signal + resolveSnapshot = resolve + }), + ) + retryMutation.mutateAsync.mockResolvedValue( + task({ + id: 'local-retry-race', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamFailedTaskThenWait('local-retry-race') + + render() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + expect(reconciliationSignal?.aborted).toBe(true) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + + await act(async () => { + resolveSnapshot?.( + task({ + errorCode: 'PARSER_FAILED', + id: 'local-retry-race', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + }) + + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + expect(screen.queryByText('PARSER_FAILED')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).not.toBeInTheDocument() + }) + + it('consumes the terminal error after final progress without duplicate side effects', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'failed-stream' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 80, + stage: 'parsed' as const, + state: 'failed' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: 'failed-stream:2026-07-20T10:03:00Z', + } + yield { + data: { errorCode: 'PARSER_FAILED', state: 'failed' as const }, + event: 'terminal' as const, + id: 'failed-stream:terminal', + } + }) + + render() + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + expect(toastMock.error).toHaveBeenCalledTimes(1) + expect(queryClient.invalidateQueries).toHaveBeenCalledOnce() + }) + + it('applies task events and clears the attention badge after completion', async () => { + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'running' })] }] } + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 100, + stage: 'published' as const, + state: 'succeeded' as const, + updatedAt: '2026-07-20T10:06:00Z', + }, + event: 'progress' as const, + id: 'running:2026-07-20T10:06:00Z', + } + yield { + data: { state: 'succeeded' as const }, + event: 'terminal' as const, + id: 'running:terminal', + } + return + } + await new Promise(() => {}) + }) + + const { rerender } = render() + + await waitFor(() => + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' }), + ).not.toHaveTextContent('1'), + ) + expect(streamProcessingTaskEvents).toHaveBeenCalledWith( + expect.objectContaining({ + documentId: 'document-1', + knowledgeSpaceId: 'space-1', + taskId: 'running', + }), + ) + expect(queryClient.invalidateQueries).toHaveBeenCalled() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'running', + progressPercent: 90, + state: 'running', + updatedAt: '2026-07-20T10:05:00Z', + }), + ], + }, + ], + } + rerender() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' }), + ).not.toHaveTextContent('1') + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'running', + progressPercent: 100, + state: 'succeeded', + updatedAt: '2026-07-20T10:06:00Z', + }), + ], + }, + ], + } + rerender() + await waitFor(() => + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' }), + ).not.toHaveTextContent('1'), + ) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'running', + progressPercent: 0, + state: 'queued', + updatedAt: '2026-07-20T10:07:00Z', + }), + ], + }, + ], + } + rerender() + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ).toHaveTextContent('1'), + ) + }) + + it('accepts an external retry newer than the terminal event version', async () => { + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'externally-retried' })] }] } + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 100, + stage: 'published' as const, + state: 'succeeded' as const, + updatedAt: '2026-07-20T10:04:00Z', + }, + event: 'progress' as const, + id: 'externally-retried:2026-07-20T10:04:00Z', + } + yield { + data: { state: 'succeeded' as const }, + event: 'terminal' as const, + id: 'externally-retried:terminal', + } + return + } + await new Promise(() => {}) + }) + + const { rerender } = render() + + await waitFor(() => + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' }), + ).not.toHaveTextContent('1'), + ) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'externally-retried', + progressPercent: 0, + state: 'queued', + updatedAt: '2026-07-20T10:05:00Z', + }), + ], + }, + ], + } + rerender() + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ).toHaveTextContent('1'), + ) + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2), { + timeout: 2000, + }) + }) + + it('ignores a stale terminal event after polling observes an external retry', async () => { + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'retry-race' })] }] } + let releaseTerminal: (() => void) | undefined + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + await new Promise((resolve) => { + releaseTerminal = resolve + }) + yield { + data: { + progressPercent: 100, + stage: 'published' as const, + state: 'succeeded' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: 'retry-race:progress', + } + yield { + data: { state: 'succeeded' as const }, + event: 'terminal' as const, + id: 'retry-race:terminal', + } + return + } + await new Promise(() => {}) + }) + + const { rerender } = render() + await waitFor(() => expect(releaseTerminal).toBeTypeOf('function')) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'retry-race', + progressPercent: 0, + state: 'queued', + updatedAt: '2026-07-20T10:05:00Z', + }), + ], + }, + ], + } + rerender() + + await act(async () => releaseTerminal?.()) + await waitFor(() => + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ).toHaveTextContent('1'), + ) + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2), { + timeout: 2000, + }) + }) + + it('keeps one event stream and its resume cursor when polling updates task versions', async () => { + let releaseStream: (() => void) | undefined + streamProcessingTaskEvents.mockImplementation(async function* () { + await new Promise((resolve) => { + releaseStream = resolve + }) + }) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'streaming' })] }] } + const { rerender } = render() + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledOnce()) + + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'streaming', updatedAt: '2026-07-20T10:02:00Z' })], + }, + ], + } + rerender() + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + + await act(async () => releaseStream?.()) + }) + + it('keeps task event progress monotonic and yields to a newer server snapshot', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document({})] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'running' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 80, + stage: 'nodes_generated' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:03:00Z', + }, + event: 'progress' as const, + id: 'running:newer', + } + yield { + data: { + progressPercent: 20, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'running:stale', + } + }) + + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await waitFor(() => + expect( + screen.getByText(/dataset\.newKnowledge\.processingTaskState\.running.*"progress":80/), + ).toBeInTheDocument(), + ) + await act(async () => {}) + expect( + screen.queryByText(/dataset\.newKnowledge\.processingTaskState\.running.*"progress":20/), + ).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'common.operation.close' })) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'running', + progressPercent: 100, + state: 'succeeded', + updatedAt: '2026-07-20T10:04:00Z', + }), + ], + }, + ], + } + rerender() + + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })).toBeInTheDocument() + }) + + it('lets an equal-timestamp terminal list row replace an active stream override', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'equal-terminal', state: 'queued' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 60, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:01:00Z', + }, + event: 'progress' as const, + id: 'equal-terminal:running', + } + await new Promise(() => {}) + }) + + const { rerender } = render() + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledOnce()) + tasksQuery.data = { + pages: [{ items: [task({ id: 'equal-terminal', state: 'failed' })] }], + } + rerender() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })).toBeEnabled() + }) + + it('moves focus to the drawer close button when an external update removes a task action', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'external-terminal' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () { + await new Promise(() => {}) + }) + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + const action = screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }) + act(() => action.focus()) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'external-terminal', + state: 'succeeded', + updatedAt: '2026-07-20T10:02:00Z', + }), + ], + }, + ], + } + rerender() + + await waitFor(() => + expect(screen.getByRole('button', { name: 'common.operation.close' })).toHaveFocus(), + ) + }) + + it('limits rendered task history while retaining an active retry for an old document', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const history = Array.from({ length: 150 }, (_, index) => + task({ + id: `history-${index}`, + state: 'succeeded', + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ) + history[0] = task({ + createdAt: '2026-07-01T10:00:00Z', + id: 'old-active-retry', + state: 'running', + updatedAt: '2026-07-01T10:00:00Z', + }) + history[1] = task({ + createdAt: '2026-07-01T10:01:00Z', + id: 'old-failed-retry', + state: 'failed', + updatedAt: '2026-08-01T10:00:00Z', + }) + tasksQuery.data = { pages: [{ items: history }] } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":2}', + }), + ) + + expect(screen.getAllByRole('listitem')).toHaveLength(100) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).toBeInTheDocument() + }) + + it('keeps a failed task actionable when one hundred active tasks fill the drawer', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + ...Array.from({ length: 100 }, (_, index) => + task({ + id: `active-${index}`, + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + task({ + id: 'failed-outside-active-limit', + state: 'failed', + updatedAt: '2026-07-01T10:00:00Z', + }), + ], + }, + ], + } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":101}', + }), + ) + + expect(screen.getAllByRole('listitem')).toHaveLength(100) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).toBeInTheDocument() + }) + + it('keeps an active task actionable when one hundred failed tasks fill the drawer', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + ...Array.from({ length: 100 }, (_, index) => + task({ + id: `failed-${index}`, + state: 'failed', + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + task({ + id: 'active-outside-failed-limit', + state: 'running', + updatedAt: '2026-07-01T10:00:00Z', + }), + ], + }, + ], + } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":101}', + }), + ) + + expect(screen.getAllByRole('listitem')).toHaveLength(100) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + await user.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'dataset.newKnowledge.loadMore', + }), + ) + expect(screen.getAllByRole('listitem')).toHaveLength(101) + }) + + it('moves focus to the drawer close button when the final task batch is revealed', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: Array.from({ length: 101 }, (_, index) => + task({ + id: `history-${index}`, + state: 'succeeded', + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + }, + ], + } + + render() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.tasks' })) + await user.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'dataset.newKnowledge.loadMore', + }), + ) + + await waitFor(() => + expect(screen.getByRole('button', { name: 'common.operation.close' })).toHaveFocus(), + ) + }) + + it('preserves focus when the user leaves a pending final-page control', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const firstPage = [ + task({ id: 'active-focus-target' }), + ...Array.from({ length: 99 }, (_, index) => + task({ + id: `history-${index}`, + state: 'succeeded', + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + ] + tasksQuery.data = { pages: [{ items: firstPage, nextCursor: 'final-page' }] } + tasksQuery.hasNextPage = true + + const { rerender } = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1} · dataset.newKnowledge.taskHistoryIncomplete', + }), + ) + await user.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'dataset.newKnowledge.loadMore', + }), + ) + const action = screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }) + act(() => action.focus()) + + tasksQuery.data = { + pages: [{ items: firstPage }, { items: [task({ id: 'last-task', state: 'succeeded' })] }], + } + tasksQuery.hasNextPage = false + rerender() + + expect(action).toHaveFocus() + }) + + it('rotates a bounded task event stream pool without polling every cursor page', async () => { + vi.useFakeTimers() + streamProcessingTaskEvents.mockImplementation(async function* () { + await new Promise(() => {}) + }) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: Array.from({ length: 20 }, (_, index) => + task({ + id: `active-${index}`, + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + }, + ], + } + + const rendered = render() + try { + await act(async () => {}) + expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(6) + await act(async () => vi.advanceTimersByTime(5000)) + expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(12) + const taskOptions = tasksInfiniteOptions.mock.lastCall?.[0] + expect(taskOptions?.refetchInterval).toBeUndefined() + expect( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":20}', + }), + ).toBeInTheDocument() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('resumes a task event stream from its cursor after rotation', async () => { + vi.useFakeTimers() + const resumeCalls: Array<{ lastEventId?: string; taskId: string }> = [] + streamProcessingTaskEvents.mockImplementation(async function* ({ + lastEventId, + taskId, + }: { + lastEventId?: string + taskId: string + }) { + resumeCalls.push({ lastEventId, taskId }) + if (taskId === 'active-0' && !lastEventId) { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:30:00Z', + }, + event: 'progress' as const, + id: 'active-0:cursor', + } + } + await new Promise(() => {}) + }) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: Array.from({ length: 20 }, (_, index) => + task({ + id: `active-${index}`, + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + }, + ], + } + + const rendered = render() + try { + await act(async () => {}) + for (let index = 0; index < 3; index += 1) { + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => {}) + } + + expect(resumeCalls.filter(({ taskId }) => taskId === 'active-0')).toEqual([ + { lastEventId: undefined, taskId: 'active-0' }, + { lastEventId: 'active-0:cursor', taskId: 'active-0' }, + ]) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('drops an event cursor after the task list reports a terminal state', async () => { + const streamCalls: Array<{ lastEventId?: string; taskId: string }> = [] + streamProcessingTaskEvents.mockImplementation(async function* ({ + lastEventId, + taskId, + }: { + lastEventId?: string + taskId: string + }) { + streamCalls.push({ lastEventId, taskId }) + if (streamCalls.length === 1) { + yield { + data: { + progressPercent: 60, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'restarted:old-cursor', + } + } + await new Promise(() => {}) + }) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'restarted' })] }] } + const { rerender } = render() + await waitFor(() => expect(streamCalls).toHaveLength(1)) + + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'restarted', state: 'failed', updatedAt: '2026-07-20T10:03:00Z' })], + }, + ], + } + rerender() + tasksQuery.data = { + pages: [{ items: [task({ id: 'restarted', updatedAt: '2026-07-20T10:04:00Z' })] }], + } + rerender() + + await waitFor(() => expect(streamCalls).toHaveLength(2)) + expect(streamCalls).toEqual([ + { lastEventId: undefined, taskId: 'restarted' }, + { lastEventId: undefined, taskId: 'restarted' }, + ]) + }) + + it('keeps a retry cursor while the base task list still has the old terminal row', async () => { + vi.useFakeTimers() + const retryStreamCalls: Array = [] + streamProcessingTaskEvents.mockImplementation(async function* ({ + lastEventId, + taskId, + }: { + lastEventId?: string + taskId: string + }) { + if (taskId === 'active-5') { + retryStreamCalls.push(lastEventId) + if (!lastEventId) { + yield { + data: { + progressPercent: 55, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'active-5:retry-cursor', + } + } + } + await new Promise(() => {}) + }) + retryMutation.mutateAsync.mockResolvedValue( + task({ id: 'active-5', state: 'queued', updatedAt: '2026-07-20T10:01:00Z' }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + ...Array.from({ length: 5 }, (_, index) => task({ id: `active-${index}` })), + task({ id: 'active-5', state: 'failed' }), + task({ id: 'active-6' }), + ], + }, + ], + } + + const rendered = render() + try { + fireEvent.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":7}', + }), + ) + fireEvent.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + await act(async () => {}) + expect(retryStreamCalls).toEqual([undefined]) + + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => {}) + + expect(retryStreamCalls).toEqual([undefined, 'active-5:retry-cursor']) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('keeps the latest event cursor when the observer effect restarts', async () => { + const streamCursors: Array = [] + streamProcessingTaskEvents.mockImplementation(async function* ({ + lastEventId, + }: { + lastEventId?: string + }) { + streamCursors.push(lastEventId) + if (streamCursors.length === 1) { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'task-1:cursor', + } + } + await new Promise(() => {}) + }) + const onLastEventIdChange = vi.fn() + const firstOnEvent = vi.fn(() => true) + const rendered = render( + , + ) + + await act(async () => {}) + expect(onLastEventIdChange).toHaveBeenCalledWith('task-1', 'task-1:cursor') + + rendered.rerender( + true)} + onLastEventIdChange={onLastEventIdChange} + onPermissionDenied={vi.fn()} + taskId="task-1" + taskVersion="2026-07-20T10:01:00Z" + />, + ) + await act(async () => {}) + + expect(streamCursors).toEqual([undefined, 'task-1:cursor']) + }) + + it('does not retain a reconnect timer for an already-aborted observer', async () => { + vi.useFakeTimers() + let finishStream!: () => void + streamProcessingTaskEvents.mockImplementation(async function* () { + await new Promise((resolve) => { + finishStream = resolve + }) + }) + const rendered = render( + true)} + onLastEventIdChange={vi.fn()} + onPermissionDenied={vi.fn()} + taskId="task-1" + taskVersion="2026-07-20T10:01:00Z" + />, + ) + try { + await act(async () => {}) + rendered.unmount() + await act(async () => finishStream()) + + expect(vi.getTimerCount()).toBe(0) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('blocks the latest streamed task version when a reconnect loses permission', async () => { + vi.useFakeTimers() + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:02:00Z', + }, + event: 'progress' as const, + id: 'task-1:version-2', + } + return + } + throw new Response(null, { status: 403 }) + }) + const onPermissionDenied = vi.fn() + const rendered = render( + true)} + onLastEventIdChange={vi.fn()} + onPermissionDenied={onPermissionDenied} + taskId="task-1" + taskVersion="2026-07-20T10:01:00Z" + />, + ) + + try { + await act(async () => {}) + await act(async () => vi.advanceTimersByTime(1000)) + + expect(onPermissionDenied).toHaveBeenCalledWith('task-1', '2026-07-20T10:02:00Z') + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('keeps an accepted task version after rejecting a stale event before a denied reconnect', async () => { + vi.useFakeTimers() + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) { + for (const updatedAt of ['2026-07-20T10:02:00Z', '2026-07-20T10:00:00Z']) { + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt, + }, + event: 'progress' as const, + id: `task-1:${updatedAt}`, + } + } + return + } + throw new Response(null, { status: 403 }) + }) + const onPermissionDenied = vi.fn() + const rendered = render( + , + ) + + try { + await act(async () => {}) + await act(async () => vi.advanceTimersByTime(1000)) + + expect(onPermissionDenied).toHaveBeenCalledWith('task-1', '2026-07-20T10:02:00Z') + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('syncs a newer task prop before a reconnect is denied', async () => { + vi.useFakeTimers() + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) return + throw new Response(null, { status: 403 }) + }) + const onPermissionDenied = vi.fn() + const rendered = render( + true)} + onLastEventIdChange={vi.fn()} + onPermissionDenied={onPermissionDenied} + taskId="task-1" + taskVersion="2026-07-20T10:01:00Z" + />, + ) + + try { + await act(async () => {}) + rendered.rerender( + true)} + onLastEventIdChange={vi.fn()} + onPermissionDenied={onPermissionDenied} + taskId="task-1" + taskVersion="2026-07-20T10:03:00Z" + />, + ) + rendered.rerender( + true)} + onLastEventIdChange={vi.fn()} + onPermissionDenied={onPermissionDenied} + taskId="task-1" + taskVersion="2026-07-20T10:02:00Z" + />, + ) + await act(async () => vi.advanceTimersByTime(1000)) + + expect(onPermissionDenied).toHaveBeenCalledWith('task-1', '2026-07-20T10:03:00Z') + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('uses a newer task prop for a terminal-only reconnect', async () => { + vi.useFakeTimers() + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + if (streamCount === 1) return + yield { + data: { errorCode: 'PARSER_FAILED', state: 'failed' as const }, + event: 'terminal' as const, + id: 'task-1:terminal', + } + }) + const onEvent = vi.fn(() => true) + const rendered = render( + , + ) + + try { + await act(async () => {}) + rendered.rerender( + , + ) + rendered.rerender( + , + ) + await act(async () => vi.advanceTimersByTime(1000)) + + expect(onEvent).toHaveBeenCalledWith( + 'task-1', + '2026-07-20T10:03:00Z', + expect.objectContaining({ event: 'terminal' }), + ) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('rejects stale active progress and backs off repeated stale reconnects', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'stale-active-progress', + state: 'queued', + updatedAt: '2026-07-20T10:05:00Z', + }), + ], + }, + ], + } + let streamCount = 0 + streamProcessingTaskEvents.mockImplementation(async function* () { + streamCount += 1 + yield { + data: { + progressPercent: streamCount < 3 ? 40 : 60, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: streamCount < 3 ? '2026-07-20T10:04:00Z' : '2026-07-20T10:06:00Z', + }, + event: 'progress' as const, + id: `stale-active-progress:${streamCount}`, + } + await new Promise(() => {}) + }) + + const rendered = render() + try { + await act(async () => {}) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + expect( + within(screen.getByRole('row', { name: /sso-enterprise\.pdf/ })).getByText( + 'dataset.newKnowledge.documentStatus.queued', + ), + ).toBeInTheDocument() + + await act(async () => vi.advanceTimersByTime(1000)) + expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2) + await act(async () => vi.advanceTimersByTime(1000)) + expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(2) + await act(async () => vi.advanceTimersByTime(1000)) + expect(streamProcessingTaskEvents).toHaveBeenCalledTimes(3) + expect( + within(screen.getByRole('row', { name: /sso-enterprise\.pdf/ })).getByText( + 'dataset.newKnowledge.documentStatus.processing', + ), + ).toBeInTheDocument() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('keeps rotation membership stable when progress updates task timestamps', async () => { + vi.useFakeTimers() + const callsByTask = new Map() + streamProcessingTaskEvents.mockImplementation(async function* ({ taskId }: { taskId: string }) { + callsByTask.set(taskId, (callsByTask.get(taskId) ?? 0) + 1) + yield { + data: { + progressPercent: 50, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: '2026-07-20T12:00:00Z', + }, + event: 'progress' as const, + id: `${taskId}:progress`, + } + await new Promise(() => {}) + }) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: Array.from({ length: 12 }, (_, index) => + task({ + createdAt: new Date(Date.UTC(2026, 6, 20, 9, index)).toISOString(), + id: `stable-${index}`, + updatedAt: new Date(Date.UTC(2026, 6, 20, 10, index)).toISOString(), + }), + ), + }, + ], + } + + const rendered = render() + try { + await act(async () => {}) + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => {}) + + expect(callsByTask).toHaveLength(12) + expect([...callsByTask.values()]).toEqual(Array.from({ length: 12 }, () => 1)) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('preserves the latest progress version when a rotated stream returns terminal-only', async () => { + vi.useFakeTimers() + const streamCounts = new Map() + streamProcessingTaskEvents.mockImplementation(async function* ({ taskId }: { taskId: string }) { + const streamCount = (streamCounts.get(taskId) ?? 0) + 1 + streamCounts.set(taskId, streamCount) + if (taskId === 'rotated-terminal' && streamCount === 1) { + yield { + data: { + progressPercent: 70, + stage: 'nodes_generated' as const, + state: 'running' as const, + updatedAt: '2026-07-20T10:05:00Z', + }, + event: 'progress' as const, + id: 'rotated-terminal:progress', + } + } else if (taskId === 'rotated-terminal') { + yield { + data: { errorCode: 'ROTATED_FAILURE', state: 'failed' as const }, + event: 'terminal' as const, + id: 'rotated-terminal:terminal', + } + return + } + await new Promise(() => {}) + }) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ id: 'rotated-terminal', updatedAt: '2026-07-20T10:01:00Z' }), + ...Array.from({ length: 12 }, (_, index) => + task({ id: `rotation-peer-${index}`, updatedAt: '2026-07-20T10:01:00Z' }), + ), + ], + }, + ], + } + getTaskSnapshot.mockResolvedValue( + task({ + errorCode: 'ROTATED_FAILURE', + id: 'rotated-terminal', + state: 'failed', + updatedAt: '2026-07-20T10:05:00Z', + }), + ) + + const rendered = render() + try { + await act(async () => {}) + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => {}) + + expect(streamCounts.get('rotated-terminal')).toBe(2) + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.taskFailedNotification') + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('relies on bounded reconnect backoff instead of invalidating queries on stream closure', async () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'closed-stream' })] }] } + streamProcessingTaskEvents.mockImplementation(async function* () {}) + + render() + + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalledOnce()) + expect(queryClient.invalidateQueries).not.toHaveBeenCalled() + }) + + it('polls failed tasks through a bounded exact-snapshot pool for external retries', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'external-retry', state: 'failed' })] }], + } + getTaskSnapshot.mockResolvedValue( + task({ + id: 'external-retry', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + await act(async () => {}) + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('keeps one failed-task poll in flight and ignores it after a local retry', async () => { + vi.useFakeTimers() + let resolvePoll: ((snapshot: DocumentProcessingTask) => void) | undefined + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'slow-failed-poll', state: 'failed' })] }], + } + getTaskSnapshot.mockReturnValue( + new Promise((resolve) => { + resolvePoll = resolve + }), + ) + retryMutation.mutateAsync.mockResolvedValue( + task({ + id: 'slow-failed-poll', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + + const rendered = render() + try { + fireEvent.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + fireEvent.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + await act(async () => {}) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + + await act(async () => { + resolvePoll?.( + task({ + errorCode: 'OLD_FAILURE', + id: 'slow-failed-poll', + state: 'failed', + updatedAt: '2026-07-20T10:01:00Z', + }), + ) + }) + + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + expect(screen.queryByText('OLD_FAILURE')).not.toBeInTheDocument() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('ignores a delayed permanent poll rejection after a local retry', async () => { + vi.useFakeTimers() + let rejectPoll: ((error: unknown) => void) | undefined + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'rejected-failed-poll', state: 'failed' })] }], + } + getTaskSnapshot + .mockReturnValueOnce( + new Promise((_resolve, reject) => { + rejectPoll = reject + }), + ) + .mockResolvedValueOnce( + task({ + id: 'rejected-failed-poll', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:05:00Z', + }), + ) + retryMutation.mutateAsync.mockResolvedValue( + task({ + id: 'rejected-failed-poll', + progressPercent: 0, + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + + const rendered = render() + try { + fireEvent.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + fireEvent.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + await act(async () => {}) + await act(async () => rejectPoll?.(new Response(null, { status: 403 }))) + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'rejected-failed-poll', + state: 'failed', + updatedAt: '2026-07-20T10:04:00Z', + }), + ], + }, + ], + } + rendered.rerender() + await act(async () => vi.advanceTimersByTime(5000)) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('does not repoll the same failed snapshot after document read recovery', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'forbidden-failed-poll', state: 'failed' })] }], + } + getTaskSnapshot.mockRejectedValue(new Response(null, { status: 403 })) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + tasksQuery.data = { + pages: [{ items: [task({ id: 'forbidden-failed-poll', state: 'failed' })] }], + } + tasksQuery.dataUpdateCount += 1 + act(() => notifyTaskQuerySuccess()) + await act(async () => vi.advanceTimersByTime(60000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('accepts a same-version active list lifecycle after a failed poll is denied', async () => { + vi.useFakeTimers() + const taskVersion = '2026-07-20T10:01:00Z' + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.dataUpdateCount = 1 + tasksQuery.data = { + pages: [ + { + items: [ + task({ id: 'denied-failed-poll-retry', state: 'failed', updatedAt: taskVersion }), + ], + }, + ], + } + getTaskSnapshot.mockRejectedValueOnce(new Response(null, { status: 403 })) + streamProcessingTaskEvents.mockImplementation(async function* () { + await new Promise(() => {}) + }) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ id: 'denied-failed-poll-retry', state: 'running', updatedAt: taskVersion }), + ], + }, + ], + } + tasksQuery.dataUpdateCount = 2 + act(() => notifyTaskQuerySuccess()) + await act(async () => {}) + + expect(streamProcessingTaskEvents).toHaveBeenCalledOnce() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('advances failed-task polling when a snapshot request never settles', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: Array.from({ length: 7 }, (_, index) => + task({ id: `failed-${index}`, state: 'failed' }), + ), + }, + ], + } + getTaskSnapshot.mockImplementation((input: { params: { taskId: string } }) => { + if (input.params.taskId !== 'failed-0') return new Promise(() => {}) + return Promise.resolve( + task({ + id: 'failed-0', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:02:00Z', + }), + ) + }) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledTimes(6) + await act(async () => vi.advanceTimersByTime(3000)) + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledTimes(12) + expect(getTaskSnapshot).toHaveBeenCalledWith( + expect.objectContaining({ params: expect.objectContaining({ taskId: 'failed-0' }) }), + expect.anything(), + ) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('forgets permanent poll failures after a task leaves the cursor result', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'removed-failed-task', state: 'failed' })] }], + } + getTaskSnapshot.mockRejectedValue(new Response(null, { status: 403 })) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + tasksQuery.data = { pages: [{ items: [] }] } + rendered.rerender() + await act(async () => {}) + tasksQuery.data = { + pages: [{ items: [task({ id: 'removed-failed-task', state: 'failed' })] }], + } + rendered.rerender() + await act(async () => vi.advanceTimersByTime(5000)) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('ignores an older active snapshot while polling a newer failed task', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ + errorCode: 'LATEST_FAILURE', + id: 'stale-active-poll', + state: 'failed', + updatedAt: '2026-07-20T10:03:00.123789Z', + }), + ], + }, + ], + } + getTaskSnapshot.mockResolvedValue( + task({ + id: 'stale-active-poll', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00.123456Z', + }), + ) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + expect(streamProcessingTaskEvents).not.toHaveBeenCalled() + fireEvent.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.getByText('LATEST_FAILURE')).toBeInTheDocument() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('accepts a terminal-only stream after an exact snapshot observes an external retry', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ + errorCode: 'OLD_FAILURE', + id: 'terminal-only-external-retry', + state: 'failed', + updatedAt: '2026-07-20T10:01:00Z', + }), + ], + }, + ], + } + getTaskSnapshot + .mockResolvedValueOnce( + task({ + id: 'terminal-only-external-retry', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + .mockResolvedValueOnce( + task({ + errorCode: 'NEW_FAILURE', + id: 'terminal-only-external-retry', + state: 'failed', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { errorCode: 'NEW_FAILURE', state: 'failed' as const }, + event: 'terminal' as const, + id: 'terminal-only-external-retry:terminal', + } + }) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + await act(async () => {}) + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + fireEvent.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(screen.getByText('NEW_FAILURE')).toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('unblocks failed-task polling after an active list snapshot without a terminal pin', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'list-retry-after-block', state: 'failed' })] }], + } + getTaskSnapshot + .mockRejectedValueOnce(new Response(null, { status: 403 })) + .mockResolvedValueOnce( + task({ + id: 'list-retry-after-block', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:05:00Z', + }), + ) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'list-retry-after-block', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ], + }, + ], + } + rendered.rerender() + await act(async () => {}) + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'list-retry-after-block', + state: 'failed', + updatedAt: '2026-07-20T10:04:00Z', + }), + ], + }, + ], + } + rendered.rerender() + await act(async () => vi.advanceTimersByTime(5000)) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('retries exact polling when a permanently blocked failed task advances version', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'new-failed-generation', + state: 'failed', + updatedAt: '2026-07-20T10:01:00Z', + }), + ], + }, + ], + } + getTaskSnapshot + .mockRejectedValueOnce(new Response(null, { status: 403 })) + .mockResolvedValueOnce( + task({ + id: 'new-failed-generation', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:03:00Z', + }), + ) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + tasksQuery.data = { + pages: [ + { + items: [ + task({ + id: 'new-failed-generation', + state: 'failed', + updatedAt: '2026-07-20T10:02:00Z', + }), + ], + }, + ], + } + rendered.rerender() + await act(async () => vi.advanceTimersByTime(5000)) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('aborts an in-flight failed-task poll on unmount', async () => { + vi.useFakeTimers() + let pollSignal: AbortSignal | undefined + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [{ items: [task({ id: 'aborted-failed-poll', state: 'failed' })] }], + } + getTaskSnapshot.mockImplementation( + (_input: unknown, options: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + pollSignal = options.signal + options.signal.addEventListener( + 'abort', + () => reject(new DOMException('Aborted', 'AbortError')), + { once: true }, + ) + }), + ) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(pollSignal?.aborted).toBe(false) + rendered.unmount() + expect(pollSignal?.aborted).toBe(true) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('renders large document results in bounded batches', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [ + { + items: Array.from({ length: 150 }, (_, index) => + document({ id: `document-${index}`, title: `Document ${index}` }), + ), + }, + ], + } + + render() + + expect(screen.getAllByRole('row')).toHaveLength(101) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.loadMore' })) + expect(screen.getAllByRole('row')).toHaveLength(151) + expect(screen.getByRole('table').parentElement).toHaveFocus() + }) + + it('eagerly exhausts task and source cursor pages for accurate labels', () => { + documentsQuery.data = { + pages: [{ items: [document({ sourceId: 'source-on-later-page' })] }], + } + tasksQuery.data = { pages: [{ items: [], nextCursor: 'next' }] } + tasksQuery.hasNextPage = true + sourcesQuery.data = { pages: [{ items: [], nextCursor: 'source-next' }] } + sourcesQuery.hasNextPage = true + + render() + + expect(tasksQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(sourcesQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('keeps filtered results partial while task-dependent statuses have more pages', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: Array.from({ length: 20 }, () => ({ items: [], nextCursor: 'next' })), + } + tasksQuery.hasNextPage = true + + render() + await user.selectOptions(screen.getByRole('combobox'), 'failed') + + expect(screen.queryByText('dataset.newKnowledge.noMatchingDocuments')).not.toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.partialDocumentResults')).toBeInTheDocument() + }) + + it('keeps source-name searches partial while unresolved sources have more pages', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: [ + { + items: [document({ sourceId: 'later-source', userMetadata: {} })], + }, + ], + } + sourcesQuery.data = { + pages: Array.from({ length: 20 }, () => ({ items: [], nextCursor: 'next' })), + } + sourcesQuery.hasNextPage = true + + render() + await user.type( + screen.getByRole('searchbox', { name: 'dataset.newKnowledge.searchDocuments' }), + 'source title from next page', + ) + + expect(screen.queryByText('dataset.newKnowledge.noMatchingDocuments')).not.toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.partialDocumentResults')).toBeInTheDocument() + }) + + it('bounds automatic cursor exhaustion and leaves all further loading explicit', async () => { + const user = userEvent.setup() + documentsQuery.data = { + pages: Array.from({ length: 20 }, (_, index) => ({ + items: index === 0 ? [document({ sourceId: 'source-on-later-page' })] : [], + nextCursor: 'next', + })), + } + documentsQuery.hasNextPage = true + tasksQuery.data = { + pages: Array.from({ length: 20 }, () => ({ items: [], nextCursor: 'next' })), + } + tasksQuery.hasNextPage = true + sourcesQuery.data = { + pages: Array.from({ length: 20 }, (_, index) => ({ + items: index === 0 ? [source()] : [], + nextCursor: 'next', + })), + } + sourcesQuery.hasNextPage = true + + render() + await user.type( + screen.getByRole('searchbox', { name: 'dataset.newKnowledge.searchDocuments' }), + 'sso', + ) + + expect(documentsQuery.fetchNextPage).not.toHaveBeenCalled() + expect(screen.getByText('dataset.newKnowledge.partialDocumentResults')).toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-describedby', + 'partial-document-results', + ) + const loadMore = screen.getByRole('button', { name: 'dataset.newKnowledge.loadMore' }) + expect(loadMore).toBeInTheDocument() + await user.clear(screen.getByRole('searchbox')) + await user.type(screen.getByRole('searchbox'), 'missing') + expect(screen.queryByText('dataset.newKnowledge.noMatchingDocuments')).not.toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.partialDocumentResults')).toBeInTheDocument() + await user.click(loadMore) + expect(documentsQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(tasksQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(sourcesQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('keeps explicit result pagination busy while a dependency page is loading', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: Array.from({ length: 20 }, () => ({ items: [], nextCursor: 'next' })), + } + tasksQuery.hasNextPage = true + tasksQuery.isFetchingNextPage = true + + render() + + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.loadMore' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + }) + + it('keeps known documents actionable while older task and source pages remain', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: Array.from({ length: 20 }, () => ({ items: [], nextCursor: 'next' })), + } + tasksQuery.hasNextPage = true + sourcesQuery.data = { + pages: Array.from({ length: 20 }, (_, index) => ({ + items: index === 0 ? [source()] : [], + nextCursor: 'next', + })), + } + sourcesQuery.hasNextPage = true + + render() + + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).not.toHaveAttribute( + 'aria-disabled', + ) + expect( + within(screen.getByRole('row', { name: /sso-enterprise\.pdf/ })).getByText( + 'dataset.newKnowledge.documentStatus.ready', + ), + ).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.loadMore' }), + ).toBeInTheDocument() + }) + + it('locks cached documents when the task query returns a permission denial', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + tasksQuery.error = { status: 403 } + + render() + + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.documentsPermissionTitle', + ) + expect(screen.queryByText('sso-enterprise.pdf')).not.toBeInTheDocument() + expect(screen.queryByLabelText('dataset.newKnowledge.uploadDocuments')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}' }), + ).not.toBeInTheDocument() + }) + + it('retires a failed task action before background invalidation settles', async () => { + const user = userEvent.setup() + retryMutation.mutateAsync.mockRejectedValueOnce(new Error('retry failed')) + queryClient.invalidateQueries.mockImplementation(() => new Promise(() => {})) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task({ id: 'failed', state: 'failed' })] }] } + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + expect(await screen.findByText('dataset.newKnowledge.taskActionFailed')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })).toHaveAttribute( + 'aria-busy', + 'false', + ) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).not.toHaveAttribute('aria-disabled', 'true') + }) + + it('trusts same-version stream progress after a local retry', async () => { + const user = userEvent.setup() + const retryVersion = '2026-07-20T10:01:00Z' + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'same-version-retry', state: 'failed', updatedAt: retryVersion })], + }, + ], + } + retryMutation.mutateAsync.mockResolvedValue( + task({ id: 'same-version-retry', state: 'queued', updatedAt: retryVersion }), + ) + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 20, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: retryVersion, + }, + event: 'progress' as const, + id: 'same-version-retry:running', + } + await new Promise(() => {}) + }) + + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + + await waitFor(() => expect(streamProcessingTaskEvents).toHaveBeenCalled()) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'dataset.newKnowledge.retryTask' })).toBeNull() + }) + + it('retries a permission-blocked failed-task poll after permission returns', async () => { + vi.useFakeTimers() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'permission-blocked-poll', state: 'failed' })], + }, + ], + } + getTaskSnapshot + .mockRejectedValueOnce(new Response(null, { status: 403 })) + .mockResolvedValueOnce( + task({ + id: 'permission-blocked-poll', + state: 'dispatch_pending', + updatedAt: '2026-07-20T10:02:00Z', + }), + ) + + const rendered = render() + try { + await act(async () => vi.advanceTimersByTime(5000)) + expect(getTaskSnapshot).toHaveBeenCalledOnce() + + documentsQuery.error = { status: 403 } + rendered.rerender() + documentsQuery.error = null + rendered.rerender() + await act(async () => vi.advanceTimersByTime(5000)) + + expect(getTaskSnapshot).toHaveBeenCalledTimes(2) + } finally { + rendered.unmount() + vi.useRealTimers() + } + }) + + it('restores document focus when a focused bulk toolbar is cleared', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + const rendered = render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.clearDocumentSelection' }), + ) + + await waitFor(() => + expect(screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' })).toHaveFocus(), + ) + rendered.unmount() + }) + + it('announces upload and re-index operations as busy', async () => { + const user = userEvent.setup() + uploadMutation.mutateAsync.mockImplementation(() => new Promise(() => {})) + const emptyPage = render() + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadDocuments'), + new File(['one'], 'one.md', { type: 'text/markdown' }), + ) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' }), + ).toHaveAttribute('aria-busy', 'true') + emptyPage.unmount() + + reindexMutation.mutateAsync.mockImplementation(() => new Promise(() => {})) + documentsQuery.data = { pages: [{ items: [document()] }] } + render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' })) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' }), + ).toHaveAttribute('aria-busy', 'true') + }) + + it('locks uploads after a write mutation reveals revoked permission', async () => { + const user = userEvent.setup() + uploadMutation.mutateAsync.mockRejectedValueOnce(new Response(null, { status: 403 })) + permissionStateMock.refreshAfterDenial.mockResolvedValueOnce({ + data: { dataset: { default_permission_keys: ['dataset.acl.readonly'] } }, + error: null, + }) + const rendered = render() + const addDocument = screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' }) + await user.click(addDocument) + expect(addDocument).toHaveFocus() + + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadDocuments'), + new File(['one'], 'one.md', { type: 'text/markdown' }), + ) + + await waitFor(() => expect(permissionStateMock.refreshAfterDenial).toHaveBeenCalledOnce()) + expect(toastMock.error).not.toHaveBeenCalledWith('dataset.newKnowledge.documentUploadFailed') + const restriction = screen.getByText('dataset.newKnowledge.documentPermissionRestricted') + expect(restriction).toBeInTheDocument() + expect(restriction).toHaveAttribute('role', 'status') + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' })).toBeDisabled() + expect(screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' })).toHaveFocus() + + permissionStateMock.datasetKeys = ['dataset.acl.readonly'] + permissionStateMock.fetching = true + rendered.rerender() + permissionStateMock.fetching = false + rendered.rerender() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' })).toBeDisabled() + + permissionStateMock.datasetKeys = ['dataset.acl.edit'] + permissionStateMock.fetching = true + rendered.rerender() + permissionStateMock.fetching = false + rendered.rerender() + await waitFor(() => + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' }), + ).toBeEnabled(), + ) + }) + + it('preserves a newer focus target when an upload later loses write permission', async () => { + const user = userEvent.setup() + let rejectUpload!: (error: unknown) => void + uploadMutation.mutateAsync.mockImplementation( + () => + new Promise((_resolve, reject) => { + rejectUpload = reject + }), + ) + permissionStateMock.refreshAfterDenial.mockResolvedValueOnce({ + data: { dataset: { default_permission_keys: ['dataset.acl.readonly'] } }, + error: null, + }) + tasksQuery.data = { pages: [{ items: [task()] }] } + render() + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addDocument' })) + await user.upload( + screen.getByLabelText('dataset.newKnowledge.uploadDocuments'), + new File(['one'], 'one.md', { type: 'text/markdown' }), + ) + const tasksButton = screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }) + tasksButton.focus() + expect(tasksButton).toHaveFocus() + + await act(async () => rejectUpload(new Response(null, { status: 403 }))) + await waitFor(() => expect(permissionStateMock.refreshAfterDenial).toHaveBeenCalledOnce()) + + expect(tasksButton).toHaveFocus() + }) + + it('locks re-indexing after a write mutation reveals revoked permission', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + reindexMutation.mutateAsync.mockRejectedValueOnce(new Response(null, { status: 403 })) + permissionStateMock.refreshAfterDenial.mockResolvedValueOnce({ + data: { dataset: { default_permission_keys: ['dataset.acl.readonly'] } }, + error: null, + }) + render() + await user.click(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocuments' })) + + await waitFor(() => expect(permissionStateMock.refreshAfterDenial).toHaveBeenCalledOnce()) + expect(toastMock.error).not.toHaveBeenCalledWith('dataset.newKnowledge.documentsReindexFailed') + expect( + screen.queryByRole('group', { name: 'dataset.newKnowledge.bulkDocumentActions' }), + ).not.toBeInTheDocument() + expect(screen.getByRole('checkbox', { name: 'sso-enterprise.pdf' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(screen.getByRole('heading', { name: 'dataset.newKnowledge.documents' })).toHaveFocus() + }) + + it('locks task actions after a write mutation reveals revoked permission', async () => { + const user = userEvent.setup() + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + cancelMutation.mutateAsync.mockRejectedValueOnce(new Response(null, { status: 403 })) + permissionStateMock.refreshAfterDenial.mockResolvedValueOnce({ + data: { dataset: { default_permission_keys: ['dataset.acl.readonly'] } }, + error: null, + }) + const rendered = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + + await waitFor(() => expect(permissionStateMock.refreshAfterDenial).toHaveBeenCalledOnce()) + expect( + within(screen.getByRole('dialog')).getByText( + 'dataset.newKnowledge.documentPermissionRestricted', + ), + ).toBeInTheDocument() + expect(screen.queryByText('dataset.newKnowledge.taskActionFailed')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.close' })).toHaveFocus() + + permissionStateMock.error = new Error('permission refresh failed') + rendered.rerender() + const dialog = screen.getByRole('dialog') + expect( + within(dialog).getByText('dataset.newKnowledge.documentPermissionRestricted'), + ).toBeInTheDocument() + expect(within(dialog).getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.permissionLoadFailed', + ) + }) + + it('does not let an older permission refresh release the latest write lock', async () => { + const user = userEvent.setup() + let rejectUpload: ((reason: unknown) => void) | undefined + let rejectCancel: ((reason: unknown) => void) | undefined + let resolveFirstRefresh: ((value: unknown) => void) | undefined + let resolveSecondRefresh: ((value: unknown) => void) | undefined + uploadMutation.mutateAsync.mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + rejectUpload = reject + }), + ) + cancelMutation.mutateAsync.mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + rejectCancel = reject + }), + ) + permissionStateMock.refreshAfterDenial + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirstRefresh = resolve + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecondRefresh = resolve + }), + ) + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { pages: [{ items: [task()] }] } + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.interruptTask' })) + fireEvent.change(screen.getByLabelText('dataset.newKnowledge.uploadDocuments'), { + target: { files: [new File(['one'], 'one.md', { type: 'text/markdown' })] }, + }) + expect(cancelMutation.mutateAsync).toHaveBeenCalledOnce() + expect(uploadMutation.mutateAsync).toHaveBeenCalledOnce() + + await act(async () => rejectUpload?.(new Response(null, { status: 403 }))) + await waitFor(() => expect(permissionStateMock.refreshAfterDenial).toHaveBeenCalledOnce()) + await act(async () => rejectCancel?.(new Response(null, { status: 403 }))) + await waitFor(() => expect(permissionStateMock.refreshAfterDenial).toHaveBeenCalledTimes(2)) + + await act(async () => + resolveFirstRefresh?.({ + data: { dataset: { default_permission_keys: ['dataset.acl.edit'] } }, + error: null, + }), + ) + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + + await act(async () => + resolveSecondRefresh?.({ + data: { dataset: { default_permission_keys: ['dataset.acl.readonly'] } }, + error: null, + }), + ) + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).not.toBeInTheDocument() + }) + + it('observes a structurally shared same-version retry when response timestamps collide', async () => { + const user = userEvent.setup() + const taskVersion = '2026-07-20T10:03:00Z' + const sharedTaskData = { + pages: [{ items: [task({ id: 'shared-active-retry', updatedAt: taskVersion })] }], + } + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = sharedTaskData + tasksQuery.dataUpdatedAt = 100 + tasksQuery.dataUpdateCount = 1 + getTaskSnapshot + .mockRejectedValueOnce(new Error('snapshot unavailable')) + .mockResolvedValueOnce( + task({ id: 'shared-active-retry', state: 'running', updatedAt: taskVersion }), + ) + streamFailedTaskThenWait('shared-active-retry') + render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect(await screen.findByText('PARSER_FAILED')).toBeInTheDocument() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + + tasksQuery.data = sharedTaskData + tasksQuery.dataUpdatedAt = 100 + tasksQuery.dataUpdateCount = 2 + act(() => notifyTaskQuerySuccess()) + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(2)) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + }) + + it('rechecks a same-version active lifecycle after a denied terminal snapshot', async () => { + const user = userEvent.setup() + const taskVersion = '2026-07-20T10:03:00Z' + const sharedTaskData = { + pages: [{ items: [task({ id: 'denied-active-retry', updatedAt: taskVersion })] }], + } + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = sharedTaskData + tasksQuery.dataUpdatedAt = 100 + tasksQuery.dataUpdateCount = 1 + getTaskSnapshot + .mockRejectedValueOnce(new Response(null, { status: 403 })) + .mockResolvedValueOnce( + task({ id: 'denied-active-retry', state: 'running', updatedAt: taskVersion }), + ) + streamFailedTaskThenWait('denied-active-retry') + render() + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledOnce()) + + tasksQuery.data = sharedTaskData + tasksQuery.dataUpdatedAt = 100 + tasksQuery.dataUpdateCount = 2 + act(() => notifyTaskQuerySuccess()) + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalledTimes(2)) + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + }) + + it('reconciles a same-version terminal list after trusted local retry progress', async () => { + const user = userEvent.setup() + const taskVersion = '2026-07-20T10:01:00Z' + documentsQuery.data = { pages: [{ items: [document()] }] } + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'trusted-terminal', state: 'failed', updatedAt: taskVersion })], + }, + ], + } + retryMutation.mutateAsync.mockResolvedValue( + task({ id: 'trusted-terminal', state: 'queued', updatedAt: taskVersion }), + ) + getTaskSnapshot.mockResolvedValue( + task({ + errorCode: 'TERMINAL_CONFIRMED', + id: 'trusted-terminal', + state: 'failed', + updatedAt: taskVersion, + }), + ) + streamProcessingTaskEvents.mockImplementation(async function* () { + yield { + data: { + progressPercent: 30, + stage: 'parsed' as const, + state: 'running' as const, + updatedAt: taskVersion, + }, + event: 'progress' as const, + id: 'trusted-terminal:running', + } + await new Promise(() => {}) + }) + const rendered = render() + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.tasksWithAttention:{"count":1}', + }), + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' })) + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.interruptTask' }), + ).toBeInTheDocument() + + tasksQuery.data = { + pages: [ + { + items: [task({ id: 'trusted-terminal', state: 'failed', updatedAt: taskVersion })], + }, + ], + } + tasksQuery.dataUpdatedAt = 100 + rendered.rerender() + + await waitFor(() => expect(getTaskSnapshot).toHaveBeenCalled()) + expect(await screen.findByText('TERMINAL_CONFIRMED')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.retryTask' }), + ).toBeInTheDocument() + }) +}) diff --git a/web/features/new-rag/__tests__/processing-task-events.spec.ts b/web/features/new-rag/__tests__/processing-task-events.spec.ts new file mode 100644 index 00000000000..f871acc9c76 --- /dev/null +++ b/web/features/new-rag/__tests__/processing-task-events.spec.ts @@ -0,0 +1,115 @@ +import type { DocumentProcessingTaskEvent } from '@dify/contracts/knowledge-fs/types.gen' +import { withEventMeta } from '@orpc/client' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { streamProcessingTaskEvents } from '../services/processing-task-events' + +const { mockStreamEvents } = vi.hoisted(() => ({ + mockStreamEvents: vi.fn(), +})) + +vi.mock('@/service/client', () => ({ + consoleClient: { + knowledgeFs: { + getKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskIdEvents: mockStreamEvents, + }, + }, +})) + +async function* eventIterator(...events: DocumentProcessingTaskEvent[]) { + yield* events +} + +describe('KnowledgeFS processing task events', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('uses the generated streaming client and resumes from the last event id', async () => { + mockStreamEvents.mockResolvedValue( + eventIterator( + withEventMeta( + { + data: { + progressPercent: 45, + stage: 'parsed', + state: 'running', + updatedAt: '2026-07-20T01:02:03Z', + }, + event: 'progress', + }, + { id: 'task-1:2026-07-20T01:02:03Z' }, + ), + withEventMeta( + { + data: { state: 'succeeded' }, + event: 'terminal', + }, + { id: 'task-1:terminal' }, + ), + ), + ) + const abortController = new AbortController() + + const events = [] + for await (const event of streamProcessingTaskEvents({ + documentId: 'document/1', + knowledgeSpaceId: 'space/1', + lastEventId: 'task-1:previous', + signal: abortController.signal, + taskId: 'task/1', + })) { + events.push(event) + } + + expect(events).toEqual([ + { + data: { + progressPercent: 45, + stage: 'parsed', + state: 'running', + updatedAt: '2026-07-20T01:02:03Z', + }, + event: 'progress', + id: 'task-1:2026-07-20T01:02:03Z', + }, + { + data: { state: 'succeeded' }, + event: 'terminal', + id: 'task-1:terminal', + }, + ]) + expect(mockStreamEvents).toHaveBeenCalledWith( + { + headers: { 'last-event-id': 'task-1:previous' }, + params: { + documentId: 'document/1', + id: 'space/1', + taskId: 'task/1', + }, + }, + { + context: { silent: true }, + signal: abortController.signal, + }, + ) + }) + + it('rejects events without a resumable event id', async () => { + mockStreamEvents.mockResolvedValue( + eventIterator({ + data: { state: 'failed' }, + event: 'terminal', + }), + ) + + await expect(async () => { + for await (const event of streamProcessingTaskEvents({ + documentId: 'document-1', + knowledgeSpaceId: 'space-1', + taskId: 'task-1', + })) { + void event + } + }).rejects.toThrow('missing an event id') + }) +}) diff --git a/web/features/new-rag/__tests__/use-query-data-update-count.spec.tsx b/web/features/new-rag/__tests__/use-query-data-update-count.spec.tsx new file mode 100644 index 00000000000..9d045d98bcb --- /dev/null +++ b/web/features/new-rag/__tests__/use-query-data-update-count.spec.tsx @@ -0,0 +1,50 @@ +import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query' +import { act, render, screen, waitFor } from '@testing-library/react' +import { useQueryDataUpdateCount } from '../use-query-data-update-count' + +const queryKey = ['task-generation'] as const +const sharedPage = { items: ['task-1'] } + +function GenerationProbe({ queryClient }: { queryClient: QueryClient }) { + const query = useInfiniteQuery({ + queryFn: async () => sharedPage, + initialPageParam: null, + getNextPageParam: () => undefined, + queryKey, + }) + const dataUpdateCount = useQueryDataUpdateCount(queryClient, queryKey) + + return ( + + {query.data ? 'ready' : 'pending'}:{query.dataUpdatedAt}:{dataUpdateCount} + + ) +} + +describe('useQueryDataUpdateCount', () => { + it('observes a real same-reference refetch when response timestamps collide', async () => { + vi.spyOn(Date, 'now').mockReturnValue(100) + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: Infinity } }, + }) + const rendered = render( + + + , + ) + + try { + await screen.findByText('ready:100:1') + const firstData = queryClient.getQueryData(queryKey) + + await act(async () => queryClient.refetchQueries({ queryKey })) + + await waitFor(() => expect(screen.getByText('ready:100:2')).toBeInTheDocument()) + expect(queryClient.getQueryData(queryKey)).toBe(firstData) + } finally { + rendered.unmount() + queryClient.clear() + vi.restoreAllMocks() + } + }) +}) diff --git a/web/features/new-rag/auxiliary-task-read-guard.ts b/web/features/new-rag/auxiliary-task-read-guard.ts new file mode 100644 index 00000000000..07cda35dc4d --- /dev/null +++ b/web/features/new-rag/auxiliary-task-read-guard.ts @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { taskVersionIsAfter } from './document-model' + +export type AuxiliaryTaskReadGuard = ReturnType + +type RefetchDocuments = (options: { cancelRefetch: true }) => Promise<{ error: unknown }> + +export function createAuxiliaryTaskReadGuard() { + const blockedVersions = new Map() + + return { + block(taskId: string, taskVersion: string) { + const blockedVersion = blockedVersions.get(taskId) + if (blockedVersion && taskVersionIsAfter(blockedVersion, taskVersion)) return + blockedVersions.set(taskId, taskVersion) + }, + clear() { + blockedVersions.clear() + }, + clearTask(taskId: string) { + blockedVersions.delete(taskId) + }, + clearThrough(taskId: string, authoritativeVersion: string) { + const blockedVersion = blockedVersions.get(taskId) + if (!blockedVersion || taskVersionIsAfter(blockedVersion, authoritativeVersion)) return false + blockedVersions.delete(taskId) + return true + }, + isBlocked(taskId: string, taskVersion: string) { + const blockedVersion = blockedVersions.get(taskId) + return Boolean(blockedVersion && !taskVersionIsAfter(taskVersion, blockedVersion)) + }, + retain(taskIds: Set) { + for (const taskId of blockedVersions.keys()) { + if (!taskIds.has(taskId)) blockedVersions.delete(taskId) + } + }, + } +} + +export function useAuxiliaryTaskReadGuard({ + documentPermissionDenied, + refetchDocuments, +}: { + documentPermissionDenied: boolean + refetchDocuments: RefetchDocuments +}) { + const guardRef = useRef(null) + if (!guardRef.current) guardRef.current = createAuxiliaryTaskReadGuard() + const guard = guardRef.current + const previousDocumentPermissionDeniedRef = useRef(documentPermissionDenied) + const denialGenerationRef = useRef(0) + const [permissionDenied, setPermissionDenied] = useState(false) + const [guardRevision, setGuardRevision] = useState(0) + + useEffect(() => { + const wasDenied = previousDocumentPermissionDeniedRef.current + previousDocumentPermissionDeniedRef.current = documentPermissionDenied + if (!wasDenied || documentPermissionDenied) return + guard.clear() + // oxlint-disable-next-line eslint-react/set-state-in-effect -- The authoritative document permission transition retires the local denial. + setPermissionDenied(false) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Guard mutations need one render so blocked readers are reconsidered. + setGuardRevision((current) => current + 1) + }, [documentPermissionDenied, guard]) + + const retry = useCallback(() => { + const denialGeneration = denialGenerationRef.current + 1 + denialGenerationRef.current = denialGeneration + setPermissionDenied(true) + void refetchDocuments({ cancelRefetch: true }).then( + (result) => { + if (denialGenerationRef.current === denialGeneration && !result.error) + setPermissionDenied(false) + }, + () => undefined, + ) + }, [refetchDocuments]) + + const deny = useCallback( + (taskId: string, taskVersion: string) => { + guard.block(taskId, taskVersion) + retry() + }, + [guard, retry], + ) + + return { deny, guard, guardRevision, permissionDenied, retry } +} diff --git a/web/features/new-rag/create-knowledge-page.tsx b/web/features/new-rag/create-knowledge-page.tsx index c12777b8831..e8f290789e1 100644 --- a/web/features/new-rag/create-knowledge-page.tsx +++ b/web/features/new-rag/create-knowledge-page.tsx @@ -3,6 +3,7 @@ import type { KnowledgeSpaceCreationResponse } from '@dify/contracts/knowledge-fs/types.gen' import type { CreateKnowledgeExitReason } from './components/create-knowledge-exit-dialog' import type { KnowledgeVisibility } from './create-knowledge-workflow' +import type { QueuedUpload } from './create-upload-queue' import type { NewKnowledgeSourceDraft, NewKnowledgeStartMode } from './routes' import { Button } from '@langgenius/dify-ui/button' import { @@ -38,7 +39,7 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useRouter, useSearchParams } from '@/next/navigation' -import { consoleQuery } from '@/service/client' +import { consoleClient, consoleQuery } from '@/service/client' import { DatasetACLPermission, hasPermission } from '@/utils/permission' import { KnowledgeIllustration, StartMode } from './components/create-knowledge-dialog-parts' import { CreateKnowledgeExitDialog } from './components/create-knowledge-exit-dialog' @@ -50,18 +51,38 @@ import { NAME_MAX_LENGTH, } from './create-knowledge-workflow' import { CreateSourceSetup } from './create-source-setup' +import { CreateUploadQueue } from './create-upload-queue' import { createRequestId } from './request-id' import { createNewKnowledgeSourceDraft, isValidWebsiteSourceDraft, newKnowledgeAddSourcePath, newKnowledgeDetailPath, + newKnowledgeDocumentsPath, newKnowledgeListPath, newKnowledgeSourceDraftStorageKey, } from './routes' function normalizeStartMode(value: string | null): NewKnowledgeStartMode { - return value === 'source' ? value : 'empty' + if (value === 'source' || value === 'upload') return value + return 'empty' +} + +async function uploadCreatedDocuments(knowledgeSpaceId: string, files: File[]) { + if (files.length === 1) { + await consoleClient.knowledgeFs.postKnowledgeSpacesByIdDocuments({ + body: { file: files[0]! }, + params: { id: knowledgeSpaceId }, + }) + return + } + + const result = await consoleClient.knowledgeFs.postKnowledgeSpacesByIdDocumentsBulk({ + body: { files }, + params: { id: knowledgeSpaceId }, + }) + if (!result.accepted) throw new Error('No files were accepted') + return result } export function CreateKnowledgePage() { @@ -89,14 +110,20 @@ export function CreateKnowledgePage() { const sourceDraftsRef = useRef< Partial> >({}) + const [uploads, setUploads] = useState([]) const [createdKnowledge, setCreatedKnowledge] = useState() const [submissionLocked, setSubmissionLocked] = useState(false) + const [uploading, setUploading] = useState(false) + const [uploadError, setUploadError] = useState(false) const [exitReason, setExitReason] = useState(null) const idempotencyKeyRef = useRef(undefined) const historyGuardArmedRef = useRef(false) const browserBackExitRef = useRef(false) const pendingNavigationRef = useRef(undefined) const createMutation = useMutation({ mutationFn: createKnowledge }) + const submissionPending = createMutation.isPending || uploading + const uploadSubmissionBlocked = + startMode === 'upload' && (!uploads.length || uploads.some((upload) => upload.issue)) const sourceSubmissionBlocked = startMode === 'source' && (sourceDraft.sourceType === 'websiteCrawl' @@ -115,6 +142,7 @@ export function CreateKnowledgePage() { visibility !== defaultVisibility || startMode !== initialStartMode || sourceDraftChanged || + uploads.length || createdKnowledge, ) @@ -186,10 +214,11 @@ export function CreateKnowledgePage() { const resetUnsubmittedError = () => { if (!submissionLocked) createMutation.reset() + setUploadError(false) } const requestClose = () => { - if (createMutation.isPending) return + if (submissionPending) return if (createdKnowledge) { setExitReason('partial') return @@ -222,7 +251,7 @@ export function CreateKnowledgePage() { } const handleSubmit = async () => { - if (createMutation.isPending || sourceSubmissionBlocked) return + if (submissionPending || uploadSubmissionBlocked || sourceSubmissionBlocked) return const normalizedName = name.trim() const normalizedDescription = description.trim() @@ -244,6 +273,33 @@ export function CreateKnowledgePage() { }, visibility, }) + if (startMode === 'upload') { + setUploading(true) + setUploadError(false) + try { + const result = await uploadCreatedDocuments( + created.id, + uploads.map((upload) => upload.file), + ) + if (result?.excluded) + toast.warning( + t(($) => $['newKnowledge.documentUploadPartial'], { + accepted: result.accepted, + details: result.items + .filter((item) => 'reason' in item) + .map((item) => item.filename) + .join(', '), + excluded: result.excluded, + }), + ) + } catch { + setUploadError(true) + return + } finally { + setUploading(false) + } + } + if (startMode === 'source') { try { const sourceDraftKey = createRequestId() @@ -259,7 +315,12 @@ export function CreateKnowledgePage() { } return } - replaceAfterHistoryGuard(newKnowledgeDetailPath(created.id)) + + replaceAfterHistoryGuard( + startMode === 'upload' + ? newKnowledgeDocumentsPath(created.id) + : newKnowledgeDetailPath(created.id), + ) } catch (error) { if (error instanceof KnowledgeCreationError && error.createdKnowledge) setCreatedKnowledge(error.createdKnowledge) @@ -294,7 +355,7 @@ export function CreateKnowledgePage() { aria-label={tCommon(($) => $['operation.close'])} className="absolute top-3 right-3 z-10 flex size-9 items-center justify-center rounded-xl bg-background-section-burn text-text-tertiary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:text-text-disabled" onClick={requestClose} - disabled={createMutation.isPending} + disabled={submissionPending} > @@ -460,12 +521,22 @@ export function CreateKnowledgePage() { /> $['newKnowledge.uploadFiles'])} description={t(($) => $['newKnowledge.uploadFilesDescription'])} - /> + > + { + setUploads(value) + resetUnsubmittedError() + }} + /> + @@ -482,18 +553,26 @@ export function CreateKnowledgePage() { )} )} + {uploadError && ( +
+ {t(($) => $['newKnowledge.documentUploadFailed'])} +
+ )}
- diff --git a/web/features/new-rag/create-upload-queue.tsx b/web/features/new-rag/create-upload-queue.tsx new file mode 100644 index 00000000000..9a2523fc230 --- /dev/null +++ b/web/features/new-rag/create-upload-queue.tsx @@ -0,0 +1,213 @@ +'use client' + +import { cn } from '@langgenius/dify-ui/cn' +import { useId, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + DOCUMENT_UPLOAD_ACCEPT, + documentUploadFileExtension, + documentUploadIssue, +} from './document-upload-policy' +import { createRequestId } from './request-id' + +const PREVIEWABLE_EXTENSIONS = new Set(['doc', 'docx', 'html', 'markdown', 'md', 'pdf', 'txt']) + +export type QueuedUpload = { + file: File + id: string + issue?: 'fileSize' | 'fileType' +} + +function createQueuedUpload(file: File): QueuedUpload { + return { + file, + id: `${file.name}:${file.size}:${file.lastModified}:${createRequestId()}`, + issue: documentUploadIssue(file), + } +} + +function formatFileSize(bytes: number) { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +function fileBadge(file: File) { + return documentUploadFileExtension(file.name).toLocaleUpperCase() || 'FILE' +} + +function mergeFiles(current: QueuedUpload[], files: File[]) { + const fingerprints = new Set( + current.map(({ file }) => `${file.name}:${file.size}:${file.lastModified}`), + ) + return [ + ...current, + ...files.flatMap((file) => { + const fingerprint = `${file.name}:${file.size}:${file.lastModified}` + if (fingerprints.has(fingerprint)) return [] + fingerprints.add(fingerprint) + return [createQueuedUpload(file)] + }), + ] +} + +export function CreateUploadQueue({ + disabled, + uploading, + uploads, + onChange, +}: { + disabled: boolean + uploading: boolean + uploads: QueuedUpload[] + onChange: (uploads: QueuedUpload[]) => void +}) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const inputId = useId() + const [dragging, setDragging] = useState(false) + const previewUnavailable = uploads.some( + (upload) => + !upload.issue && + !uploading && + PREVIEWABLE_EXTENSIONS.has(documentUploadFileExtension(upload.file.name)), + ) + + const addFiles = (files: File[]) => { + if (!disabled && files.length) onChange(mergeFiles(uploads, files)) + } + + return ( +
+ $['newKnowledge.uploadFiles'])} + disabled={disabled} + onChange={(event) => { + addFiles([...(event.currentTarget.files ?? [])]) + event.currentTarget.value = '' + }} + /> + + + {!!uploads.length && ( +
    $['newKnowledge.uploadFiles'])}> + {uploads.map((upload) => ( +
  • + + {uploading && !upload.issue ? ( + + ) : ( + fileBadge(upload.file) + )} + + + + {upload.file.name} + + + {uploading && !upload.issue + ? t(($) => $['newKnowledge.uploadingFiles']) + : upload.issue === 'fileSize' + ? t(($) => $['newKnowledge.documentUploadExclusion.fileSize']) + : upload.issue === 'fileType' + ? t(($) => $['newKnowledge.documentUploadExclusion.fileType']) + : `${formatFileSize(upload.file.size)} · ${t(($) => $['newKnowledge.uploadCharactersUnavailable'])}`} + + + {!uploading && + !upload.issue && + PREVIEWABLE_EXTENSIONS.has(documentUploadFileExtension(upload.file.name)) && ( + + )} + +
  • + ))} +
+ )} + {previewUnavailable && ( +

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

+ )} +
+ ) +} diff --git a/web/features/new-rag/document-list.tsx b/web/features/new-rag/document-list.tsx new file mode 100644 index 00000000000..1192627963c --- /dev/null +++ b/web/features/new-rag/document-list.tsx @@ -0,0 +1,712 @@ +'use client' + +import type { LogicalDocument } from '@dify/contracts/knowledge-fs/types.gen' +import type { FocusEventHandler } from 'react' +import type { DocumentDisplayStatus } from './document-model' +import { Button } from '@langgenius/dify-ui/button' +import { Checkbox } from '@langgenius/dify-ui/checkbox' +import { cn } from '@langgenius/dify-ui/cn' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@langgenius/dify-ui/dropdown-menu' +import { toast } from '@langgenius/dify-ui/toast' +import { memo, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Loading from '@/app/components/base/loading' +import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' +import { sourceName } from './document-model' + +export type DocumentFilter = DocumentDisplayStatus | 'all' + +const DOCUMENT_RENDER_BATCH_SIZE = 100 +const PARTIAL_RESULTS_DESCRIPTION_ID = 'partial-document-results' + +const statusIconClass: Record = { + ready: 'i-ri-check-line text-text-success', + queued: 'i-ri-time-line text-text-tertiary', + processing: 'i-ri-loader-2-line animate-spin text-text-accent motion-reduce:animate-none', + failed: 'i-ri-error-warning-fill text-text-destructive', + disabled: 'i-ri-indeterminate-circle-line text-text-tertiary', +} + +function TaskTrigger({ + activeTaskCount, + attentionTaskBadge, + hasTaskError, + onOpenTasks, + tasksButtonLabel, + tasksLiveStatus, +}: { + activeTaskCount: number + attentionTaskBadge?: string + hasTaskError: boolean + onOpenTasks: () => void + tasksButtonLabel: string + tasksLiveStatus: string +}) { + const { t } = useTranslation('dataset') + return ( + <> + + + {tasksLiveStatus} + + + ) +} + +const DocumentRow = memo( + ({ + document, + formatTimeFromNow, + onSelectedChange, + readOnlyReasonId, + selected, + selectionDisabled, + source, + sourcePending, + status, + statusPending, + }: { + document: LogicalDocument + formatTimeFromNow: (time: number) => string + onSelectedChange: (documentId: string) => void + readOnlyReasonId?: string + selected: boolean + selectionDisabled: boolean + source?: string + sourcePending: boolean + status: DocumentDisplayStatus + statusPending: boolean + }) => { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const titleId = `new-document-${document.id}` + const revision = document.activeRevision ?? document.active?.revision + const updatedTime = Date.parse(document.updatedAt) + + return ( + + + onSelectedChange(document.id)} + /> + + +
+ + + {document.title} + + {revision !== undefined && ( + + v{revision} + + )} +
+ + + {sourcePending ? ( + + + {tCommon(($) => $.loading)} + + ) : ( + + {source ?? t(($) => $['newKnowledge.manualUpload'])} + + )} + + + {statusPending ? ( + + + {tCommon(($) => $.loading)} + + ) : ( + + + {t(($) => $[`newKnowledge.documentStatus.${status}`])} + + )} + + + {Number.isNaN(updatedTime) ? document.updatedAt : formatTimeFromNow(updatedTime)} + + + + $['newKnowledge.documentActions'], { name: document.title })} + className="flex size-7 items-center justify-center rounded-md text-text-tertiary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid" + > + + + + toast.info(t(($) => $['newKnowledge.documentActionsUnavailable']))} + > + + {t(($) => $['newKnowledge.downloadDocuments'])} + + + toast.info(t(($) => $['newKnowledge.documentActionsUnavailable']))} + > + + {t(($) => $['newKnowledge.deleteDocuments'])} + + + + + + ) + }, +) + +export function DocumentsEmpty({ + activeTaskCount, + attentionTaskBadge, + canEdit, + hasTaskError, + onAddDocument, + onDropFiles, + onOpenTasks, + readOnlyReasonId, + tasksButtonLabel, + tasksLiveStatus, + uploading, +}: { + activeTaskCount: number + attentionTaskBadge?: string + canEdit: boolean + hasTaskError: boolean + onAddDocument: () => void + onDropFiles: (files: File[]) => void + onOpenTasks: () => void + readOnlyReasonId?: string + tasksButtonLabel: string + tasksLiveStatus: string + uploading: boolean +}) { + const { t } = useTranslation('dataset') + const tasksVisible = activeTaskCount > 0 || Boolean(attentionTaskBadge) || hasTaskError + + return ( +
{ + event.preventDefault() + event.dataTransfer.dropEffect = canEdit ? 'copy' : 'none' + }} + onDrop={(event) => { + event.preventDefault() + if (canEdit) onDropFiles([...event.dataTransfer.files]) + }} + > + + + +

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

+

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

+ + {canEdit && ( +

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

+ )} + {tasksVisible && ( +
+ +
+ )} +
+ ) +} + +export function DocumentsList({ + activeTaskCount, + allSelected, + attentionTaskBadge, + canEdit, + completingResults, + documents, + filter, + hasNextPage, + hasSelectableDocuments, + hasTaskError, + isFetchNextPageError, + isFetchingNextDocumentPage, + isFetchingNextPage, + onAddDocument, + onFilterChange, + onLoadMore, + onOpenTasks, + onSearchChange, + onSelectAll, + onSelectDocument, + readOnlyReasonId, + resultsIncomplete, + search, + selectionDisabled, + selectedDocumentIds, + someSelected, + sourcesPending, + sourceNames, + statusPending, + statuses, + tasksPending, + tasksButtonLabel, + tasksLiveStatus, + uploading, +}: { + activeTaskCount: number + allSelected: boolean + attentionTaskBadge?: string + canEdit: boolean + completingResults: boolean + documents: LogicalDocument[] + filter: DocumentFilter + hasNextPage: boolean + hasSelectableDocuments: boolean + hasTaskError: boolean + isFetchNextPageError: boolean + isFetchingNextDocumentPage: boolean + isFetchingNextPage: boolean + onAddDocument: () => void + onFilterChange: (filter: DocumentFilter) => void + onLoadMore: () => void + onOpenTasks: () => void + onSearchChange: (search: string) => void + onSelectAll: () => void + onSelectDocument: (documentId: string) => void + readOnlyReasonId?: string + resultsIncomplete: boolean + search: string + selectionDisabled: boolean + selectedDocumentIds: Set + someSelected: boolean + sourcesPending: boolean + sourceNames: Map + statusPending: boolean + statuses: Map + tasksPending: boolean + tasksButtonLabel: string + tasksLiveStatus: string + uploading: boolean +}) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const { formatTimeFromNow } = useFormatTimeFromNow() + const [visibleDocumentLimit, setVisibleDocumentLimit] = useState(DOCUMENT_RENDER_BATCH_SIZE) + const loadMoreButtonRef = useRef(null) + const resultsContainerRef = useRef(null) + const restoreLoadMoreFocusRef = useRef(false) + const filterActive = filter !== 'all' || Boolean(search.trim()) + const visibleDocuments = documents.slice(0, visibleDocumentLimit) + const hasHiddenDocuments = visibleDocuments.length < documents.length + + useEffect(() => { + if (isFetchingNextPage || !restoreLoadMoreFocusRef.current) return + if (hasHiddenDocuments || hasNextPage || isFetchNextPageError) { + loadMoreButtonRef.current?.focus() + return + } + restoreLoadMoreFocusRef.current = false + resultsContainerRef.current?.focus() + }, [hasHiddenDocuments, hasNextPage, isFetchNextPageError, isFetchingNextPage]) + + return ( + <> +
+ + + + + + + +
+
+ + + + + + + + + + + + {visibleDocuments.map((document) => ( + + ))} + +
+ $['newKnowledge.selectAllDocuments'])} + onCheckedChange={onSelectAll} + /> + {t(($) => $['newKnowledge.documentColumn'])} + {t(($) => $['newKnowledge.sourceColumn'])} + {t(($) => $['newKnowledge.statusColumn'])} + {t(($) => $['newKnowledge.updatedColumn'])} + $['newKnowledge.actionsColumn'])} /> +
+ {!documents.length && !completingResults && !isFetchNextPageError && !resultsIncomplete && ( +

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

+ )} + {resultsIncomplete && ( +

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

+ )} + {completingResults && ( +
+ +
+ )} +
+

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

+ {hasHiddenDocuments ? ( +
+ +
+ ) : isFetchNextPageError ? ( +
+ + {t(($) => $['newKnowledge.documentsErrorDescription'])} + + +
+ ) : hasNextPage && (!filterActive || !completingResults) ? ( +
+ +
+ ) : null} + + ) +} + +export function DocumentBulkActions({ + disabled, + disabledReason, + onClear, + onBlurCapture, + onFocusCapture, + onReindex, + reindexing, + selectedCount, +}: { + disabled: boolean + disabledReason?: string + onClear: () => void + onBlurCapture: FocusEventHandler + onFocusCapture: FocusEventHandler + onReindex: () => void + reindexing: boolean + selectedCount: number +}) { + const { t } = useTranslation('dataset') + return ( +
+
$['newKnowledge.bulkDocumentActions'])} + className="pointer-events-auto flex max-w-full min-w-0 items-center gap-2 overflow-x-auto rounded-[14px] border border-divider-subtle bg-components-panel-bg px-3 py-2.5 shadow-xl" + role="group" + onBlurCapture={onBlurCapture} + onFocusCapture={onFocusCapture} + > + + + + {t(($) => $['newKnowledge.documentsSelected'], { count: selectedCount })} + + {disabled && disabledReason && ( + + {t(($) => $['newKnowledge.reindexDocuments'])} + {' · '} + {disabledReason} + + )} + + {t(($) => $['newKnowledge.downloadDocuments'])} + {' / '} + {t(($) => $['newKnowledge.deleteDocuments'])} + {' · '} + {t(($) => $['cornerLabel.unavailable'])} + + + +
+
+ ) +} diff --git a/web/features/new-rag/document-model.ts b/web/features/new-rag/document-model.ts new file mode 100644 index 00000000000..b1026e2d1c2 --- /dev/null +++ b/web/features/new-rag/document-model.ts @@ -0,0 +1,100 @@ +import type { + DocumentProcessingTask, + LogicalDocument, +} from '@dify/contracts/knowledge-fs/types.gen' + +export type DocumentDisplayStatus = 'ready' | 'queued' | 'processing' | 'failed' | 'disabled' + +export const ACTIVE_TASK_STATES = new Set([ + 'dispatch_pending', + 'queued', + 'running', + 'retry_wait', +]) + +const ATTENTION_TASK_STATES = new Set([ + ...ACTIVE_TASK_STATES, + 'failed', +]) + +function rfc3339Parts(value: string) { + const match = /^(.*:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$/.exec(value) + if (!match) return + const epochSecond = Date.parse(`${match[1]}${match[3]}`) + if (Number.isNaN(epochSecond)) return + return { epochSecond, fraction: match[2] ?? '' } +} + +export function taskVersionIsAfter(candidate: string, baseline: string) { + const candidateParts = rfc3339Parts(candidate) + const baselineParts = rfc3339Parts(baseline) + if (candidateParts && baselineParts) { + if (candidateParts.epochSecond !== baselineParts.epochSecond) + return candidateParts.epochSecond > baselineParts.epochSecond + const precision = Math.max(candidateParts.fraction.length, baselineParts.fraction.length) + return ( + candidateParts.fraction.padEnd(precision, '0') > baselineParts.fraction.padEnd(precision, '0') + ) + } + return candidate.localeCompare(baseline) > 0 +} + +export function sourceName(document: LogicalDocument) { + const value = document.userMetadata.sourceName + if (typeof value === 'string' && value.trim()) return value + return document.sourceId +} + +function documentDisabled(document: LogicalDocument, sourceDisabled = false) { + return document.status === 'deleting' || sourceDisabled +} + +export function newestTaskByDocument(tasks: DocumentProcessingTask[]) { + const result = new Map() + for (const task of tasks) { + const current = result.get(task.documentId) + if ( + !current || + task.documentRevision > current.documentRevision || + (task.documentRevision === current.documentRevision && + (taskVersionIsAfter(task.updatedAt, current.updatedAt) || + (task.updatedAt === current.updatedAt && task.id > current.id))) + ) + result.set(task.documentId, task) + } + return result +} + +export function documentDisplayStatus( + document: LogicalDocument, + task?: DocumentProcessingTask, + sourceDisabled = false, +): DocumentDisplayStatus { + if (documentDisabled(document, sourceDisabled)) return 'disabled' + + const activeRevision = document.activeRevision ?? document.active?.revision ?? 0 + const taskRepresentsLatestRevision = task && task.documentRevision >= activeRevision + if (taskRepresentsLatestRevision) { + if (task.state === 'running') return 'processing' + if (task.state === 'dispatch_pending' || task.state === 'queued' || task.state === 'retry_wait') + return 'queued' + if (task.state === 'failed') return 'failed' + if (task.state === 'canceled' && !document.active) return 'failed' + } + + if (document.status === 'failed') return 'failed' + if (document.status === 'pending') return 'queued' + return 'ready' +} + +export function taskNeedsAttention(task: DocumentProcessingTask) { + return ATTENTION_TASK_STATES.has(task.state) +} + +export function taskIsActive(task: DocumentProcessingTask) { + return ACTIVE_TASK_STATES.has(task.state) +} + +export function taskCanRetry(task: DocumentProcessingTask) { + return task.state === 'failed' +} diff --git a/web/features/new-rag/document-upload-policy.ts b/web/features/new-rag/document-upload-policy.ts new file mode 100644 index 00000000000..feac24f1342 --- /dev/null +++ b/web/features/new-rag/document-upload-policy.ts @@ -0,0 +1,42 @@ +const DOCUMENT_UPLOAD_MAX_BYTES = 15 * 1024 * 1024 + +const DOCUMENT_UPLOAD_EXTENSIONS = [ + 'csv', + 'doc', + 'docx', + 'epub', + 'htm', + 'html', + 'json', + 'jsonl', + 'md', + 'pdf', + 'ppt', + 'pptx', + 'rtf', + 'text', + 'txt', + 'xls', + 'xlsx', +] as const + +const documentUploadExtensionSet = new Set(DOCUMENT_UPLOAD_EXTENSIONS) + +export const DOCUMENT_UPLOAD_ACCEPT = DOCUMENT_UPLOAD_EXTENSIONS.map( + (extension) => `.${extension}`, +).join(',') + +export type DocumentUploadIssue = 'fileSize' | 'fileType' + +export function documentUploadFileExtension(name: string) { + const normalizedName = name.trim().toLocaleLowerCase() + const dotIndex = normalizedName.lastIndexOf('.') + return dotIndex >= 0 && dotIndex < normalizedName.length - 1 + ? normalizedName.slice(dotIndex + 1) + : '' +} + +export function documentUploadIssue(file: File): DocumentUploadIssue | undefined { + if (file.size > DOCUMENT_UPLOAD_MAX_BYTES) return 'fileSize' + if (!documentUploadExtensionSet.has(documentUploadFileExtension(file.name))) return 'fileType' +} diff --git a/web/features/new-rag/documents-page.tsx b/web/features/new-rag/documents-page.tsx new file mode 100644 index 00000000000..f869226c15d --- /dev/null +++ b/web/features/new-rag/documents-page.tsx @@ -0,0 +1,2226 @@ +'use client' + +import type { DocumentProcessingTask } from '@dify/contracts/knowledge-fs/types.gen' +import type { + ProcessingTaskEvent, + ProcessingTaskProgressEvent, +} from './services/processing-task-events' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' +import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useAtomValue, useSetAtom } from 'jotai' +import { debounce, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs' +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Loading from '@/app/components/base/loading' +import { + datasetDefaultPermissionKeysAtom, + refreshWorkspacePermissionKeysAfterMutationDenialAtom, + retryWorkspacePermissionKeysAtom, + workspacePermissionKeysErrorAtom, + workspacePermissionKeysFetchingAtom, + workspacePermissionKeysLoadingAtom, +} from '@/context/permission-state' +import { consoleClient, consoleQuery } from '@/service/client' +import { DatasetACLPermission, hasPermission } from '@/utils/permission' +import { useAuxiliaryTaskReadGuard } from './auxiliary-task-read-guard' +import { DocumentBulkActions, DocumentsEmpty, DocumentsList } from './document-list' +import { + ACTIVE_TASK_STATES, + documentDisplayStatus, + newestTaskByDocument, + sourceName, + taskIsActive, + taskNeedsAttention, + taskVersionIsAfter, +} from './document-model' +import { DOCUMENT_UPLOAD_ACCEPT, documentUploadIssue } from './document-upload-policy' +import { ProcessingTasksDrawer } from './processing-tasks-drawer' +import { TaskEventObserver } from './task-event-observer' +import { createTaskProgressStore } from './task-progress-store' +import { useQueryDataUpdateCount } from './use-query-data-update-count' + +const DOCUMENT_PAGE_SIZE = 50 +const TASK_PAGE_SIZE = 100 +const MAX_TASK_EVENT_STREAMS = 6 +const MAX_AUTO_CURSOR_PAGES = 20 +const FAILED_TASK_POLL_REQUEST_TIMEOUT = 3000 +const TERMINAL_RECONCILIATION_REQUEST_TIMEOUT = 3000 +const BLOCKED_ACTIVE_TASK_REFRESH_INTERVAL = 5000 +const MAX_BLOCKED_ACTIVE_TASK_REFRESH_INTERVAL = 30000 +const documentFilterParser = parseAsStringLiteral([ + 'all', + 'ready', + 'queued', + 'processing', + 'failed', + 'disabled', +] as const) + .withDefault('all') + .withOptions({ history: 'push' }) +const documentSearchParser = parseAsString.withDefault('').withOptions({ + limitUrlUpdates: debounce(300), +}) + +const uploadExclusionReasonKey = { + batch_byte_limit_exceeded: 'batchLimit', + document_not_found: 'target', + file_count_limit_exceeded: 'countLimit', + file_too_large: 'fileSize', + invalid_file: 'fileType', + invalid_target: 'target', + processing_failed: 'processing', + quota_exceeded: 'quota', + revision_conflict: 'target', + unsupported_mime_type: 'fileType', +} as const + +type UploadExclusionReasonKey = + (typeof uploadExclusionReasonKey)[keyof typeof uploadExclusionReasonKey] + +type TerminalTaskPin = { + observedAt: string + taskListGeneration: number +} + +type TrustedActiveOverride = { + taskListGeneration: number + updatedAt: string +} + +type AuxiliaryTaskReadDenial = { + taskListGeneration: number + taskVersion: string +} + +const DOCUMENT_PERMISSION_DENIED = 1 +const TASK_PERMISSION_DENIED = 2 +const SOURCE_PERMISSION_DENIED = 4 + +function recoveryQueryMaskForPermissionDenials(permissionDenialMask: number) { + let recoveryQueryMask = 0 + if (permissionDenialMask & DOCUMENT_PERMISSION_DENIED) + recoveryQueryMask |= TASK_PERMISSION_DENIED | SOURCE_PERMISSION_DENIED + if (permissionDenialMask & TASK_PERMISSION_DENIED) recoveryQueryMask |= SOURCE_PERMISSION_DENIED + if (permissionDenialMask & SOURCE_PERMISSION_DENIED) recoveryQueryMask |= TASK_PERMISSION_DENIED + return recoveryQueryMask & ~permissionDenialMask +} + +function responseStatus(error: unknown): number | undefined { + if (error instanceof Response) return error.status + if (error && typeof error === 'object' && 'status' in error) + return typeof error.status === 'number' ? error.status : undefined + if (error && typeof error === 'object' && 'data' in error) { + const data = error.data + if (data && typeof data === 'object' && 'status' in data) + return typeof data.status === 'number' ? data.status : undefined + } +} + +function taskSnapshotErrorIsTransient(error: unknown) { + const status = responseStatus(error) + return status === undefined || status === 408 || status === 429 || status >= 500 +} + +function queryKeyMatchesKnowledgeSpace(queryKey: readonly unknown[], knowledgeSpaceId: string) { + const state = queryKey[1] + if (!state || typeof state !== 'object' || !('input' in state)) return false + const input = state.input + if (!input || typeof input !== 'object' || !('params' in input)) return false + const params = input.params + return Boolean( + params && typeof params === 'object' && 'id' in params && params.id === knowledgeSpaceId, + ) +} + +function normalizedTaskSnapshot(task: DocumentProcessingTask): DocumentProcessingTask { + return { + ...task, + errorCode: task.errorCode, + errorMessage: task.errorMessage, + } +} + +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) + const workspacePermissionKeysFetching = useAtomValue(workspacePermissionKeysFetchingAtom) + const workspacePermissionKeysError = useAtomValue(workspacePermissionKeysErrorAtom) + const retryWorkspacePermissionKeys = useSetAtom(retryWorkspacePermissionKeysAtom) + const refreshWorkspacePermissionKeysAfterMutationDenial = useSetAtom( + refreshWorkspacePermissionKeysAfterMutationDenialAtom, + ) + const canEdit = hasPermission(datasetDefaultPermissionKeys, DatasetACLPermission.Edit) + const permissionPending = workspacePermissionKeysLoading + const permissionQueryError = Boolean(workspacePermissionKeysError) + const hasWorkspaceWritePermission = canEdit && !permissionPending && !permissionQueryError + const uploadInputRef = useRef(null) + const documentPermissionAlertRef = useRef(null) + const writePermissionFocusRecoveryRequestedRef = useRef(false) + const writePermissionFocusOriginRef = useRef(null) + const uploadPendingRef = useRef(false) + const reindexPendingRef = useRef(false) + const [filter, setFilter] = useQueryState('status', documentFilterParser) + const [search, setSearch] = useQueryState('query', documentSearchParser) + const [selectedDocumentIds, setSelectedDocumentIds] = useState>(() => new Set()) + const [tasksOpen, setTasksOpen] = useState(false) + const [writePermissionRevoked, setWritePermissionRevoked] = useState(false) + const [writePermissionRecoveryGeneration, setWritePermissionRecoveryGeneration] = useState< + number | undefined + >() + const writePermissionDenialGenerationRef = useRef(0) + const writePermissionRecoveryFetchSeenRef = useRef(false) + const [blockingDependencyRetries, setBlockingDependencyRetries] = useState({ + sources: false, + tasks: false, + }) + const [taskStreamOffset, setTaskStreamOffset] = useState(0) + const failedTaskPollOffsetRef = useRef(0) + const [taskOverrides, setTaskOverrides] = useState< + Record> + >({}) + const [terminalTaskPins, setTerminalTaskPins] = useState>({}) + const [taskObserverGenerations, setTaskObserverGenerations] = useState>({}) + const terminalReconciliationGenerationsRef = useRef(new Map()) + const failedTaskPollGenerationsRef = useRef(new Map()) + const blockedFailedTaskPollVersionsRef = useRef(new Map()) + const failedPollAuxiliaryDenialsRef = useRef(new Map()) + const terminalConfirmableAuxiliaryDenialsRef = useRef(new Map()) + const equalRetryListGenerationsRef = useRef(new Map()) + const terminalReconciliationTimeoutsRef = useRef(new Map()) + const terminalReconciliationControllersRef = useRef(new Map()) + const pendingTerminalProgressRef = useRef(new Map()) + const taskEventCursorsRef = useRef(new Map()) + const streamActiveOverrideVersionsRef = useRef(new Map()) + const trustedActiveOverrideVersionsRef = useRef(new Map()) + const trustedOverrideListGenerationsRef = useRef(new Map()) + const taskProgressStoreRef = useRef | null>(null) + if (!taskProgressStoreRef.current) taskProgressStoreRef.current = createTaskProgressStore() + const taskProgressStore = taskProgressStoreRef.current + const [uploading, setUploading] = useState(false) + const [reindexing, setReindexing] = useState(false) + const { mutateAsync: uploadDocument } = useMutation( + consoleQuery.knowledgeFs.postKnowledgeSpacesByIdDocuments.mutationOptions(), + ) + const { mutateAsync: bulkUploadDocuments } = useMutation( + consoleQuery.knowledgeFs.postKnowledgeSpacesByIdDocumentsBulk.mutationOptions(), + ) + const { mutateAsync: reindexDocuments } = useMutation( + consoleQuery.knowledgeFs.postKnowledgeSpacesByIdDocumentsBulkReindex.mutationOptions(), + ) + + const documentsQuery = useInfiniteQuery( + consoleQuery.knowledgeFs.getKnowledgeSpacesByIdLogicalDocuments.infiniteOptions({ + input: (pageParam) => ({ + params: { id: knowledgeSpaceId }, + query: { + limit: DOCUMENT_PAGE_SIZE, + ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}), + }, + }), + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: null as string | null, + }), + ) + const documentPermissionDenied = responseStatus(documentsQuery.error) === 403 + const refetchDocumentsQuery = documentsQuery.refetch + const { + deny: denyAuxiliaryTaskRead, + guard: auxiliaryTaskReadGuard, + permissionDenied: auxiliaryReadPermissionDenied, + retry: retryAuxiliaryTaskRead, + } = useAuxiliaryTaskReadGuard({ + documentPermissionDenied, + refetchDocuments: refetchDocumentsQuery, + }) + useEffect(() => { + if (!documentPermissionDenied) return + failedPollAuxiliaryDenialsRef.current.clear() + terminalConfirmableAuxiliaryDenialsRef.current.clear() + }, [documentPermissionDenied]) + const tasksQueryOptions = useMemo( + () => + consoleQuery.knowledgeFs.getKnowledgeSpacesByIdProcessingTasks.infiniteOptions({ + enabled: !documentPermissionDenied, + input: (pageParam) => ({ + params: { id: knowledgeSpaceId }, + query: { + limit: TASK_PAGE_SIZE, + ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}), + }, + }), + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: null as string | null, + }), + [documentPermissionDenied, knowledgeSpaceId], + ) + const tasksQuery = useInfiniteQuery(tasksQueryOptions) + const sourcesQuery = useInfiniteQuery( + consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSources.infiniteOptions({ + enabled: !documentPermissionDenied, + input: (pageParam) => ({ + params: { id: knowledgeSpaceId }, + query: { + limit: TASK_PAGE_SIZE, + ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}), + }, + }), + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: null as string | null, + }), + ) + const permissionDenialMask = + (documentPermissionDenied || auxiliaryReadPermissionDenied ? DOCUMENT_PERMISSION_DENIED : 0) | + (responseStatus(tasksQuery.error) === 403 ? TASK_PERMISSION_DENIED : 0) | + (responseStatus(sourcesQuery.error) === 403 ? SOURCE_PERMISSION_DENIED : 0) + const permissionDenied = permissionDenialMask !== 0 + const previousPermissionDenialMaskRef = useRef(permissionDenialMask) + const permissionRecoveryQueryMaskRef = useRef( + recoveryQueryMaskForPermissionDenials(permissionDenialMask), + ) + const canWrite = hasWorkspaceWritePermission && !permissionDenied && !writePermissionRevoked + const documentWriteRestrictionReasonId = permissionPending + ? 'documents-permission-pending' + : permissionQueryError + ? 'documents-permission-error' + : !canEdit || writePermissionRevoked + ? 'documents-readonly-reason' + : undefined + const documentsRecoveryDescription = auxiliaryReadPermissionDenied + ? t(($) => $['newKnowledge.documentsPermissionDescription']) + : t(($) => $['newKnowledge.documentsErrorDescription']) + const documentsSectionRef = useRef(null) + const documentsTitleRef = useRef(null) + const documentSurfaceHadFocusRef = useRef(false) + const bulkActionsHadFocusRef = useRef(false) + const mainRetryFocusRequestedRef = useRef(false) + const permissionRetryButtonRef = useRef(null) + const documentsRetryButtonRef = useRef(null) + const dependencyRetryButtonRef = useRef(null) + const { + fetchNextPage: fetchNextDocumentPage, + hasNextPage: hasNextDocumentPage, + isFetchNextPageError: isFetchNextDocumentPageError, + isFetchingNextPage: isFetchingNextDocumentPage, + } = documentsQuery + const { + fetchNextPage: fetchNextTaskPage, + hasNextPage: hasNextTaskPage, + isFetchNextPageError: isFetchNextTaskPageError, + isFetchingNextPage: isFetchingNextTaskPage, + refetch: refetchTasksQuery, + } = tasksQuery + const { + fetchNextPage: fetchNextSourcePage, + hasNextPage: hasNextSourcePage, + isFetchNextPageError: isFetchNextSourcePageError, + isFetchingNextPage: isFetchingNextSourcePage, + refetch: refetchSourcesQuery, + } = sourcesQuery + const canAutoFetchDocumentPage = Boolean( + hasNextDocumentPage && (documentsQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, + ) + const canAutoFetchTaskPage = Boolean( + hasNextTaskPage && (tasksQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, + ) + const documents = useMemo( + () => documentsQuery.data?.pages.flatMap((page) => page.items) ?? [], + [documentsQuery.data], + ) + const baseTasks = useMemo( + () => tasksQuery.data?.pages.flatMap((page) => page.items) ?? [], + [tasksQuery.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 taskDataUpdateCount = useQueryDataUpdateCount(queryClient, tasksQueryOptions.queryKey) + const taskListSnapshotRef = useRef({ + data: tasksQuery.data, + dataUpdateCount: taskDataUpdateCount, + dataUpdatedAt: tasksQuery.dataUpdatedAt, + }) + const taskListGenerationRef = useRef(0) + const [taskListGeneration, setTaskListGeneration] = useState(0) + useLayoutEffect(() => { + if ( + taskListSnapshotRef.current.dataUpdateCount === taskDataUpdateCount && + taskListSnapshotRef.current.dataUpdatedAt === tasksQuery.dataUpdatedAt && + taskListSnapshotRef.current.data === tasksQuery.data + ) + return + taskListSnapshotRef.current = { + data: tasksQuery.data, + dataUpdateCount: taskDataUpdateCount, + dataUpdatedAt: tasksQuery.dataUpdatedAt, + } + taskListGenerationRef.current += 1 + setTaskListGeneration(taskListGenerationRef.current) + }, [taskDataUpdateCount, tasksQuery.data, tasksQuery.dataUpdatedAt]) + const baseTaskById = useMemo(() => new Map(baseTasks.map((task) => [task.id, task])), [baseTasks]) + const sourceNames = useMemo( + () => + new Map( + (sourcesQuery.data?.pages.flatMap((page) => page.items) ?? []).map((source) => [ + source.id, + source.name, + ]), + ), + [sourcesQuery.data], + ) + 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 || + (hasNextTaskPage && isFetchingNextTaskPage) || + (hasRelevantNextSourcePage && isFetchingNextSourcePage), + ) + const canAutoFetchSourcePage = Boolean( + hasRelevantNextSourcePage && (sourcesQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, + ) + const disabledSourceIds = useMemo( + () => + new Set( + (sourcesQuery.data?.pages.flatMap((page) => page.items) ?? []) + .filter((source) => source.status === 'disabled') + .map((source) => source.id), + ), + [sourcesQuery.data], + ) + const baseTaskByIdRef = useRef(baseTaskById) + useLayoutEffect(() => { + baseTaskByIdRef.current = baseTaskById + }, [baseTaskById]) + const tasks = useMemo( + () => + baseTasks.map((task) => { + const override = taskOverrides[task.id] + const terminalTaskPin = terminalTaskPins[task.id] + if ( + terminalTaskPin && + override && + taskIsActive(task) && + !taskVersionIsAfter(task.updatedAt, terminalTaskPin.observedAt) + ) + return { ...task, ...override } + if (!override?.updatedAt) return override ? { ...task, ...override } : task + if (taskVersionIsAfter(task.updatedAt, override.updatedAt)) return task + const mergedTask = { ...task, ...override } + if ( + !taskIsActive(task) && + taskIsActive(mergedTask) && + !taskVersionIsAfter(override.updatedAt, task.updatedAt) && + streamActiveOverrideVersionsRef.current.get(task.id) === override.updatedAt + ) + return task + return mergedTask + }), + [baseTasks, taskOverrides, terminalTaskPins], + ) + const effectiveTaskById = useMemo(() => new Map(tasks.map((task) => [task.id, task])), [tasks]) + useEffect(() => { + for (const task of tasks) { + if (!taskIsActive(task)) { + taskEventCursorsRef.current.delete(task.id) + streamActiveOverrideVersionsRef.current.delete(task.id) + trustedActiveOverrideVersionsRef.current.delete(task.id) + trustedOverrideListGenerationsRef.current.delete(task.id) + } + } + }, [tasks]) + + useEffect(() => { + const clearedActiveTaskIds: string[] = [] + for (const task of baseTasks) { + const denial = terminalConfirmableAuxiliaryDenialsRef.current.get(task.id) + if (!denial || taskListGeneration <= denial.taskListGeneration) continue + if (taskIsActive(task)) { + if (!taskVersionIsAfter(task.updatedAt, denial.taskVersion)) continue + auxiliaryTaskReadGuard.clearTask(task.id) + terminalConfirmableAuxiliaryDenialsRef.current.delete(task.id) + continue + } + if (!auxiliaryTaskReadGuard.clearThrough(task.id, task.updatedAt)) continue + terminalConfirmableAuxiliaryDenialsRef.current.delete(task.id) + const effectiveTask = effectiveTaskById.get(task.id) + if (effectiveTask && taskIsActive(effectiveTask)) clearedActiveTaskIds.push(task.id) + } + if (!clearedActiveTaskIds.length) return + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Clearing a committed terminal guard must remount effective active observers. + setTaskObserverGenerations((current) => { + const next = { ...current } + for (const taskId of clearedActiveTaskIds) next[taskId] = (next[taskId] ?? 0) + 1 + return next + }) + }, [auxiliaryTaskReadGuard, baseTasks, effectiveTaskById, taskListGeneration]) + const currentTaskStateRef = useRef(new Map(tasks.map((task) => [task.id, task.state]))) + const currentTaskVersionRef = useRef(new Map(tasks.map((task) => [task.id, task.updatedAt]))) + useLayoutEffect(() => { + const currentTaskIds = new Set(tasks.map((task) => task.id)) + for (const task of tasks) { + const currentVersion = currentTaskVersionRef.current.get(task.id) + if (!currentVersion || !taskVersionIsAfter(currentVersion, task.updatedAt)) { + currentTaskStateRef.current.set(task.id, task.state) + currentTaskVersionRef.current.set(task.id, task.updatedAt) + } + } + for (const taskId of currentTaskVersionRef.current.keys()) { + if (currentTaskIds.has(taskId)) continue + currentTaskStateRef.current.delete(taskId) + currentTaskVersionRef.current.delete(taskId) + } + }, [tasks]) + + const taskByDocument = useMemo(() => newestTaskByDocument(tasks), [tasks]) + const documentStatuses = useMemo( + () => + new Map( + documents.map((document) => [ + document.id, + documentDisplayStatus( + document, + taskByDocument.get(document.id), + Boolean( + document.sourceId && + (disabledSourceIds.has(document.sourceId) || + (!hasNextSourcePage && + !sourcesQuery.error && + !sourcesQuery.isFetchNextPageError && + !sourceNames.has(document.sourceId))), + ), + ), + ]), + ), + [ + disabledSourceIds, + documents, + hasNextSourcePage, + sourceNames, + sourcesQuery.error, + sourcesQuery.isFetchNextPageError, + taskByDocument, + ], + ) + const filterActive = filter !== 'all' || Boolean(search.trim()) + const statusFilterActive = filter !== 'all' + const availableDocumentIds = useMemo( + () => + new Set( + documents + .filter((document) => documentStatuses.get(document.id) !== 'disabled') + .map((document) => document.id), + ), + [documents, documentStatuses], + ) + const validSelectedDocumentIds = useMemo( + () => + new Set( + [...selectedDocumentIds].filter((documentId) => availableDocumentIds.has(documentId)), + ), + [availableDocumentIds, selectedDocumentIds], + ) + 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)) || + (statusFilterActive && + (hasNextTaskPage || tasksQuery.isFetchingNextPage || tasksQuery.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) || + tasksQuery.isFetchNextPageError || + (sourcesQuery.error && sourcesQuery.data) || + sourcesQuery.isFetchNextPageError, + ) + const taskResultsIncomplete = Boolean( + !tasksQuery.data || tasksQuery.isPending || tasksQuery.isFetchingNextPage, + ) + 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) || tasksQuery.isFetchNextPageError, + ) + 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( + permissionQueryError || + documentsQuery.error || + dependencyQueryBlockingError || + dependencyQueryWarning, + ) + const mainRecoveryIdentity = [ + permissionQueryError ? 'permission' : '', + 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 selectableFilteredDocuments = filteredDocuments.filter( + (document) => documentStatuses.get(document.id) !== 'disabled', + ) + const attentionTasks = tasks.filter(taskNeedsAttention) + const hasTaskError = attentionTasks.some( + (task) => task.state === 'failed' || task.state === 'canceled', + ) + const activeTasks = useMemo(() => tasks.filter(taskIsActive), [tasks]) + const orderedActiveTasks = useMemo( + () => + [...activeTasks].sort( + (left, right) => + left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), + ), + [activeTasks], + ) + const taskObserverVersion = (task: DocumentProcessingTask) => { + let latestVersion = task.updatedAt + for (const candidate of [ + currentTaskVersionRef.current.get(task.id), + taskProgressStore.get(task.id)?.updatedAt, + ]) { + if (candidate && taskVersionIsAfter(candidate, latestVersion)) latestVersion = candidate + } + return latestVersion + } + const streamedActiveTasks = (() => { + if (permissionDenied) return [] + const streamableActiveTasks = orderedActiveTasks.filter( + (task) => !auxiliaryTaskReadGuard.isBlocked(task.id, taskObserverVersion(task)), + ) + const streamCount = Math.min(MAX_TASK_EVENT_STREAMS, streamableActiveTasks.length) + if (!streamCount) return [] + const offset = taskStreamOffset % streamableActiveTasks.length + return Array.from( + { length: streamCount }, + (_, index) => streamableActiveTasks[(offset + index) % streamableActiveTasks.length]!, + ) + })() + const blockedActiveTaskSignature = orderedActiveTasks + .map((task) => [task.id, taskObserverVersion(task)] as const) + .filter(([taskId, taskVersion]) => auxiliaryTaskReadGuard.isBlocked(taskId, taskVersion)) + .map(([taskId, taskVersion]) => `${taskId}:${taskVersion}`) + .join('|') + const orderedFailedTasks = useMemo( + () => + tasks + .filter((task) => task.state === 'failed') + .sort((left, right) => { + if (taskVersionIsAfter(left.updatedAt, right.updatedAt)) return -1 + if (taskVersionIsAfter(right.updatedAt, left.updatedAt)) return 1 + return right.id.localeCompare(left.id) + }), + [tasks], + ) + const orderedFailedTasksRef = useRef(orderedFailedTasks) + useLayoutEffect(() => { + orderedFailedTasksRef.current = orderedFailedTasks + }, [orderedFailedTasks]) + const failedTaskPollSignature = orderedFailedTasks + .map((task) => `${task.id}:${task.updatedAt}`) + .join('|') + 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 allFilteredSelected = + selectableFilteredDocuments.length > 0 && + selectableFilteredDocuments.every((document) => validSelectedDocumentIds.has(document.id)) + const someFilteredSelected = selectableFilteredDocuments.some((document) => + validSelectedDocumentIds.has(document.id), + ) + const bulkActionsVisible = canWrite && validSelectedDocumentIds.size > 0 + const previousBulkActionsVisibleRef = useRef(bulkActionsVisible) + + useLayoutEffect(() => { + const wasVisible = previousBulkActionsVisibleRef.current + previousBulkActionsVisibleRef.current = bulkActionsVisible + if (!wasVisible || bulkActionsVisible || !bulkActionsHadFocusRef.current) return + if (permissionDenied) return + bulkActionsHadFocusRef.current = false + documentsTitleRef.current?.focus() + }, [bulkActionsVisible, permissionDenied]) + + useEffect(() => { + if (permissionDenied || orderedActiveTasks.length <= MAX_TASK_EVENT_STREAMS) return + const interval = window.setInterval( + () => setTaskStreamOffset((current) => current + MAX_TASK_EVENT_STREAMS), + 5000, + ) + return () => window.clearInterval(interval) + }, [orderedActiveTasks.length, permissionDenied]) + + useEffect(() => { + if (permissionDenied || !blockedActiveTaskSignature) return + let canceled = false + let refreshInterval = BLOCKED_ACTIVE_TASK_REFRESH_INTERVAL + let timeout: number | undefined + const refreshBlockedTasks = async () => { + if (canceled) return + await refetchTasksQuery({ cancelRefetch: false }).catch(() => undefined) + if (canceled) return + refreshInterval = Math.min(refreshInterval * 2, MAX_BLOCKED_ACTIVE_TASK_REFRESH_INTERVAL) + timeout = window.setTimeout(refreshBlockedTasks, refreshInterval) + } + timeout = window.setTimeout(refreshBlockedTasks, BLOCKED_ACTIVE_TASK_REFRESH_INTERVAL) + return () => { + canceled = true + if (timeout !== undefined) window.clearTimeout(timeout) + } + }, [blockedActiveTaskSignature, permissionDenied, refetchTasksQuery]) + + useLayoutEffect(() => { + if (!writePermissionRevoked || !writePermissionFocusRecoveryRequestedRef.current) return + writePermissionFocusRecoveryRequestedRef.current = false + writePermissionFocusOriginRef.current = null + documentsTitleRef.current?.focus() + }, [writePermissionRevoked]) + + useEffect(() => { + const previousPermissionDenialMask = previousPermissionDenialMaskRef.current + previousPermissionDenialMaskRef.current = permissionDenialMask + const addedPermissionDenials = permissionDenialMask & ~previousPermissionDenialMask + let recoveryQueryMask = + permissionRecoveryQueryMaskRef.current | + recoveryQueryMaskForPermissionDenials(addedPermissionDenials) + if ( + previousPermissionDenialMask & TASK_PERMISSION_DENIED && + !(permissionDenialMask & TASK_PERMISSION_DENIED) + ) + recoveryQueryMask &= ~TASK_PERMISSION_DENIED + if ( + previousPermissionDenialMask & SOURCE_PERMISSION_DENIED && + !(permissionDenialMask & SOURCE_PERMISSION_DENIED) + ) + recoveryQueryMask &= ~SOURCE_PERMISSION_DENIED + recoveryQueryMask &= ~permissionDenialMask + permissionRecoveryQueryMaskRef.current = recoveryQueryMask + if (previousPermissionDenialMask && !permissionDenied) { + blockedFailedTaskPollVersionsRef.current.clear() + if (recoveryQueryMask & TASK_PERMISSION_DENIED) + void refetchTasksQuery({ cancelRefetch: false }) + if (recoveryQueryMask & SOURCE_PERMISSION_DENIED) + void refetchSourcesQuery({ cancelRefetch: false }) + permissionRecoveryQueryMaskRef.current = 0 + if (documentSurfaceHadFocusRef.current || bulkActionsHadFocusRef.current) + documentsTitleRef.current?.focus() + bulkActionsHadFocusRef.current = false + return + } + if (previousPermissionDenialMask || !permissionDenied) return + const shouldRestoreFocus = + tasksOpen || documentSurfaceHadFocusRef.current || bulkActionsHadFocusRef.current + if (tasksOpen) { + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Permission revocation permanently closes the controlled task drawer. + setTasksOpen(false) + } + if (shouldRestoreFocus) documentPermissionAlertRef.current?.focus() + bulkActionsHadFocusRef.current = false + }, [permissionDenialMask, permissionDenied, refetchSourcesQuery, refetchTasksQuery, tasksOpen]) + + useEffect(() => { + if ( + !writePermissionRevoked || + writePermissionRecoveryGeneration !== writePermissionDenialGenerationRef.current + ) { + writePermissionRecoveryFetchSeenRef.current = false + return + } + if (workspacePermissionKeysFetching) { + writePermissionRecoveryFetchSeenRef.current = true + return + } + if ( + !writePermissionRecoveryFetchSeenRef.current || + permissionPending || + permissionQueryError || + !canEdit + ) + return + writePermissionRecoveryFetchSeenRef.current = false + // oxlint-disable-next-line eslint-react/set-state-in-effect -- A post-denial permission request is the authoritative event that retires the local mutation lock. + setWritePermissionRevoked(false) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- The completed recovery generation is retired with its write lock. + setWritePermissionRecoveryGeneration(undefined) + }, [ + canEdit, + permissionPending, + permissionQueryError, + workspacePermissionKeysFetching, + writePermissionRecoveryGeneration, + writePermissionRevoked, + ]) + + useEffect(() => { + if (!mainRetryFocusRequestedRef.current) return + if (mainRecoveryVisible) { + if (documentPermissionDenied) { + mainRetryFocusRequestedRef.current = false + documentPermissionAlertRef.current?.focus() + } else if (permissionQueryError) permissionRetryButtonRef.current?.focus() + else if (documentsQuery.error && !permissionDenied) documentsRetryButtonRef.current?.focus() + else dependencyRetryButtonRef.current?.focus() + return + } + mainRetryFocusRequestedRef.current = false + documentsTitleRef.current?.focus() + }, [ + documentsQuery.error, + documentPermissionDenied, + mainRecoveryIdentity, + mainRecoveryVisible, + permissionDenied, + permissionQueryError, + ]) + + 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 ( + !permissionDenied && + (filterActive || (tasksOpen && unresolvedTaskDocumentIds.size > 0)) && + canAutoFetchDocumentPage && + !isFetchingNextDocumentPage && + !isFetchNextDocumentPageError + ) + void fetchNextDocumentPage() + }, [ + fetchNextDocumentPage, + filterActive, + tasksOpen, + unresolvedTaskDocumentIds, + canAutoFetchDocumentPage, + isFetchNextDocumentPageError, + isFetchingNextDocumentPage, + permissionDenied, + ]) + + useEffect(() => { + if ( + !permissionDenied && + canAutoFetchTaskPage && + !isFetchingNextTaskPage && + !isFetchNextTaskPageError + ) + void fetchNextTaskPage() + }, [ + canAutoFetchTaskPage, + fetchNextTaskPage, + isFetchNextTaskPageError, + isFetchingNextTaskPage, + permissionDenied, + ]) + + useEffect(() => { + if ( + !permissionDenied && + canAutoFetchSourcePage && + !isFetchingNextSourcePage && + !isFetchNextSourcePageError + ) + void fetchNextSourcePage() + }, [ + canAutoFetchSourcePage, + fetchNextSourcePage, + isFetchNextSourcePageError, + isFetchingNextSourcePage, + permissionDenied, + ]) + + const refreshDocuments = useCallback(() => { + void queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdLogicalDocuments.key(), + }) + }, [knowledgeSpaceId, queryClient]) + + const refreshDocumentsAndTasks = useCallback(() => { + void Promise.allSettled([ + queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdLogicalDocuments.key(), + }), + queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdProcessingTasks.key(), + }), + ]) + }, [knowledgeSpaceId, queryClient]) + + const refreshWorkspacePermissions = useCallback( + async (releaseWriteLockOnSuccess: boolean) => { + const denialGeneration = writePermissionDenialGenerationRef.current + const result = await (releaseWriteLockOnSuccess && writePermissionRevoked + ? refreshWorkspacePermissionKeysAfterMutationDenial() + : retryWorkspacePermissionKeys()) + const refreshedPermissionKeys = result.data?.dataset?.default_permission_keys + if ( + releaseWriteLockOnSuccess && + writePermissionDenialGenerationRef.current === denialGeneration && + !result.error && + refreshedPermissionKeys && + hasPermission(refreshedPermissionKeys, DatasetACLPermission.Edit) + ) { + writePermissionRecoveryFetchSeenRef.current = false + setWritePermissionRevoked(false) + setWritePermissionRecoveryGeneration(undefined) + } + }, + [ + refreshWorkspacePermissionKeysAfterMutationDenial, + retryWorkspacePermissionKeys, + writePermissionRevoked, + ], + ) + + const handleWritePermissionDenied = useCallback(() => { + const denialGeneration = writePermissionDenialGenerationRef.current + 1 + writePermissionDenialGenerationRef.current = denialGeneration + writePermissionRecoveryFetchSeenRef.current = false + setWritePermissionRecoveryGeneration(undefined) + setWritePermissionRevoked(true) + void refreshWorkspacePermissionKeysAfterMutationDenial().then((result) => { + const refreshedPermissionKeys = result.data?.dataset?.default_permission_keys + if (writePermissionDenialGenerationRef.current !== denialGeneration) return + if ( + !result.error && + refreshedPermissionKeys && + hasPermission(refreshedPermissionKeys, DatasetACLPermission.Edit) + ) { + writePermissionRecoveryFetchSeenRef.current = false + setWritePermissionRevoked(false) + setWritePermissionRecoveryGeneration(undefined) + return + } + setWritePermissionRecoveryGeneration(denialGeneration) + }) + }, [refreshWorkspacePermissionKeysAfterMutationDenial]) + + const reconcileTerminalTask = useCallback( + async function reconcileTerminalTaskRequest( + taskId: string, + terminalVersion: string, + reconciliationGeneration: number, + retryAttempt = 0, + ) { + const currentTask = baseTaskByIdRef.current.get(taskId) + if (!currentTask || auxiliaryTaskReadGuard.isBlocked(taskId, terminalVersion)) return + const pendingRetryTimeout = terminalReconciliationTimeoutsRef.current.get(taskId) + if (pendingRetryTimeout !== undefined) window.clearTimeout(pendingRetryTimeout) + terminalReconciliationTimeoutsRef.current.delete(taskId) + terminalReconciliationControllersRef.current.get(taskId)?.abort() + const controller = new AbortController() + terminalReconciliationControllersRef.current.set(taskId, controller) + const requestTimeout = window.setTimeout( + () => controller.abort(), + TERMINAL_RECONCILIATION_REQUEST_TIMEOUT, + ) + try { + const snapshot = + await consoleClient.knowledgeFs.getKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskId( + { + params: { + documentId: currentTask.documentId, + id: knowledgeSpaceId, + taskId, + }, + }, + { signal: controller.signal }, + ) + if ( + terminalReconciliationControllersRef.current.get(taskId) !== controller || + terminalReconciliationGenerationsRef.current.get(taskId) !== reconciliationGeneration + ) + return + terminalReconciliationControllersRef.current.delete(taskId) + const currentTaskVersion = currentTaskVersionRef.current.get(taskId) + if (currentTaskVersion && taskVersionIsAfter(currentTaskVersion, snapshot.updatedAt)) return + if (taskVersionIsAfter(terminalVersion, snapshot.updatedAt)) return + auxiliaryTaskReadGuard.clearTask(taskId) + failedPollAuxiliaryDenialsRef.current.delete(taskId) + terminalConfirmableAuxiliaryDenialsRef.current.delete(taskId) + const normalizedSnapshot = normalizedTaskSnapshot(snapshot) + if (taskIsActive(snapshot)) + trustedActiveOverrideVersionsRef.current.set(taskId, { + taskListGeneration: taskListGenerationRef.current, + updatedAt: snapshot.updatedAt, + }) + else trustedActiveOverrideVersionsRef.current.delete(taskId) + taskProgressStore.delete(taskId) + currentTaskStateRef.current.set(taskId, snapshot.state) + currentTaskVersionRef.current.set(taskId, snapshot.updatedAt) + setTaskOverrides((current) => { + const currentVersion = current[taskId]?.updatedAt + if (currentVersion && taskVersionIsAfter(currentVersion, snapshot.updatedAt)) + return current + return { ...current, [taskId]: normalizedSnapshot } + }) + if (taskIsActive(snapshot)) { + blockedFailedTaskPollVersionsRef.current.delete(taskId) + taskEventCursorsRef.current.delete(taskId) + const pollGeneration = failedTaskPollGenerationsRef.current.get(taskId) ?? 0 + failedTaskPollGenerationsRef.current.set(taskId, pollGeneration + 1) + setTerminalTaskPins((current) => { + const pin = current[taskId] + if (!pin || taskVersionIsAfter(pin.observedAt, snapshot.updatedAt)) return current + const next = { ...current } + delete next[taskId] + return next + }) + setTaskObserverGenerations((current) => ({ + ...current, + [taskId]: (current[taskId] ?? 0) + 1, + })) + } + } catch (error) { + if (terminalReconciliationControllersRef.current.get(taskId) !== controller) return + terminalReconciliationControllersRef.current.delete(taskId) + if (responseStatus(error) === 403) { + const currentTaskVersion = currentTaskVersionRef.current.get(taskId) + const deniedVersion = + currentTaskVersion && taskVersionIsAfter(currentTaskVersion, terminalVersion) + ? currentTaskVersion + : terminalVersion + terminalConfirmableAuxiliaryDenialsRef.current.set(taskId, { + taskListGeneration: taskListGenerationRef.current, + taskVersion: deniedVersion, + }) + denyAuxiliaryTaskRead(taskId, deniedVersion) + return + } + if ( + retryAttempt >= 4 || + !taskSnapshotErrorIsTransient(error) || + terminalReconciliationGenerationsRef.current.get(taskId) !== reconciliationGeneration + ) + return + const timeout = window.setTimeout( + () => { + terminalReconciliationTimeoutsRef.current.delete(taskId) + if ( + terminalReconciliationGenerationsRef.current.get(taskId) === reconciliationGeneration + ) + void reconcileTerminalTaskRequest( + taskId, + terminalVersion, + reconciliationGeneration, + retryAttempt + 1, + ) + }, + Math.min(1000 * 2 ** retryAttempt, 30000), + ) + terminalReconciliationTimeoutsRef.current.set(taskId, timeout) + } finally { + window.clearTimeout(requestTimeout) + } + }, + [auxiliaryTaskReadGuard, denyAuxiliaryTaskRead, knowledgeSpaceId, taskProgressStore], + ) + + useEffect(() => { + if (permissionDenied) return + for (const task of baseTasks) { + if (taskIsActive(task)) continue + const override = taskOverrides[task.id] + const trustedOverride = trustedActiveOverrideVersionsRef.current.get(task.id) + if ( + !override?.updatedAt || + !taskIsActive({ ...task, ...override }) || + taskVersionIsAfter(override.updatedAt, task.updatedAt) || + trustedOverride?.updatedAt !== override.updatedAt || + taskListGeneration <= trustedOverride.taskListGeneration || + trustedOverrideListGenerationsRef.current.get(task.id) === taskListGeneration + ) + continue + trustedOverrideListGenerationsRef.current.set(task.id, taskListGeneration) + const reconciliationGeneration = + (terminalReconciliationGenerationsRef.current.get(task.id) ?? 0) + 1 + terminalReconciliationGenerationsRef.current.set(task.id, reconciliationGeneration) + void reconcileTerminalTask(task.id, task.updatedAt, reconciliationGeneration) + } + }, [baseTasks, permissionDenied, reconcileTerminalTask, taskListGeneration, taskOverrides]) + + useEffect( + () => () => { + for (const controller of terminalReconciliationControllersRef.current.values()) + controller.abort() + terminalReconciliationControllersRef.current.clear() + for (const timeout of terminalReconciliationTimeoutsRef.current.values()) + window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.clear() + }, + [knowledgeSpaceId], + ) + + useEffect(() => { + if (!permissionDenied) return + void queryClient.cancelQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdProcessingTasks.key(), + }) + void queryClient.cancelQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSources.key(), + }) + for (const [taskId, controller] of terminalReconciliationControllersRef.current) { + controller.abort() + terminalReconciliationControllersRef.current.delete(taskId) + const generation = terminalReconciliationGenerationsRef.current.get(taskId) ?? 0 + terminalReconciliationGenerationsRef.current.set(taskId, generation + 1) + } + for (const timeout of terminalReconciliationTimeoutsRef.current.values()) + window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.clear() + equalRetryListGenerationsRef.current.clear() + }, [knowledgeSpaceId, permissionDenied, queryClient]) + + useEffect(() => { + const taskIds = new Set(baseTasks.map((task) => task.id)) + const pruneMap = (map: Map) => { + for (const taskId of map.keys()) { + if (!taskIds.has(taskId)) map.delete(taskId) + } + } + + for (const [taskId, controller] of terminalReconciliationControllersRef.current) { + if (taskIds.has(taskId)) continue + controller.abort() + terminalReconciliationControllersRef.current.delete(taskId) + } + for (const [taskId, timeout] of terminalReconciliationTimeoutsRef.current) { + if (taskIds.has(taskId)) continue + window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.delete(taskId) + } + pruneMap(terminalReconciliationGenerationsRef.current) + pruneMap(failedTaskPollGenerationsRef.current) + pruneMap(blockedFailedTaskPollVersionsRef.current) + pruneMap(failedPollAuxiliaryDenialsRef.current) + pruneMap(terminalConfirmableAuxiliaryDenialsRef.current) + auxiliaryTaskReadGuard.retain(taskIds) + pruneMap(equalRetryListGenerationsRef.current) + pruneMap(pendingTerminalProgressRef.current) + pruneMap(taskEventCursorsRef.current) + pruneMap(trustedOverrideListGenerationsRef.current) + for (const map of [ + streamActiveOverrideVersionsRef.current, + trustedActiveOverrideVersionsRef.current, + ]) { + for (const taskId of map.keys()) { + if (!taskIds.has(taskId)) map.delete(taskId) + } + } + taskProgressStore.retain(taskIds) + + const retainTaskState = (current: Record) => { + const staleTaskIds = Object.keys(current).filter((taskId) => !taskIds.has(taskId)) + if (!staleTaskIds.length) return current + const next = { ...current } + for (const taskId of staleTaskIds) delete next[taskId] + return next + } + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Prune state for tasks removed by a refreshed cursor result. + setTaskOverrides(retainTaskState) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Prune state for tasks removed by a refreshed cursor result. + setTerminalTaskPins(retainTaskState) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Prune state for tasks removed by a refreshed cursor result. + setTaskObserverGenerations(retainTaskState) + }, [auxiliaryTaskReadGuard, baseTasks, taskProgressStore]) + + useEffect(() => { + if (permissionDenied) return + const strictRetries = new Map() + const equalTimestampRetries = new Map() + const confirmedTerminals = new Map() + for (const task of baseTasks) { + const pin = terminalTaskPins[task.id] + if (!pin || taskListGeneration <= pin.taskListGeneration) continue + if (!taskIsActive(task)) { + if (!taskVersionIsAfter(pin.observedAt, task.updatedAt)) + confirmedTerminals.set(task.id, task) + continue + } + if (taskVersionIsAfter(task.updatedAt, pin.observedAt)) strictRetries.set(task.id, task) + else if (!taskVersionIsAfter(pin.observedAt, task.updatedAt)) + equalTimestampRetries.set(task.id, pin) + } + + for (const taskId of confirmedTerminals.keys()) { + const generation = terminalReconciliationGenerationsRef.current.get(taskId) ?? 0 + terminalReconciliationGenerationsRef.current.set(taskId, generation + 1) + terminalReconciliationControllersRef.current.get(taskId)?.abort() + terminalReconciliationControllersRef.current.delete(taskId) + const timeout = terminalReconciliationTimeoutsRef.current.get(taskId) + if (timeout !== undefined) window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.delete(taskId) + } + if (confirmedTerminals.size) { + // oxlint-disable-next-line eslint-react/set-state-in-effect -- A current terminal list snapshot supersedes the partial SSE terminal payload. + setTerminalTaskPins((current) => { + const next = { ...current } + for (const taskId of confirmedTerminals.keys()) delete next[taskId] + return next + }) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Restore complete server error details from the authoritative terminal list snapshot. + setTaskOverrides((current) => { + const next = { ...current } + for (const [taskId, task] of confirmedTerminals) { + const overrideVersion = current[taskId]?.updatedAt + if (!overrideVersion || !taskVersionIsAfter(overrideVersion, task.updatedAt)) + delete next[taskId] + } + return next + }) + } + + for (const taskId of strictRetries.keys()) { + taskProgressStore.delete(taskId) + blockedFailedTaskPollVersionsRef.current.delete(taskId) + auxiliaryTaskReadGuard.clearTask(taskId) + failedPollAuxiliaryDenialsRef.current.delete(taskId) + terminalConfirmableAuxiliaryDenialsRef.current.delete(taskId) + taskEventCursorsRef.current.delete(taskId) + const pollGeneration = failedTaskPollGenerationsRef.current.get(taskId) ?? 0 + failedTaskPollGenerationsRef.current.set(taskId, pollGeneration + 1) + const generation = terminalReconciliationGenerationsRef.current.get(taskId) ?? 0 + terminalReconciliationGenerationsRef.current.set(taskId, generation + 1) + terminalReconciliationControllersRef.current.get(taskId)?.abort() + terminalReconciliationControllersRef.current.delete(taskId) + const timeout = terminalReconciliationTimeoutsRef.current.get(taskId) + if (timeout !== undefined) window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.delete(taskId) + } + if (strictRetries.size) { + // oxlint-disable-next-line eslint-react/set-state-in-effect -- A strictly newer active list snapshot proves a retry without another request. + setTerminalTaskPins((current) => { + const next = { ...current } + for (const taskId of strictRetries.keys()) delete next[taskId] + return next + }) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Drop terminal overrides only after a strictly newer active list snapshot. + setTaskOverrides((current) => { + const next = { ...current } + for (const [taskId, task] of strictRetries) { + const overrideVersion = current[taskId]?.updatedAt + if (!overrideVersion || !taskVersionIsAfter(overrideVersion, task.updatedAt)) + delete next[taskId] + } + return next + }) + } + + for (const [taskId, pin] of equalTimestampRetries) { + if (equalRetryListGenerationsRef.current.get(taskId) === taskListGeneration) continue + equalRetryListGenerationsRef.current.set(taskId, taskListGeneration) + auxiliaryTaskReadGuard.clearTask(taskId) + failedPollAuxiliaryDenialsRef.current.delete(taskId) + terminalConfirmableAuxiliaryDenialsRef.current.delete(taskId) + const timeout = terminalReconciliationTimeoutsRef.current.get(taskId) + if (timeout !== undefined) window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.delete(taskId) + const reconciliationGeneration = + (terminalReconciliationGenerationsRef.current.get(taskId) ?? 0) + 1 + terminalReconciliationGenerationsRef.current.set(taskId, reconciliationGeneration) + void reconcileTerminalTask(taskId, pin.observedAt, reconciliationGeneration) + } + }, [ + auxiliaryTaskReadGuard, + baseTasks, + permissionDenied, + reconcileTerminalTask, + taskListGeneration, + taskProgressStore, + terminalTaskPins, + ]) + + const handleUploadFiles = useCallback( + async (files: File[]) => { + if (!canWrite || !files.length || uploadPendingRef.current) return + const uploadableFiles: File[] = [] + const localExclusions: Array<{ + filename: string + reasonKey: UploadExclusionReasonKey + }> = [] + for (const file of files) { + const issue = documentUploadIssue(file) + if (issue) localExclusions.push({ filename: file.name, reasonKey: issue }) + else uploadableFiles.push(file) + } + const formatExclusionDetails = ( + exclusions: Array<{ filename: string; reasonKey: UploadExclusionReasonKey }>, + ) => { + const detailItems = exclusions + .slice(0, 3) + .map( + ({ filename, reasonKey }) => + `${filename} (${t(($) => $[`newKnowledge.documentUploadExclusion.${reasonKey}`])})`, + ) + if (exclusions.length > detailItems.length) + detailItems.push( + t(($) => $['newKnowledge.documentUploadExclusion.more'], { + count: exclusions.length - detailItems.length, + }), + ) + return detailItems.join('; ') + } + if (!uploadableFiles.length) { + toast.error( + t(($) => $['newKnowledge.documentUploadRejected'], { + details: formatExclusionDetails(localExclusions), + }), + ) + return + } + let writePermissionDenied = false + uploadPendingRef.current = true + setUploading(true) + try { + let acceptedCount = 0 + const exclusions = [...localExclusions] + if (uploadableFiles.length === 1) { + await uploadDocument({ + body: { file: uploadableFiles[0]! }, + params: { id: knowledgeSpaceId }, + }) + acceptedCount = 1 + } else { + const result = await bulkUploadDocuments({ + body: { files: uploadableFiles }, + params: { id: knowledgeSpaceId }, + }) + acceptedCount = result.accepted + for (const item of result.items) { + if (!('reason' in item)) continue + const reasonKey = uploadExclusionReasonKey[item.reason] + exclusions.push({ filename: item.filename, reasonKey }) + } + } + const exclusionDetails = formatExclusionDetails(exclusions) + if (!acceptedCount) { + toast.error( + t(($) => $['newKnowledge.documentUploadRejected'], { + details: exclusionDetails, + }), + ) + return + } + if (exclusions.length) + toast.warning( + t(($) => $['newKnowledge.documentUploadPartial'], { + accepted: acceptedCount, + details: exclusionDetails, + excluded: exclusions.length, + }), + ) + else toast.success(t(($) => $['newKnowledge.documentUploadStarted'])) + refreshDocumentsAndTasks() + } catch (error) { + if (responseStatus(error) === 403) { + writePermissionDenied = true + handleWritePermissionDenied() + } else toast.error(t(($) => $['newKnowledge.documentUploadFailed'])) + } finally { + if (!writePermissionDenied) { + writePermissionFocusRecoveryRequestedRef.current = false + writePermissionFocusOriginRef.current = null + } + uploadPendingRef.current = false + setUploading(false) + } + }, + [ + bulkUploadDocuments, + canWrite, + handleWritePermissionDenied, + knowledgeSpaceId, + refreshDocumentsAndTasks, + t, + uploadDocument, + ], + ) + + const handleReindexDocuments = useCallback(async () => { + if ( + !canWrite || + selectionDisabled || + !validSelectedDocumentIds.size || + reindexPendingRef.current + ) + return + reindexPendingRef.current = true + setReindexing(true) + try { + const selectedIds = [...validSelectedDocumentIds].sort() + const result = await reindexDocuments({ + body: { documentIds: selectedIds }, + params: { id: knowledgeSpaceId }, + }) + const missingIds = result.items + .filter((item) => item.status === 'not_found') + .map((item) => item.documentId) + const queuedCount = result.items.length - missingIds.length + if (!queuedCount) { + setSelectedDocumentIds(new Set()) + toast.error( + t(($) => $['newKnowledge.documentsReindexPartial'], { + missing: missingIds.length, + queued: 0, + }), + ) + refreshDocumentsAndTasks() + return + } + setSelectedDocumentIds(new Set(missingIds)) + if (missingIds.length) + toast.warning( + t(($) => $['newKnowledge.documentsReindexPartial'], { + missing: missingIds.length, + queued: queuedCount, + }), + ) + else toast.success(t(($) => $['newKnowledge.documentsReindexStarted'])) + refreshDocumentsAndTasks() + } catch (error) { + if (responseStatus(error) === 403) handleWritePermissionDenied() + else toast.error(t(($) => $['newKnowledge.documentsReindexFailed'])) + } finally { + reindexPendingRef.current = false + setReindexing(false) + } + }, [ + canWrite, + handleWritePermissionDenied, + knowledgeSpaceId, + refreshDocumentsAndTasks, + reindexDocuments, + selectionDisabled, + t, + validSelectedDocumentIds, + ]) + + const handleTaskEvent = useCallback( + (taskId: string, taskVersion: string, event: ProcessingTaskEvent) => { + const eventVersion = event.event === 'progress' ? event.data.updatedAt : taskVersion + const terminalSnapshot = !ACTIVE_TASK_STATES.has(event.data.state) + const currentVersion = currentTaskVersionRef.current.get(taskId) + if (currentVersion && taskVersionIsAfter(currentVersion, eventVersion)) { + if (terminalSnapshot) pendingTerminalProgressRef.current.delete(taskId) + return false + } + + if (event.event === 'progress' && ACTIVE_TASK_STATES.has(event.data.state)) { + if (trustedActiveOverrideVersionsRef.current.get(taskId)?.updatedAt === eventVersion) + streamActiveOverrideVersionsRef.current.delete(taskId) + else streamActiveOverrideVersionsRef.current.set(taskId, eventVersion) + } else streamActiveOverrideVersionsRef.current.delete(taskId) + + if (event.event === 'progress' && terminalSnapshot) { + currentTaskVersionRef.current.set(taskId, eventVersion) + taskProgressStore.set(taskId, event.data) + pendingTerminalProgressRef.current.set(taskId, event) + return true + } + + const pendingTerminalProgress = + event.event === 'terminal' ? pendingTerminalProgressRef.current.get(taskId) : undefined + if (event.event === 'terminal') pendingTerminalProgressRef.current.delete(taskId) + + if (event.event === 'progress') taskProgressStore.set(taskId, event.data) + else taskProgressStore.delete(taskId) + + const currentTaskState = currentTaskStateRef.current.get(taskId) + currentTaskVersionRef.current.set(taskId, eventVersion) + if (event.event === 'progress' && currentTaskState === event.data.state) return true + currentTaskStateRef.current.set(taskId, event.data.state) + + setTaskOverrides((current) => { + const previous = current[taskId] + if ( + event.event === 'progress' && + previous?.updatedAt && + taskVersionIsAfter(previous.updatedAt, event.data.updatedAt) + ) + return current + return { + ...current, + [taskId]: + event.event === 'progress' + ? { + errorCode: undefined, + errorMessage: undefined, + progressPercent: event.data.progressPercent, + stage: event.data.stage, + state: event.data.state, + updatedAt: event.data.updatedAt, + } + : { + errorCode: event.data.errorCode, + errorMessage: undefined, + ...(pendingTerminalProgress + ? { + progressPercent: pendingTerminalProgress.data.progressPercent, + stage: pendingTerminalProgress.data.stage, + } + : {}), + state: event.data.state, + updatedAt: eventVersion, + }, + } + }) + if (event.event === 'terminal') { + const pollGeneration = failedTaskPollGenerationsRef.current.get(taskId) ?? 0 + failedTaskPollGenerationsRef.current.set(taskId, pollGeneration + 1) + const timeout = terminalReconciliationTimeoutsRef.current.get(taskId) + if (timeout !== undefined) window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.delete(taskId) + const reconciliationGeneration = + (terminalReconciliationGenerationsRef.current.get(taskId) ?? 0) + 1 + terminalReconciliationGenerationsRef.current.set(taskId, reconciliationGeneration) + setTerminalTaskPins((current) => ({ + ...current, + [taskId]: { + observedAt: eventVersion, + taskListGeneration: taskListGenerationRef.current, + }, + })) + if (event.data.state === 'failed') + toast.error(t(($) => $['newKnowledge.taskFailedNotification'])) + refreshDocuments() + void reconcileTerminalTask(taskId, eventVersion, reconciliationGeneration) + } + return true + }, + [reconcileTerminalTask, refreshDocuments, t, taskProgressStore], + ) + + const handleTaskUpdated = useCallback( + (task: DocumentProcessingTask) => { + const currentVersion = currentTaskVersionRef.current.get(task.id) + if (currentVersion && taskVersionIsAfter(currentVersion, task.updatedAt)) return + streamActiveOverrideVersionsRef.current.delete(task.id) + auxiliaryTaskReadGuard.clearTask(task.id) + failedPollAuxiliaryDenialsRef.current.delete(task.id) + terminalConfirmableAuxiliaryDenialsRef.current.delete(task.id) + if (taskIsActive(task)) + trustedActiveOverrideVersionsRef.current.set(task.id, { + taskListGeneration: taskListGenerationRef.current, + updatedAt: task.updatedAt, + }) + else trustedActiveOverrideVersionsRef.current.delete(task.id) + trustedOverrideListGenerationsRef.current.delete(task.id) + taskProgressStore.delete(task.id) + currentTaskStateRef.current.set(task.id, task.state) + currentTaskVersionRef.current.set(task.id, task.updatedAt) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- A committed active list lifecycle retires the denied failed snapshot. + setTaskOverrides((current) => ({ ...current, [task.id]: normalizedTaskSnapshot(task) })) + if (taskIsActive(task)) { + blockedFailedTaskPollVersionsRef.current.delete(task.id) + taskEventCursorsRef.current.delete(task.id) + terminalReconciliationControllersRef.current.get(task.id)?.abort() + terminalReconciliationControllersRef.current.delete(task.id) + const timeout = terminalReconciliationTimeoutsRef.current.get(task.id) + if (timeout !== undefined) window.clearTimeout(timeout) + terminalReconciliationTimeoutsRef.current.delete(task.id) + const generation = terminalReconciliationGenerationsRef.current.get(task.id) ?? 0 + terminalReconciliationGenerationsRef.current.set(task.id, generation + 1) + const pollGeneration = failedTaskPollGenerationsRef.current.get(task.id) ?? 0 + failedTaskPollGenerationsRef.current.set(task.id, pollGeneration + 1) + pendingTerminalProgressRef.current.delete(task.id) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- The authoritative active lifecycle supersedes the terminal pin. + setTerminalTaskPins((current) => { + const next = { ...current } + delete next[task.id] + return next + }) + } + }, + [auxiliaryTaskReadGuard, taskProgressStore], + ) + + useEffect(() => { + if (permissionDenied) return + for (const task of baseTasks) { + if (!taskIsActive(task)) continue + const denial = failedPollAuxiliaryDenialsRef.current.get(task.id) + if (!denial || taskListGeneration <= denial.taskListGeneration) continue + if (taskVersionIsAfter(denial.taskVersion, task.updatedAt)) continue + failedPollAuxiliaryDenialsRef.current.delete(task.id) + handleTaskUpdated(task) + } + }, [baseTasks, handleTaskUpdated, permissionDenied, taskListGeneration]) + + useEffect(() => { + for (const task of activeTasks) { + const hadBlockedPoll = blockedFailedTaskPollVersionsRef.current.delete(task.id) + if (!hadBlockedPoll) continue + const generation = failedTaskPollGenerationsRef.current.get(task.id) ?? 0 + failedTaskPollGenerationsRef.current.set(task.id, generation + 1) + } + }, [activeTasks]) + + useEffect(() => { + if (permissionDenied || !orderedFailedTasksRef.current.length) return + let canceled = false + let timeout: number | undefined + const cancelRequests = new Set<() => void>() + const pollNextBatch = async () => { + const failedTasks = orderedFailedTasksRef.current + const pollableTasks = failedTasks.filter( + (task) => + blockedFailedTaskPollVersionsRef.current.get(task.id) !== task.updatedAt && + !auxiliaryTaskReadGuard.isBlocked(task.id, task.updatedAt), + ) + if (pollableTasks.length) { + const pollCount = Math.min(MAX_TASK_EVENT_STREAMS, pollableTasks.length) + const offset = failedTaskPollOffsetRef.current % pollableTasks.length + const tasksToPoll = Array.from( + { length: pollCount }, + (_, index) => pollableTasks[(offset + index) % pollableTasks.length]!, + ) + failedTaskPollOffsetRef.current += MAX_TASK_EVENT_STREAMS + await Promise.allSettled( + tasksToPoll.map(async (task) => { + const requestGeneration = (failedTaskPollGenerationsRef.current.get(task.id) ?? 0) + 1 + failedTaskPollGenerationsRef.current.set(task.id, requestGeneration) + const requestController = new AbortController() + let requestTimeout: number | undefined + let rejectDeadline: ((reason?: unknown) => void) | undefined + const cancelRequest = () => { + requestController.abort() + rejectDeadline?.(new DOMException('Task snapshot request aborted', 'AbortError')) + } + try { + const request = + consoleClient.knowledgeFs.getKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskId( + { + params: { + documentId: task.documentId, + id: knowledgeSpaceId, + taskId: task.id, + }, + }, + { signal: requestController.signal }, + ) + const deadline = new Promise((_resolve, reject) => { + rejectDeadline = reject + requestTimeout = window.setTimeout(() => { + requestController.abort() + reject(new DOMException('Task snapshot request timed out', 'TimeoutError')) + }, FAILED_TASK_POLL_REQUEST_TIMEOUT) + }) + cancelRequests.add(cancelRequest) + const snapshot = await Promise.race([request, deadline]) + if ( + canceled || + failedTaskPollGenerationsRef.current.get(task.id) !== requestGeneration + ) + return + const currentVersion = currentTaskVersionRef.current.get(task.id) + if (currentVersion && taskVersionIsAfter(currentVersion, snapshot.updatedAt)) return + handleTaskUpdated(snapshot) + } catch (error) { + if ( + canceled || + failedTaskPollGenerationsRef.current.get(task.id) !== requestGeneration + ) + return + if (responseStatus(error) === 403) { + const currentTaskVersion = currentTaskVersionRef.current.get(task.id) + const deniedVersion = + currentTaskVersion && taskVersionIsAfter(currentTaskVersion, task.updatedAt) + ? currentTaskVersion + : task.updatedAt + failedPollAuxiliaryDenialsRef.current.set(task.id, { + taskListGeneration: taskListGenerationRef.current, + taskVersion: deniedVersion, + }) + denyAuxiliaryTaskRead(task.id, deniedVersion) + return + } + if (!taskSnapshotErrorIsTransient(error)) + blockedFailedTaskPollVersionsRef.current.set(task.id, task.updatedAt) + } finally { + if (requestTimeout !== undefined) window.clearTimeout(requestTimeout) + cancelRequests.delete(cancelRequest) + } + }), + ) + } + if (!canceled) timeout = window.setTimeout(() => void pollNextBatch(), 5000) + } + timeout = window.setTimeout(() => void pollNextBatch(), 5000) + return () => { + canceled = true + for (const cancelRequest of cancelRequests) cancelRequest() + cancelRequests.clear() + if (timeout !== undefined) window.clearTimeout(timeout) + } + }, [ + auxiliaryTaskReadGuard, + denyAuxiliaryTaskRead, + failedTaskPollSignature, + handleTaskUpdated, + knowledgeSpaceId, + permissionDenied, + ]) + + const toggleDocument = useCallback( + (documentId: string) => { + if (!canWrite || selectionDisabled) return + setSelectedDocumentIds((current) => { + const next = new Set(current) + if (next.has(documentId)) next.delete(documentId) + else next.add(documentId) + return next + }) + }, + [canWrite, selectionDisabled], + ) + + const toggleAllFiltered = () => { + if (!canWrite || selectionDisabled) return + setSelectedDocumentIds((current) => { + const next = new Set(current) + if (allFilteredSelected) + selectableFilteredDocuments.forEach((document) => next.delete(document.id)) + else selectableFilteredDocuments.forEach((document) => next.add(document.id)) + return next + }) + } + + const retryDependencyQueries = () => { + if (taskQueryBlockingError || sourceQueryBlockingError) + setBlockingDependencyRetries((current) => ({ + sources: current.sources || sourceQueryBlockingError, + tasks: current.tasks || taskQueryBlockingError, + })) + if (tasksQuery.isFetchNextPageError) void tasksQuery.fetchNextPage() + else if (tasksQuery.error || taskQueryBlockingError) void tasksQuery.refetch() + if (sourcesQuery.isFetchNextPageError) void sourcesQuery.fetchNextPage() + else if (sourcesQuery.error || sourceQueryBlockingError) void sourcesQuery.refetch() + } + + const loadMoreResults = () => { + const requests: Promise[] = [] + if (hasNextDocumentPage && !isFetchingNextDocumentPage) requests.push(fetchNextDocumentPage()) + if (hasNextTaskPage && !isFetchingNextTaskPage) requests.push(fetchNextTaskPage()) + if (hasRelevantNextSourcePage && !isFetchingNextSourcePage) requests.push(fetchNextSourcePage()) + void Promise.allSettled(requests) + } + + const handleTaskEventCursor = useCallback((taskId: string, eventId?: string) => { + if (eventId) taskEventCursorsRef.current.set(taskId, eventId) + else taskEventCursorsRef.current.delete(taskId) + }, []) + + const handleTaskStreamPermissionDenied = useCallback( + (taskId: string, taskVersion: string) => { + terminalConfirmableAuxiliaryDenialsRef.current.set(taskId, { + taskListGeneration: taskListGenerationRef.current, + taskVersion, + }) + denyAuxiliaryTaskRead(taskId, taskVersion) + }, + [denyAuxiliaryTaskRead], + ) + + return ( + <> + {streamedActiveTasks.map((task) => { + const observerVersion = taskObserverVersion(task) + return ( + + ) + })} + {canWrite && ( + $['newKnowledge.uploadDocuments'])} + tabIndex={-1} + type="file" + onChange={(event) => { + const files = [...(event.currentTarget.files ?? [])] + event.currentTarget.value = '' + void handleUploadFiles(files) + }} + /> + )} +
{ + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + documentSurfaceHadFocusRef.current = false + if (event.relatedTarget && event.relatedTarget !== uploadInputRef.current) { + writePermissionFocusRecoveryRequestedRef.current = false + writePermissionFocusOriginRef.current = null + } + } + }} + onFocusCapture={(event) => { + if ( + writePermissionFocusRecoveryRequestedRef.current && + event.target !== writePermissionFocusOriginRef.current + ) { + writePermissionFocusRecoveryRequestedRef.current = false + writePermissionFocusOriginRef.current = null + } + documentSurfaceHadFocusRef.current = true + }} + > +
+

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

+

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

+ {permissionPending && ( +

+ {t(($) => $['newKnowledge.permission'])} + {' · '} + {tCommon(($) => $.loading)} +

+ )} + {!permissionPending && !permissionQueryError && (!canEdit || writePermissionRevoked) && ( +

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

+ )} +
+ {permissionQueryError && ( +
+ + {t(($) => $['newKnowledge.permissionLoadFailed'])} + + +
+ )} + {documentsQuery.error && + documentsQuery.data && + !permissionDenied && + !documentsQuery.isFetchNextPageError && ( +
+ + {t(($) => + responseStatus(documentsQuery.error) === 403 + ? $['newKnowledge.documentsPermissionDescription'] + : $['newKnowledge.documentsErrorDescription'], + )} + + {responseStatus(documentsQuery.error) !== 403 && ( + + )} +
+ )} + {!permissionDenied && !dependencyQueryBlockingError && dependencyQueryWarning && ( +
+ + {sourcesQuery.error || sourcesQuery.isFetchNextPageError + ? t(($) => $['newKnowledge.sourcesErrorDescription']) + : t(($) => $['newKnowledge.tasksErrorDescription'])} + + +
+ )} + {documentsQuery.isPending && !permissionDenied ? ( +
+ +
+ ) : permissionDenied || (documentsQuery.error && !documentsQuery.data) ? ( +
+ +

+ {t(($) => + permissionDenied + ? $['newKnowledge.documentsPermissionTitle'] + : $['newKnowledge.documentsErrorTitle'], + )} +

+

+ {t(($) => + permissionDenied + ? $['newKnowledge.documentsPermissionDescription'] + : $['newKnowledge.documentsErrorDescription'], + )} +

+ {(!permissionDenied || + (auxiliaryReadPermissionDenied && !documentPermissionDenied)) && ( + + )} +
+ ) : dependencyQueryBlockingError ? ( +
+ +

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

+ +
+ ) : !documents.length ? ( + { + writePermissionFocusRecoveryRequestedRef.current = true + writePermissionFocusOriginRef.current = document.activeElement as HTMLElement | null + uploadInputRef.current?.click() + }} + onDropFiles={(files) => void handleUploadFiles(files)} + onOpenTasks={() => setTasksOpen(true)} + readOnlyReasonId={documentWriteRestrictionReasonId} + tasksButtonLabel={tasksButtonLabel} + tasksLiveStatus={tasksLiveStatus} + uploading={uploading} + /> + ) : ( + { + writePermissionFocusRecoveryRequestedRef.current = true + writePermissionFocusOriginRef.current = document.activeElement as HTMLElement | null + uploadInputRef.current?.click() + }} + onFilterChange={setFilter} + onLoadMore={loadMoreResults} + onOpenTasks={() => setTasksOpen(true)} + onSearchChange={setSearch} + onSelectAll={toggleAllFiltered} + onSelectDocument={toggleDocument} + readOnlyReasonId={documentWriteRestrictionReasonId} + resultsIncomplete={filteredResultsIncomplete} + search={search} + selectionDisabled={selectionDisabled} + selectedDocumentIds={validSelectedDocumentIds} + someSelected={someFilteredSelected} + sourcesPending={sourceResultsIncomplete} + sourceNames={sourceNames} + statusPending={dependencyResultsIncomplete} + statuses={documentStatuses} + tasksPending={taskResultsIncomplete} + tasksButtonLabel={tasksButtonLabel} + tasksLiveStatus={tasksLiveStatus} + uploading={uploading} + /> + )} +
+ {bulkActionsVisible && ( + setSelectedDocumentIds(new Set())} + onBlurCapture={(event) => { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) + bulkActionsHadFocusRef.current = false + }} + onFocusCapture={() => { + bulkActionsHadFocusRef.current = true + }} + onReindex={() => void handleReindexDocuments()} + reindexing={reindexing} + selectedCount={validSelectedDocumentIds.size} + /> + )} + 0} + isFetchingNextDocumentPage={isFetchingNextDocumentPage} + isFetchingNextTaskPage={isFetchingNextTaskPage} + knowledgeSpaceId={knowledgeSpaceId} + onLoadMoreDocuments={() => void fetchNextDocumentPage()} + onLoadMoreTasks={() => void fetchNextTaskPage()} + onOpenChange={setTasksOpen} + onRefreshDocumentsAndTasks={refreshDocumentsAndTasks} + onRetryPermissionQuery={() => void refreshWorkspacePermissions(true)} + onRetryDocumentQuery={() => { + if (documentsQuery.isFetchNextPageError) void documentsQuery.fetchNextPage() + else void refetchDocumentsQuery() + }} + onRetryTaskQuery={() => { + if (tasksQuery.isFetchNextPageError) void tasksQuery.fetchNextPage() + else void tasksQuery.refetch() + }} + onTaskUpdated={handleTaskUpdated} + onWritePermissionDenied={handleWritePermissionDenied} + open={tasksOpen && !permissionDenied} + permissionQueryError={permissionQueryError} + permissionQueryFetching={workspacePermissionKeysFetching} + permissionQueryPending={permissionPending} + readOnlyReason={ + writePermissionRevoked || (!permissionPending && !permissionQueryError && !canEdit) + ? t(($) => $['newKnowledge.documentPermissionRestricted']) + : undefined + } + taskQueryPending={tasksQuery.isPending} + taskQueryError={Boolean(tasksQuery.error || tasksQuery.isFetchNextPageError)} + taskQueryFetching={tasksQuery.isFetching} + taskProgressStore={taskProgressStore} + tasks={tasks} + /> + + ) +} diff --git a/web/features/new-rag/knowledge-route-placeholder.tsx b/web/features/new-rag/knowledge-route-placeholder.tsx deleted file mode 100644 index 7062325dfb5..00000000000 --- a/web/features/new-rag/knowledge-route-placeholder.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client' - -import { useTranslation } from 'react-i18next' - -export function KnowledgeRoutePlaceholder({ type }: { type: 'documents' | 'sources' }) { - const { t } = useTranslation('dataset') - - return ( -
- -

- {t(($) => (type === 'sources' ? $['newKnowledge.sources'] : $['newKnowledge.documents']))} -

-

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

-
- ) -} diff --git a/web/features/new-rag/processing-tasks-drawer.tsx b/web/features/new-rag/processing-tasks-drawer.tsx new file mode 100644 index 00000000000..cd6ef467899 --- /dev/null +++ b/web/features/new-rag/processing-tasks-drawer.tsx @@ -0,0 +1,636 @@ +'use client' + +import type { + DocumentProcessingTask, + LogicalDocument, +} from '@dify/contracts/knowledge-fs/types.gen' +import type { TaskProgressStore } from './task-progress-store' +import { Button } from '@langgenius/dify-ui/button' +import { + Drawer, + DrawerBackdrop, + DrawerCloseButton, + DrawerContent, + DrawerDescription, + DrawerPopup, + DrawerPortal, + DrawerTitle, + DrawerViewport, +} from '@langgenius/dify-ui/drawer' +import { useMutation } from '@tanstack/react-query' +import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react' +import { useTranslation } from 'react-i18next' +import Loading from '@/app/components/base/loading' +import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' +import { consoleQuery } from '@/service/client' +import { taskCanRetry, taskIsActive, taskVersionIsAfter } from './document-model' + +type TaskAction = 'cancel' | 'retry' + +const TASK_DRAWER_LIMIT = 100 +const noopSubscribe = () => () => undefined + +function taskTime(task: DocumentProcessingTask) { + return task.completedAt ?? task.updatedAt +} + +function taskLifecycle(task: DocumentProcessingTask) { + return `${task.updatedAt}:${task.state}` +} + +function responseStatus(error: unknown): number | undefined { + if (error instanceof Response) return error.status + if (error && typeof error === 'object' && 'status' in error) + return typeof error.status === 'number' ? error.status : undefined + if (error && typeof error === 'object' && 'data' in error) { + const data = error.data + if (data && typeof data === 'object' && 'status' in data) + return typeof data.status === 'number' ? data.status : undefined + } +} + +function compareTaskRecency(left: DocumentProcessingTask, right: DocumentProcessingTask) { + if (taskVersionIsAfter(left.updatedAt, right.updatedAt)) return -1 + if (taskVersionIsAfter(right.updatedAt, left.updatedAt)) return 1 + return right.id.localeCompare(left.id) +} + +function newestTasks( + tasks: DocumentProcessingTask[], + limit: number, + predicate: (task: DocumentProcessingTask) => boolean, +) { + const selected: DocumentProcessingTask[] = [] + for (const task of tasks) { + if (!predicate(task)) continue + let low = 0 + let high = selected.length + while (low < high) { + const middle = Math.floor((low + high) / 2) + if (compareTaskRecency(task, selected[middle]!) < 0) high = middle + else low = middle + 1 + } + selected.splice(low, 0, task) + if (selected.length > limit) selected.pop() + } + return selected +} + +export function ProcessingTasksDrawer({ + canEdit, + actionResultsValid, + documentQueryError, + documentQueryFetching, + documents, + documentsPending, + hasNextDocumentPage, + hasNextTaskPage, + hasUnresolvedTaskDocuments, + isFetchingNextDocumentPage, + isFetchingNextTaskPage, + knowledgeSpaceId, + onLoadMoreDocuments, + onLoadMoreTasks, + onOpenChange, + onRefreshDocumentsAndTasks, + onRetryPermissionQuery, + onTaskUpdated, + onWritePermissionDenied, + open, + permissionQueryError, + permissionQueryFetching, + permissionQueryPending, + readOnlyReason, + taskQueryError, + taskQueryFetching, + taskQueryPending, + tasks, + taskProgressStore, + onRetryTaskQuery, + onRetryDocumentQuery, +}: { + canEdit: boolean + actionResultsValid: boolean + documentQueryError: boolean + documentQueryFetching: boolean + documents: LogicalDocument[] + documentsPending: boolean + hasNextDocumentPage: boolean + hasNextTaskPage: boolean + hasUnresolvedTaskDocuments: boolean + isFetchingNextDocumentPage: boolean + isFetchingNextTaskPage: boolean + knowledgeSpaceId: string + onLoadMoreDocuments: () => void + onLoadMoreTasks: () => void + onOpenChange: (open: boolean) => void + onRefreshDocumentsAndTasks: () => void + onRetryPermissionQuery: () => void + onTaskUpdated: (task: DocumentProcessingTask) => void + onWritePermissionDenied: () => void + open: boolean + permissionQueryError: boolean + permissionQueryFetching: boolean + permissionQueryPending: boolean + readOnlyReason?: string + taskQueryError: boolean + taskQueryFetching: boolean + taskQueryPending: boolean + tasks: DocumentProcessingTask[] + taskProgressStore: TaskProgressStore + onRetryDocumentQuery: () => void + onRetryTaskQuery: () => void +}) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const { formatTimeFromNow } = useFormatTimeFromNow() + const cancelTask = useMutation( + consoleQuery.knowledgeFs.deleteKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskId.mutationOptions(), + ) + const retryTask = useMutation( + consoleQuery.knowledgeFs.postKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskIdRetry.mutationOptions(), + ) + const pendingActionsRef = useRef(new Set()) + const drawerCloseButtonRef = useRef(null) + const taskQueryRetryButtonRef = useRef(null) + const documentQueryRetryButtonRef = useRef(null) + const permissionQueryRetryButtonRef = useRef(null) + const focusedTaskActionRef = useRef(null) + const loadMoreRequestedRef = useRef(false) + const queryRetryFocusRequestedRef = useRef(false) + const permissionRetryFocusRequestedRef = useRef(false) + const loadMoreButtonRef = useRef(null) + const openCycleRef = useRef(0) + const openRef = useRef(open) + const actionResultsValidRef = useRef(actionResultsValid) + const previousOpenRef = useRef(open) + const [pendingActions, setPendingActions] = useState>(() => new Set()) + const [actionErrors, setActionErrors] = useState>({}) + const [visibleTaskLimit, setVisibleTaskLimit] = useState(TASK_DRAWER_LIMIT) + useSyncExternalStore( + open ? taskProgressStore.subscribe : noopSubscribe, + taskProgressStore.getSnapshot, + taskProgressStore.getSnapshot, + ) + const documentTitles = useMemo( + () => new Map(documents.map((document) => [document.id, document.title])), + [documents], + ) + const orderedBaseTasks = useMemo(() => { + if (!open) return [] + const reservedLimit = Math.min(TASK_DRAWER_LIMIT / 2, visibleTaskLimit) + const retryableTasks = newestTasks(tasks, reservedLimit, taskCanRetry) + const activeTasks = newestTasks(tasks, reservedLimit, taskIsActive) + const attentionTaskIds = new Set([ + ...retryableTasks.map((task) => task.id), + ...activeTasks.map((task) => task.id), + ]) + const remainingAttentionTasks = newestTasks( + tasks, + visibleTaskLimit - attentionTaskIds.size, + (task) => (taskCanRetry(task) || taskIsActive(task)) && !attentionTaskIds.has(task.id), + ) + for (const task of remainingAttentionTasks) attentionTaskIds.add(task.id) + const terminalTasks = newestTasks( + tasks, + visibleTaskLimit - attentionTaskIds.size, + (task) => !attentionTaskIds.has(task.id), + ) + return [...retryableTasks, ...activeTasks, ...remainingAttentionTasks, ...terminalTasks].sort( + compareTaskRecency, + ) + }, [open, tasks, visibleTaskLimit]) + const hasMoreTasks = + open && + (tasks.length > orderedBaseTasks.length || + hasNextTaskPage || + (hasUnresolvedTaskDocuments && hasNextDocumentPage)) + const orderedTasks = orderedBaseTasks.map((task) => { + const progress = taskProgressStore.get(task.id) + if (!progress || !taskIsActive(task) || taskVersionIsAfter(task.updatedAt, progress.updatedAt)) + return task + return { + ...task, + errorCode: undefined, + errorMessage: undefined, + ...progress, + } + }) + const activeActionCount = orderedTasks.filter(taskIsActive).length + const retryActionCount = orderedTasks.filter(taskCanRetry).length + const taskLifecycles = useMemo( + () => new Map(tasks.map((task) => [task.id, taskLifecycle(task)])), + [tasks], + ) + const taskLifecycleGenerationsRef = useRef( + new Map(), + ) + useLayoutEffect(() => { + actionResultsValidRef.current = actionResultsValid + const currentTaskIds = new Set(tasks.map((task) => task.id)) + for (const task of tasks) { + const lifecycle = taskLifecycle(task) + const previous = taskLifecycleGenerationsRef.current.get(task.id) + if (previous?.lifecycle === lifecycle) continue + taskLifecycleGenerationsRef.current.set(task.id, { + generation: (previous?.generation ?? 0) + 1, + lifecycle, + }) + } + for (const taskId of taskLifecycleGenerationsRef.current.keys()) { + if (!currentTaskIds.has(taskId)) taskLifecycleGenerationsRef.current.delete(taskId) + } + }, [actionResultsValid, tasks]) + + useEffect(() => { + openRef.current = open + const wasOpen = previousOpenRef.current + previousOpenRef.current = open + if (!wasOpen || open) return + openCycleRef.current += 1 + loadMoreRequestedRef.current = false + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Every committed controlled close resets drawer-local pagination and errors. + setVisibleTaskLimit(TASK_DRAWER_LIMIT) + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Every committed controlled close starts a fresh action-error cycle. + setActionErrors({}) + }, [open]) + + useEffect(() => { + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Task lifecycle changes retire action errors from older task versions. + setActionErrors((current) => { + const staleTaskIds = Object.keys(current).filter( + (taskId) => taskLifecycles.get(taskId) !== current[taskId], + ) + if (!staleTaskIds.length) return current + const next = { ...current } + for (const taskId of staleTaskIds) delete next[taskId] + return next + }) + }, [taskLifecycles]) + + useEffect(() => { + if (!open) return + // oxlint-disable-next-line eslint-react/set-state-in-effect -- A newly opened drawer starts a fresh action-error cycle. + setActionErrors({}) + }, [open]) + + useEffect(() => { + if (!open || hasMoreTasks || !loadMoreRequestedRef.current) return + loadMoreRequestedRef.current = false + drawerCloseButtonRef.current?.focus() + }, [hasMoreTasks, open]) + + useEffect(() => { + const queryRetryVisible = taskQueryError || (documentQueryError && hasUnresolvedTaskDocuments) + if (!open || !queryRetryFocusRequestedRef.current) return + if (queryRetryVisible) { + if (taskQueryError) taskQueryRetryButtonRef.current?.focus() + else documentQueryRetryButtonRef.current?.focus() + return + } + queryRetryFocusRequestedRef.current = false + drawerCloseButtonRef.current?.focus() + }, [documentQueryError, hasUnresolvedTaskDocuments, open, taskQueryError]) + + useEffect(() => { + if (!open || !permissionRetryFocusRequestedRef.current) return + if (permissionQueryError) { + permissionQueryRetryButtonRef.current?.focus() + return + } + permissionRetryFocusRequestedRef.current = false + drawerCloseButtonRef.current?.focus() + }, [open, permissionQueryError]) + + useEffect(() => { + const focusedAction = focusedTaskActionRef.current + if (!open || !focusedAction || focusedAction.isConnected) return + focusedTaskActionRef.current = null + drawerCloseButtonRef.current?.focus() + }, [open, orderedTasks]) + + const performAction = async (task: DocumentProcessingTask, action: TaskAction) => { + if (!canEdit || pendingActionsRef.current.has(task.id)) return + pendingActionsRef.current.add(task.id) + const actionOpenCycle = openCycleRef.current + const actionLifecycleGeneration = taskLifecycleGenerationsRef.current.get(task.id)?.generation + const actionFocusTarget = document.activeElement + setPendingActions((current) => new Set(current).add(task.id)) + setActionErrors((current) => { + const next = { ...current } + delete next[task.id] + return next + }) + try { + const input = { + params: { + documentId: task.documentId, + id: knowledgeSpaceId, + taskId: task.id, + }, + } + const updated = + action === 'cancel' + ? await cancelTask.mutateAsync(input) + : await retryTask.mutateAsync(input) + if ( + !actionResultsValidRef.current || + taskLifecycleGenerationsRef.current.get(task.id)?.generation !== actionLifecycleGeneration + ) + return + onTaskUpdated(updated) + setActionErrors((current) => { + const next = { ...current } + delete next[task.id] + return next + }) + if ( + actionResultsValidRef.current && + openRef.current && + openCycleRef.current === actionOpenCycle && + document.activeElement === actionFocusTarget + ) + drawerCloseButtonRef.current?.focus() + } catch (error) { + const permissionDenied = responseStatus(error) === 403 + if (permissionDenied) onWritePermissionDenied() + if ( + !permissionDenied && + actionResultsValidRef.current && + openRef.current && + openCycleRef.current === actionOpenCycle && + taskLifecycleGenerationsRef.current.get(task.id)?.generation === actionLifecycleGeneration + ) + setActionErrors((current) => ({ ...current, [task.id]: taskLifecycle(task) })) + } finally { + pendingActionsRef.current.delete(task.id) + setPendingActions((current) => { + const next = new Set(current) + next.delete(task.id) + return next + }) + if (actionResultsValidRef.current) onRefreshDocumentsAndTasks() + } + } + + return ( + { + openRef.current = nextOpen + onOpenChange(nextOpen) + }} + > + + + + + +
+
+ + {t(($) => $['newKnowledge.backgroundTasks'])} + + $['operation.close'])} + className="size-6 rounded-md" + /> +
+ + {t(($) => $['newKnowledge.backgroundTasksDescription'])} + + {readOnlyReason && ( +

+ + {readOnlyReason} +

+ )} + {permissionQueryPending && ( +

+ {t(($) => $['newKnowledge.permission'])} + {' · '} + {tCommon(($) => $.loading)} +

+ )} +
+
+ {permissionQueryError && ( +
+

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

+ +
+ )} + {taskQueryError && ( +
+

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

+ +
+ )} + {documentQueryError && hasUnresolvedTaskDocuments && ( +
+

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

+ +
+ )} + {taskQueryPending && !orderedTasks.length ? ( +
+ +
+ ) : orderedTasks.length ? ( +
    + {orderedTasks.map((task) => { + const title = + documentTitles.get(task.documentId) ?? + (documentsPending + ? t(($) => $['newKnowledge.documentColumn']) + : task.documentId) + const timestamp = Date.parse( + taskIsActive(task) ? task.createdAt : taskTime(task), + ) + const taskError = task.errorMessage ?? task.errorCode + const actionTarget = `${documentTitles.get(task.documentId) ?? task.documentId} · ${task.id}` + return ( +
  • + +
    +

    + {t(($) => $['newKnowledge.processDocument'], { name: title })} +

    +

    + {t(($) => $[`newKnowledge.processingTaskState.${task.state}`], { + progress: task.progressPercent, + })} + {!Number.isNaN(timestamp) && ( + <> + · + {formatTimeFromNow(timestamp)} + + )} +

    + {taskError && ( +

    + {taskError} +

    + )} + {actionErrors[task.id] === taskLifecycle(task) && ( +

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

    + )} +
    + {canEdit && taskIsActive(task) ? ( + + ) : canEdit && taskCanRetry(task) ? ( + + ) : null} +
  • + ) + })} +
+ ) : !taskQueryError && !hasMoreTasks ? ( +

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

+ ) : null} + {hasMoreTasks && ( +
+ +
+ )} +
+
+
+
+
+
+ ) +} diff --git a/web/features/new-rag/services/processing-task-events.ts b/web/features/new-rag/services/processing-task-events.ts new file mode 100644 index 00000000000..01154f741d0 --- /dev/null +++ b/web/features/new-rag/services/processing-task-events.ts @@ -0,0 +1,51 @@ +import type { DocumentProcessingTaskEvent } from '@dify/contracts/knowledge-fs/types.gen' +import { getEventMeta } from '@orpc/client' +import { consoleClient } from '@/service/client' + +type ProcessingTaskEventWithId = T & { + id: string +} + +export type ProcessingTaskProgressEvent = ProcessingTaskEventWithId< + Extract +> + +type ProcessingTaskTerminalEvent = ProcessingTaskEventWithId< + Extract +> + +export type ProcessingTaskEvent = ProcessingTaskProgressEvent | ProcessingTaskTerminalEvent + +type StreamProcessingTaskEventsInput = { + documentId: string + knowledgeSpaceId: string + lastEventId?: string + signal?: AbortSignal + taskId: string +} + +export async function* streamProcessingTaskEvents( + input: StreamProcessingTaskEventsInput, +): AsyncGenerator { + const events = + await consoleClient.knowledgeFs.getKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskIdEvents( + { + headers: input.lastEventId ? { 'last-event-id': input.lastEventId } : undefined, + params: { + documentId: input.documentId, + id: input.knowledgeSpaceId, + taskId: input.taskId, + }, + }, + { + context: { silent: true }, + signal: input.signal, + }, + ) + + for await (const event of events) { + const id = getEventMeta(event)?.id + if (!id) throw new Error('KnowledgeFS processing task event is missing an event id') + yield { ...event, id } + } +} diff --git a/web/features/new-rag/task-event-observer.tsx b/web/features/new-rag/task-event-observer.tsx new file mode 100644 index 00000000000..10addb634d2 --- /dev/null +++ b/web/features/new-rag/task-event-observer.tsx @@ -0,0 +1,128 @@ +'use client' + +import type { ProcessingTaskEvent } from './services/processing-task-events' +import { useEffect, useLayoutEffect, useRef } from 'react' +import { taskVersionIsAfter } from './document-model' +import { streamProcessingTaskEvents } from './services/processing-task-events' + +const TASK_EVENT_RECONNECT_DELAY = 1000 +const TASK_EVENT_MAX_RECONNECT_DELAY = 30000 + +function waitForTaskEventReconnect(signal: AbortSignal, delay: number) { + if (signal.aborted) return Promise.resolve() + return new Promise((resolve) => { + let settled = false + const timeout = window.setTimeout(finish, delay) + signal.addEventListener('abort', finish, { once: true }) + if (signal.aborted) finish() + + function finish() { + if (settled) return + settled = true + window.clearTimeout(timeout) + signal.removeEventListener('abort', finish) + resolve() + } + }) +} + +function responseStatus(error: unknown): number | undefined { + if (error instanceof Response) return error.status + if (error && typeof error === 'object' && 'status' in error) + return typeof error.status === 'number' ? error.status : undefined + if (error && typeof error === 'object' && 'data' in error) { + const data = error.data + if (data && typeof data === 'object' && 'status' in data) + return typeof data.status === 'number' ? data.status : undefined + } +} + +export function TaskEventObserver({ + documentId, + knowledgeSpaceId, + lastEventId, + onEvent, + onLastEventIdChange, + onPermissionDenied, + taskId, + taskVersion, +}: { + documentId: string + knowledgeSpaceId: string + lastEventId?: string + onEvent: (taskId: string, taskVersion: string, event: ProcessingTaskEvent) => boolean + onLastEventIdChange: (taskId: string, eventId?: string) => void + onPermissionDenied: (taskId: string, taskVersion: string) => void + taskId: string + taskVersion: string +}) { + const resumeEventIdRef = useRef(lastEventId) + useLayoutEffect(() => { + resumeEventIdRef.current = lastEventId + }, [lastEventId]) + const latestTaskVersionRef = useRef(taskVersion) + const streamTaskVersionRef = useRef(taskVersion) + useLayoutEffect(() => { + if (taskVersionIsAfter(taskVersion, latestTaskVersionRef.current)) + latestTaskVersionRef.current = taskVersion + }, [taskVersion]) + + useEffect(() => { + const controller = new AbortController() + void (async () => { + let reconnectDelay = TASK_EVENT_RECONNECT_DELAY + while (!controller.signal.aborted) { + if (taskVersionIsAfter(latestTaskVersionRef.current, streamTaskVersionRef.current)) + streamTaskVersionRef.current = latestTaskVersionRef.current + try { + for await (const event of streamProcessingTaskEvents({ + documentId, + knowledgeSpaceId, + lastEventId: resumeEventIdRef.current, + signal: controller.signal, + taskId, + })) { + if (controller.signal.aborted) return + resumeEventIdRef.current = event.id + onLastEventIdChange(taskId, event.id) + const acceptedTaskVersion = streamTaskVersionRef.current + const eventTaskVersion = + event.event === 'progress' ? event.data.updatedAt : acceptedTaskVersion + const accepted = onEvent(taskId, eventTaskVersion, event) + if (!accepted) { + resumeEventIdRef.current = undefined + onLastEventIdChange(taskId) + streamTaskVersionRef.current = taskVersionIsAfter( + latestTaskVersionRef.current, + acceptedTaskVersion, + ) + ? latestTaskVersionRef.current + : acceptedTaskVersion + break + } + streamTaskVersionRef.current = eventTaskVersion + reconnectDelay = TASK_EVENT_RECONNECT_DELAY + if (event.event === 'terminal') { + resumeEventIdRef.current = undefined + onLastEventIdChange(taskId) + return + } + } + } catch (error) { + if (controller.signal.aborted) return + if (responseStatus(error) === 403) { + if (taskVersionIsAfter(latestTaskVersionRef.current, streamTaskVersionRef.current)) + streamTaskVersionRef.current = latestTaskVersionRef.current + onPermissionDenied(taskId, streamTaskVersionRef.current) + return + } + } + await waitForTaskEventReconnect(controller.signal, reconnectDelay) + reconnectDelay = Math.min(reconnectDelay * 2, TASK_EVENT_MAX_RECONNECT_DELAY) + } + })() + return () => controller.abort() + }, [documentId, knowledgeSpaceId, onEvent, onLastEventIdChange, onPermissionDenied, taskId]) + + return null +} diff --git a/web/features/new-rag/task-progress-store.ts b/web/features/new-rag/task-progress-store.ts new file mode 100644 index 00000000000..675497f9050 --- /dev/null +++ b/web/features/new-rag/task-progress-store.ts @@ -0,0 +1,53 @@ +import type { ProcessingTaskProgressEvent } from './services/processing-task-events' +import { taskVersionIsAfter } from './document-model' + +type TaskProgress = ProcessingTaskProgressEvent['data'] +type Listener = () => void + +export type TaskProgressStore = { + delete: (taskId: string) => void + get: (taskId: string) => TaskProgress | undefined + getSnapshot: () => number + retain: (taskIds: Set) => void + set: (taskId: string, progress: TaskProgress) => void + subscribe: (listener: Listener) => () => void +} + +export function createTaskProgressStore(): TaskProgressStore { + const progressByTaskId = new Map() + const listeners = new Set() + let revision = 0 + + const emit = () => { + revision += 1 + for (const listener of listeners) listener() + } + + return { + delete(taskId) { + if (!progressByTaskId.delete(taskId)) return + emit() + }, + get: (taskId) => progressByTaskId.get(taskId), + getSnapshot: () => revision, + retain(taskIds) { + let changed = false + for (const taskId of progressByTaskId.keys()) { + if (taskIds.has(taskId)) continue + progressByTaskId.delete(taskId) + changed = true + } + if (changed) emit() + }, + set(taskId, progress) { + const current = progressByTaskId.get(taskId) + if (current && taskVersionIsAfter(current.updatedAt, progress.updatedAt)) return + progressByTaskId.set(taskId, progress) + emit() + }, + subscribe(listener) { + listeners.add(listener) + return () => listeners.delete(listener) + }, + } +} diff --git a/web/features/new-rag/use-query-data-update-count.ts b/web/features/new-rag/use-query-data-update-count.ts new file mode 100644 index 00000000000..414d1d65da1 --- /dev/null +++ b/web/features/new-rag/use-query-data-update-count.ts @@ -0,0 +1,26 @@ +'use client' + +import type { QueryClient, QueryKey } from '@tanstack/react-query' +import { hashKey } from '@tanstack/react-query' +import { useCallback, useSyncExternalStore } from 'react' + +export function useQueryDataUpdateCount(queryClient: QueryClient, queryKey: QueryKey) { + const queryHash = hashKey(queryKey) + const subscribe = useCallback( + (onStoreChange: () => void) => + queryClient.getQueryCache().subscribe((event) => { + if ( + event.type === 'updated' && + event.action.type === 'success' && + event.query.queryHash === queryHash + ) + onStoreChange() + }), + [queryClient, queryHash], + ) + const getSnapshot = useCallback( + () => queryClient.getQueryState(queryKey)?.dataUpdateCount ?? 0, + [queryClient, queryKey], + ) + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) +} diff --git a/web/i18n/ar-TN/dataset.json b/web/i18n/ar-TN/dataset.json index c71f2673509..ac24aadb012 100644 --- a/web/i18n/ar-TN/dataset.json +++ b/web/i18n/ar-TN/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "مطلوب نموذج إعادة الترتيب لخلط المعرفة الداخلية والخارجية.", "multimodal": "متعدد الوسائط", "newKnowledge.actionsColumn": "الإجراءات", + "newKnowledge.addDocument": "أضف وثيقة", "newKnowledge.addSource": "أضف المصدر", "newKnowledge.addSourceDescription": "قم بتوصيل مصدر بقاعدة المعرفة هذه - تتم مزامنته تلقائيًا ويظل محدثًا.", "newKnowledge.addSourceFailed": "تعذّرت إضافة هذا المصدر. راجع الإعدادات وحاول مرة أخرى.", "newKnowledge.addSourceRequiresSelection": "حدّد صفحة واحدة على الأقل من الصفحات التي تم الزحف إليها قبل إضافة المصدر.", "newKnowledge.adjustAndRecrawl": "الضبط وإعادة الزحف", + "newKnowledge.allDocumentStatuses": "كل الوضع", "newKnowledge.allSources": "جميع المصادر", "newKnowledge.appsUnavailable": "— تطبيقات", "newKnowledge.authKind.api-key": "مفتاح API", "newKnowledge.authKind.endpoint": "نقطة النهاية", "newKnowledge.authenticationMethod": "طريقة المصادقة", "newKnowledge.backToList": "العودة إلى قواعد المعرفة", + "newKnowledge.backgroundTasks": "مهام الخلفية", + "newKnowledge.backgroundTasksDescription": "يضيف المستند، ويتزامن المصدر، ويعيد فهرسة المهام.", "newKnowledge.booleanFalse": "لا", "newKnowledge.booleanTrue": "نعم", + "newKnowledge.bulkDocumentActions": "إجراءات المستندات المجمعة", "newKnowledge.cancelAddSource": "إلغاء", "newKnowledge.cardType": "مساحة معرفة", + "newKnowledge.clearDocumentSelection": "مسح تحديد المستندات", "newKnowledge.configureProvider": "تكوين {{provider}}", "newKnowledge.connectNotion": "ربط Notion", "newKnowledge.connectProvider": "توصيل {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "المنشئون", "newKnowledge.customIntervalHours": "فاصل زمني مخصّص (بالساعات)", "newKnowledge.customIntervalInvalid": "أدخل عددًا صحيحًا من 1 إلى 720 ساعة.", + "newKnowledge.deleteDocuments": "حذف...", "newKnowledge.description": "الوصف", "newKnowledge.descriptionHelp": "يساعد الوصف الواضح التطبيقات على تحديد وقت البحث في هذه المعرفة.", "newKnowledge.descriptionPlaceholder": "على سبيل المثال سياسة استرداد الأموال، والأسئلة الشائعة حول الشحن، وخطوات التصعيد - تُستخدم لطرح أسئلة دعم العملاء", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "ستتم إزالة إعداد المصدر ومعاينة الزحف.", "newKnowledge.discardSourceDraftFailed": "تعذّر تجاهل مسودة المصدر. أعد المحاولة.", "newKnowledge.discardSourceDraftTitle": "هل تريد تجاهل مسودة المصدر هذه؟", + "newKnowledge.documentActions": "إجراءات {{name}}", + "newKnowledge.documentActionsUnavailable": "إجراءات المستند غير متاحة لقاعدة المعرفة هذه حتى الآن.", + "newKnowledge.documentColumn": "وثيقة", + "newKnowledge.documentFilterLabel": "تصفية المستندات حسب الحالة", + "newKnowledge.documentPermissionRestricted": "لا تسمح أذونات مساحة العمل الخاصة بك بإجراءات المستندات أو المهام.", + "newKnowledge.documentStatus.disabled": "معطل", + "newKnowledge.documentStatus.failed": "فشل", + "newKnowledge.documentStatus.processing": "قيد المعالجة", + "newKnowledge.documentStatus.queued": "في قائمة الانتظار", + "newKnowledge.documentStatus.ready": "جاهز", + "newKnowledge.documentUploadExclusion.batchLimit": "تم تجاوز الحد الأقصى لحجم الدفعة", + "newKnowledge.documentUploadExclusion.countLimit": "تم تجاوز الحد الأقصى لعدد الملفات", + "newKnowledge.documentUploadExclusion.fileSize": "الملف كبير جدًا", + "newKnowledge.documentUploadExclusion.fileType": "نوع الملف غير مدعوم أو غير صالح", + "newKnowledge.documentUploadExclusion.more": "و{{count}} أخرى", + "newKnowledge.documentUploadExclusion.processing": "فشلت المعالجة", + "newKnowledge.documentUploadExclusion.quota": "تم تجاوز حصة مساحة العمل", + "newKnowledge.documentUploadExclusion.target": "لم يعد هدف المستند صالحًا", + "newKnowledge.documentUploadFailed": "لم نتمكن من تحميل هذه المستندات. حاول ثانية.", + "newKnowledge.documentUploadPartial": "بدأت معالجة {{accepted}} مستندًا؛ تعذرت إضافة {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "لم يتم قبول أي مستند: {{details}}", + "newKnowledge.documentUploadStarted": "بدأت معالجة المستندات.", "newKnowledge.documents": "المستندات", + "newKnowledge.documentsDescription": "الأصول المعرفية ومراجعاتها - من أين جاءت كل وثيقة وما إذا كانت المراجعة الأخيرة مفهرسة أم لا.", + "newKnowledge.documentsDropHint": "أو قم بإسقاط PDF أو DOCX أو Markdown أو HTML أو XLSX أو TXT للتحميل", + "newKnowledge.documentsEmptyDescription": "قم بتحميل الملفات هنا، أو قم بتوصيل مصدر في علامة التبويب \"المصادر\" - يتم تحليل المستندات وتجميعها وفهرستها تلقائيًا.", + "newKnowledge.documentsEmptyTitle": "لا توجد وثائق حتى الآن", + "newKnowledge.documentsErrorDescription": "لم نتمكن من تحميل المستندات. حاول ثانية.", + "newKnowledge.documentsErrorTitle": "تعذر تحميل المستندات", + "newKnowledge.documentsPermissionDescription": "ليس لديك إذن لعرض المستندات في قاعدة المعرفة هذه.", + "newKnowledge.documentsPermissionTitle": "الوثائق مقيدة", + "newKnowledge.documentsReindexFailed": "لم نتمكن من البدء في إعادة الفهرسة. حاول ثانية.", + "newKnowledge.documentsReindexPartial": "مستندات {{queued}} في قائمة الانتظار؛ {{missing}} لم تعد موجودة.", + "newKnowledge.documentsReindexStarted": "بدأت عملية إعادة الفهرسة.", + "newKnowledge.documentsSelected": "تم تحديد وثائق {{count}}", "newKnowledge.documentsUnavailable": "— مستندات", + "newKnowledge.downloadDocuments": "تحميل", "newKnowledge.editSource": "تحرير", "newKnowledge.emptyDescription": "أضف مستنداتك ليتمكن الوكلاء من قراءتها مباشرة — من دون إعداد الفهرسة.", "newKnowledge.emptyTitle": "طريقة أذكى لبناء قواعد المعرفة", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "تعرّف على قاعدة المعرفة الجديدة ✨", "newKnowledge.illustrationHeadline": "اربط محتواك مرة واحدة — ليجيب كل تطبيق بأحدث معرفة.", "newKnowledge.includeSubpages": "تضمين الصفحات الفرعية", + "newKnowledge.interruptTask": "إيقاف", "newKnowledge.invalidRootUrl": "أدخل عنوان URL صالحًا يبدأ بـ http(s).", "newKnowledge.keepEditing": "متابعة التحرير", + "newKnowledge.lastReadyRevisionHint": "لا تؤدي المعالجة الفاشلة إلى مقاطعة الاسترداد مطلقًا، حيث تظل آخر مراجعة جاهزة متاحة في فهرس المحتوى.", "newKnowledge.lastSyncColumn": "آخر مزامنة", "newKnowledge.learnMore": "معرفة المزيد", "newKnowledge.leavePartialSetupConfirm": "مغادرة الإعداد", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "قديم", "newKnowledge.loadMore": "تحميل المزيد", "newKnowledge.loadingSyncPolicy": "جارٍ تحميل سياسة المزامنة", + "newKnowledge.manualUpload": "التحميل اليدوي", "newKnowledge.maxPages": "الحد الأقصى للصفحات", + "newKnowledge.metadata": "بيانات التعريف", "newKnowledge.moreProviders": "مزوّدون آخرون", "newKnowledge.name": "اسم المعرفة", "newKnowledge.namePlaceholder": "على سبيل المثال دليل دعم العملاء", "newKnowledge.nameRequired": "أدخل اسم قاعدة المعرفة.", "newKnowledge.new": "جديد", + "newKnowledge.noBackgroundTasks": "لا توجد مهام خلفية حتى الآن.", "newKnowledge.noDescription": "لا يوجد وصف", + "newKnowledge.noMatchingDocuments": "لا توجد وثائق تطابق هذا البحث.", "newKnowledge.noMatchingSources": "لا توجد مصادر تطابق هذا البحث.", "newKnowledge.noPagesFound": "لم يتم العثور على صفحات في {{host}}", "newKnowledge.noPagesFoundDescription": "وصل برنامج الزحف إلى الموقع لكنه لم يجد صفحات قابلة للفهرسة. جرّب عنوان URL مختلفًا أو وسّع خيارات الزحف.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "تم الزحف إلى {{count}} صفحة في {{host}}", "newKnowledge.pagesCrawled_other": "تم الزحف إلى {{count}} صفحة في {{host}}", "newKnowledge.pagesSelected": "تم تحديد {{count}}", + "newKnowledge.partialDocumentResults": "قد تتطابق مستندات أخرى. حمّل المزيد لمواصلة البحث.", "newKnowledge.permission": "إذن", "newKnowledge.permissionAllMembers": "مساحة العمل · يمكن لجميع الأعضاء العرض والتحرير", + "newKnowledge.permissionLoadFailed": "تعذر التحقق من أذونات التحرير. حاول مرة أخرى.", "newKnowledge.permissionOnlyMe": "أنا فقط", "newKnowledge.permissionRestricted": "لا تسمح أذونات مساحة العمل الخاصة بك بتغيير هذا الإعداد.", "newKnowledge.permissionUpdateFailed": "تم إنشاء قاعدة المعرفة، لكن تعذّر تأكيد أذوناتها. أعد المحاولة لإكمال الإعداد.", "newKnowledge.preview": "معاينة", "newKnowledge.previewUnavailable": "تتوفر المعاينة بعد أن تنتهي الواجهة الخلفية من معالجة هذا الملف.", + "newKnowledge.processDocument": "عملية {{name}}", + "newKnowledge.processingTaskState.canceled": "تمت مقاطعته", + "newKnowledge.processingTaskState.dispatch_pending": "تحضير", + "newKnowledge.processingTaskState.failed": "فشل", + "newKnowledge.processingTaskState.queued": "في قائمة الانتظار", + "newKnowledge.processingTaskState.retry_wait": "في انتظار إعادة المحاولة", + "newKnowledge.processingTaskState.running": "المعالجة · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "مكتمل", + "newKnowledge.processingTaskState.superseded": "تم استبداله", "newKnowledge.providerConnected": "تم توصيل {{provider}}", "newKnowledge.providerLabel": "اختر مزودًا", "newKnowledge.providerLoadFailed": "لم يتمكن الموفرون من التحميل", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "اطلب من محرر مساحة العمل إنشاء أول قاعدة معرفة.", "newKnowledge.refreshConnectionStatus": "تحديث الحالة", "newKnowledge.refreshingConnection": "منعش…", + "newKnowledge.reindexDocuments": "إعادة الفهرسة", "newKnowledge.removeSource": "قم بإزالة المصدر", "newKnowledge.retryCrawl": "إعادة محاولة الزحف", "newKnowledge.retryProviderLoad": "أعد المحاولة", "newKnowledge.retrySyncPolicy": "إعادة محاولة سياسة المزامنة", + "newKnowledge.retryTask": "أعد المحاولة", "newKnowledge.rootUrl": "عنوان URL الجذر", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "سيصبح هذا القسم متاحًا في خطوة الإعداد التالية.", + "newKnowledge.searchDocuments": "البحث في المستندات", "newKnowledge.searchSources": "مصادر البحث", "newKnowledge.selectAll": "تحديد الكل", + "newKnowledge.selectAllDocuments": "حدد كافة المستندات", "newKnowledge.selectAllSources": "تحديد كل المصادر", "newKnowledge.selectFilesAndFolders": "حدد الملفات والمجلدات للمزامنة", "newKnowledge.skippedFailed": "تم التخطي (فشل)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "يدوي فقط", "newKnowledge.syncPolicyProvider": "استخدام جدول المزوّد", "newKnowledge.tags": "الوسوم", + "newKnowledge.taskActionFailed": "لم نتمكن من تحديث هذه المهمة. قم بالتحديث وحاول مرة أخرى.", + "newKnowledge.taskAttentionClear": "لا توجد مهام في الخلفية تحتاج إلى الاهتمام.", + "newKnowledge.taskAttentionCount": "مهام الخلفية {{count}} تحتاج إلى الاهتمام.", + "newKnowledge.taskAttentionErrorCount": "تحتاج مهام الخلفية {{count}} إلى الاهتمام، بما في ذلك المهام الفاشلة.", + "newKnowledge.taskFailedNotification": "فشلت معالجة المستند — أعد المحاولة من المهام", + "newKnowledge.taskHistoryIncomplete": "يتوفر المزيد من سجل المهام؛ عدد المهام التي تتطلب الانتباه غير مكتمل.", + "newKnowledge.tasks": "المهام", + "newKnowledge.tasksErrorDescription": "لم نتمكن من تحميل مهام الخلفية. حاول ثانية.", + "newKnowledge.tasksWithAttention": "المهام، {{count}} تحتاج إلى الاهتمام", "newKnowledge.unavailableDescription": "KnowledgeFS غير متاح لمساحة العمل هذه حاليًا.", "newKnowledge.unavailableTitle": "المعرفة الجديدة غير متاحة", "newKnowledge.updated": "تم التحديث في {{date}}", + "newKnowledge.updatedColumn": "تم التحديث", "newKnowledge.uploadCharactersUnavailable": "الأحرف: —", + "newKnowledge.uploadDocuments": "تحميل المستندات", "newKnowledge.uploadFiles": "رفع الملفات", "newKnowledge.uploadFilesDescription": "أضف ملفات من جهازك — ستتم فهرستها بمجرد إنشائها.", "newKnowledge.uploadingFiles": "جارٍ الرفع…", diff --git a/web/i18n/de-DE/dataset.json b/web/i18n/de-DE/dataset.json index 4b0f8fc5c61..4c0361d195c 100644 --- a/web/i18n/de-DE/dataset.json +++ b/web/i18n/de-DE/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Das Rerank-Modell ist für die Mischung von internem und externem Wissen erforderlich.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Aktionen", + "newKnowledge.addDocument": "Dokument hinzufügen", "newKnowledge.addSource": "Quelle hinzufügen", "newKnowledge.addSourceDescription": "Verbinden Sie eine Quelle mit dieser Wissensdatenbank – sie wird automatisch synchronisiert und bleibt aktuell.", "newKnowledge.addSourceFailed": "Diese Quelle konnte nicht hinzugefügt werden. Prüfe die Einstellungen und versuche es erneut.", "newKnowledge.addSourceRequiresSelection": "Wähle mindestens eine gecrawlte Seite aus, bevor du die Quelle hinzufügst.", "newKnowledge.adjustAndRecrawl": "Anpassen und erneut crawlen", + "newKnowledge.allDocumentStatuses": "Alle Status", "newKnowledge.allSources": "Alle Quellen", "newKnowledge.appsUnavailable": "— Apps", "newKnowledge.authKind.api-key": "API-Schlüssel", "newKnowledge.authKind.endpoint": "Endpunkt", "newKnowledge.authenticationMethod": "Authentifizierungsmethode", "newKnowledge.backToList": "Zurück zu den Wissensdatenbanken", + "newKnowledge.backgroundTasks": "Hintergrundaufgaben", + "newKnowledge.backgroundTasksDescription": "Dokumenthinzufügungen, Quellsynchronisierungen und Neuindizierungsaufträge.", "newKnowledge.booleanFalse": "Nein", "newKnowledge.booleanTrue": "Ja", + "newKnowledge.bulkDocumentActions": "Massendokumentaktionen", "newKnowledge.cancelAddSource": "Abbrechen", "newKnowledge.cardType": "Wissensbereich", + "newKnowledge.clearDocumentSelection": "Dokumentauswahl löschen", "newKnowledge.configureProvider": "{{provider}} konfigurieren", "newKnowledge.connectNotion": "Notion verbinden", "newKnowledge.connectProvider": "Verbinden Sie {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Ersteller", "newKnowledge.customIntervalHours": "Benutzerdefiniertes Intervall (Stunden)", "newKnowledge.customIntervalInvalid": "Gib eine ganze Zahl zwischen 1 und 720 Stunden ein.", + "newKnowledge.deleteDocuments": "Löschen…", "newKnowledge.description": "Beschreibung", "newKnowledge.descriptionHelp": "Eine klare Beschreibung hilft Apps bei der Entscheidung, wann dieses Wissen durchsucht werden soll.", "newKnowledge.descriptionPlaceholder": "z.B. Rückerstattungsrichtlinien, häufig gestellte Fragen zum Versand und Eskalationsschritte – für Fragen zum Kundensupport verwenden", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Die Quellenkonfiguration und die Crawl-Vorschau werden entfernt.", "newKnowledge.discardSourceDraftFailed": "Der Quellenentwurf konnte nicht verworfen werden. Versuche es erneut.", "newKnowledge.discardSourceDraftTitle": "Diesen Quellenentwurf verwerfen?", + "newKnowledge.documentActions": "Aktionen für {{name}}", + "newKnowledge.documentActionsUnavailable": "Für diese Wissensdatenbank sind noch keine Dokumentaktionen verfügbar.", + "newKnowledge.documentColumn": "Dokument", + "newKnowledge.documentFilterLabel": "Dokumente nach Status filtern", + "newKnowledge.documentPermissionRestricted": "Ihre Workspace-Berechtigungen erlauben keine Dokument- oder Aufgabenaktionen.", + "newKnowledge.documentStatus.disabled": "Deaktiviert", + "newKnowledge.documentStatus.failed": "Fehlgeschlagen", + "newKnowledge.documentStatus.processing": "Verarbeitung", + "newKnowledge.documentStatus.queued": "In der Warteschlange", + "newKnowledge.documentStatus.ready": "Bereit", + "newKnowledge.documentUploadExclusion.batchLimit": "Maximale Batchgröße überschritten", + "newKnowledge.documentUploadExclusion.countLimit": "Maximale Dateianzahl überschritten", + "newKnowledge.documentUploadExclusion.fileSize": "Datei ist zu groß", + "newKnowledge.documentUploadExclusion.fileType": "Nicht unterstützter oder ungültiger Dateityp", + "newKnowledge.documentUploadExclusion.more": "und {{count}} weitere", + "newKnowledge.documentUploadExclusion.processing": "Verarbeitung fehlgeschlagen", + "newKnowledge.documentUploadExclusion.quota": "Arbeitsbereichskontingent überschritten", + "newKnowledge.documentUploadExclusion.target": "Dokumentziel ist nicht mehr gültig", + "newKnowledge.documentUploadFailed": "Wir konnten diese Dokumente nicht hochladen. Versuchen Sie es erneut.", + "newKnowledge.documentUploadPartial": "{{accepted}} Dokumente wurden gestartet; {{excluded}} konnten nicht hinzugefügt werden: {{details}}", + "newKnowledge.documentUploadRejected": "Keine Dokumente wurden akzeptiert: {{details}}", + "newKnowledge.documentUploadStarted": "Die Dokumentenverarbeitung wurde gestartet.", "newKnowledge.documents": "Dokumente", + "newKnowledge.documentsDescription": "Wissensbestände und ihre Überarbeitungen – woher jedes Dokument stammt und ob die neueste Überarbeitung indiziert ist.", + "newKnowledge.documentsDropHint": "oder laden Sie PDF, DOCX, Markdown, HTML, XLSX oder TXT hoch", + "newKnowledge.documentsEmptyDescription": "Laden Sie hier Dateien hoch oder verbinden Sie eine Quelle auf der Registerkarte „Quellen“ – Dokumente werden automatisch analysiert, segmentiert und indiziert.", + "newKnowledge.documentsEmptyTitle": "Noch keine Dokumente", + "newKnowledge.documentsErrorDescription": "Wir konnten keine Dokumente laden. Versuchen Sie es erneut.", + "newKnowledge.documentsErrorTitle": "Dokumente konnten nicht geladen werden", + "newKnowledge.documentsPermissionDescription": "Sie sind nicht berechtigt, Dokumente in dieser Wissensdatenbank anzuzeigen.", + "newKnowledge.documentsPermissionTitle": "Dokumente sind eingeschränkt", + "newKnowledge.documentsReindexFailed": "Wir konnten nicht mit der Neuindizierung beginnen. Versuchen Sie es erneut.", + "newKnowledge.documentsReindexPartial": "{{queued}} Dokumente in der Warteschlange; {{missing}} existiert nicht mehr.", + "newKnowledge.documentsReindexStarted": "Die Neuindizierung wurde gestartet.", + "newKnowledge.documentsSelected": "{{count}} Dokumente ausgewählt", "newKnowledge.documentsUnavailable": "— Dokumente", + "newKnowledge.downloadDocuments": "Herunterladen", "newKnowledge.editSource": "Bearbeiten", "newKnowledge.emptyDescription": "Fügen Sie Ihre Dokumente hinzu, damit Agents sie direkt lesen können — ohne eine Indizierung einzurichten.", "newKnowledge.emptyTitle": "Wissensdatenbanken intelligenter aufbauen", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Lernen Sie die neue Wissensdatenbank kennen ✨", "newKnowledge.illustrationHeadline": "Verbinde deine Inhalte einmal – und jede App antwortet mit dem neuesten Wissen.", "newKnowledge.includeSubpages": "Unterseiten einbeziehen", + "newKnowledge.interruptTask": "Unterbrechen", "newKnowledge.invalidRootUrl": "Gib eine gültige http(s)-URL ein.", "newKnowledge.keepEditing": "Weiter bearbeiten", + "newKnowledge.lastReadyRevisionHint": "Eine fehlgeschlagene Verarbeitung unterbricht nie den Abruf – die letzte fertige Revision bleibt im Inhaltsindex verfügbar.", "newKnowledge.lastSyncColumn": "Letzte Synchronisierung", "newKnowledge.learnMore": "Mehr erfahren", "newKnowledge.leavePartialSetupConfirm": "Einrichtung verlassen", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Legacy", "newKnowledge.loadMore": "Mehr laden", "newKnowledge.loadingSyncPolicy": "Synchronisierungsrichtlinie wird geladen", + "newKnowledge.manualUpload": "Manueller Upload", "newKnowledge.maxPages": "Maximale Seitenzahl", + "newKnowledge.metadata": "Metadaten", "newKnowledge.moreProviders": "Weitere Anbieter", "newKnowledge.name": "Wissensname", "newKnowledge.namePlaceholder": "z.B. Kundendiensthandbuch", "newKnowledge.nameRequired": "Geben Sie einen Namen für die Wissensdatenbank ein.", "newKnowledge.new": "Neu", + "newKnowledge.noBackgroundTasks": "Noch keine Hintergrundaufgaben.", "newKnowledge.noDescription": "Keine Beschreibung", + "newKnowledge.noMatchingDocuments": "Zu dieser Suche passen keine Dokumente.", "newKnowledge.noMatchingSources": "Keine Quellen passen zu dieser Suche.", "newKnowledge.noPagesFound": "Keine Seiten unter {{host}} gefunden", "newKnowledge.noPagesFoundDescription": "Der Crawler hat die Website erreicht, aber keine indexierbaren Seiten gefunden. Versuche eine andere URL oder erweitere die Crawl-Optionen.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} Seiten unter {{host}} gecrawlt", "newKnowledge.pagesCrawled_other": "{{count}} Seiten unter {{host}} gecrawlt", "newKnowledge.pagesSelected": "{{count}} ausgewählt", + "newKnowledge.partialDocumentResults": "Weitere Dokumente könnten passen. Lade mehr, um die Suche fortzusetzen.", "newKnowledge.permission": "Erlaubnis", "newKnowledge.permissionAllMembers": "Arbeitsbereich · Alle Mitglieder können ihn ansehen und bearbeiten", + "newKnowledge.permissionLoadFailed": "Ihre Bearbeitungsberechtigungen konnten nicht überprüft werden. Versuchen Sie es erneut.", "newKnowledge.permissionOnlyMe": "Nur ich", "newKnowledge.permissionRestricted": "Ihre Arbeitsbereichsberechtigungen erlauben keine Änderung dieser Einstellung.", "newKnowledge.permissionUpdateFailed": "Die Wissensdatenbank wurde erstellt, aber ihre Berechtigung konnte nicht bestätigt werden. Versuchen Sie es erneut, um die Einrichtung abzuschließen.", "newKnowledge.preview": "Vorschau", "newKnowledge.previewUnavailable": "Die Vorschau ist verfügbar, nachdem das Backend diese Datei verarbeitet hat.", + "newKnowledge.processDocument": "Prozess {{name}}", + "newKnowledge.processingTaskState.canceled": "Unterbrochen", + "newKnowledge.processingTaskState.dispatch_pending": "Vorbereiten", + "newKnowledge.processingTaskState.failed": "Fehlgeschlagen", + "newKnowledge.processingTaskState.queued": "In der Warteschlange", + "newKnowledge.processingTaskState.retry_wait": "Warte auf einen erneuten Versuch", + "newKnowledge.processingTaskState.running": "Verarbeitung · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Abgeschlossen", + "newKnowledge.processingTaskState.superseded": "Abgelöst", "newKnowledge.providerConnected": "{{provider}} verbunden", "newKnowledge.providerLabel": "Wählen Sie einen Anbieter", "newKnowledge.providerLoadFailed": "Anbieter konnten nicht geladen werden", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Bitten Sie einen Arbeitsbereich-Editor, die erste Wissensdatenbank zu erstellen.", "newKnowledge.refreshConnectionStatus": "Status aktualisieren", "newKnowledge.refreshingConnection": "Erfrischend…", + "newKnowledge.reindexDocuments": "Neu indizieren", "newKnowledge.removeSource": "Quelle entfernen", "newKnowledge.retryCrawl": "Crawl wiederholen", "newKnowledge.retryProviderLoad": "Wiederholen", "newKnowledge.retrySyncPolicy": "Synchronisierungsrichtlinie erneut laden", + "newKnowledge.retryTask": "Versuchen Sie es noch einmal", "newKnowledge.rootUrl": "Stamm-URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Dieser Abschnitt wird im nächsten Einrichtungsschritt verfügbar.", + "newKnowledge.searchDocuments": "Dokumente durchsuchen", "newKnowledge.searchSources": "Quellen durchsuchen", "newKnowledge.selectAll": "Alle auswählen", + "newKnowledge.selectAllDocuments": "Wählen Sie alle Dokumente aus", "newKnowledge.selectAllSources": "Alle Quellen auswählen", "newKnowledge.selectFilesAndFolders": "Dateien und Ordner zum Synchronisieren auswählen", "newKnowledge.skippedFailed": "Übersprungen (fehlgeschlagen)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Nur manuell", "newKnowledge.syncPolicyProvider": "Zeitplan des Anbieters verwenden", "newKnowledge.tags": "Tags", + "newKnowledge.taskActionFailed": "Wir konnten diese Aufgabe nicht aktualisieren. Aktualisieren Sie und versuchen Sie es erneut.", + "newKnowledge.taskAttentionClear": "Keine Hintergrundaufgaben erfordern Aufmerksamkeit.", + "newKnowledge.taskAttentionCount": "{{count}} Hintergrundaufgaben erfordern Aufmerksamkeit.", + "newKnowledge.taskAttentionErrorCount": "{{count}} Hintergrundaufgaben erfordern Aufmerksamkeit, einschließlich fehlgeschlagener Aufgaben.", + "newKnowledge.taskFailedNotification": "Die Dokumentverarbeitung ist fehlgeschlagen. Versuchen Sie es erneut über „Aufgaben“.", + "newKnowledge.taskHistoryIncomplete": "Weitere Aufgaben sind verfügbar; die Anzahl der Aufgaben mit Handlungsbedarf ist unvollständig.", + "newKnowledge.tasks": "Aufgaben", + "newKnowledge.tasksErrorDescription": "Wir konnten keine Hintergrundaufgaben laden. Versuchen Sie es erneut.", + "newKnowledge.tasksWithAttention": "Aufgaben, {{count}} erfordern Aufmerksamkeit", "newKnowledge.unavailableDescription": "KnowledgeFS ist für diesen Arbeitsbereich derzeit nicht verfügbar.", "newKnowledge.unavailableTitle": "Neues Wissen ist nicht verfügbar", "newKnowledge.updated": "Aktualisiert am {{date}}", + "newKnowledge.updatedColumn": "Aktualisiert", "newKnowledge.uploadCharactersUnavailable": "Zeichen: —", + "newKnowledge.uploadDocuments": "Dokumente hochladen", "newKnowledge.uploadFiles": "Dateien hochladen", "newKnowledge.uploadFilesDescription": "Fügen Sie Dateien von Ihrem Computer hinzu — sie werden beim Erstellen sofort indiziert.", "newKnowledge.uploadingFiles": "Wird hochgeladen…", diff --git a/web/i18n/en-US/dataset.json b/web/i18n/en-US/dataset.json index 72175950b5e..3d64dd40001 100644 --- a/web/i18n/en-US/dataset.json +++ b/web/i18n/en-US/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "The Rerank model is required for mixture of internal and external knowledge.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Actions", + "newKnowledge.addDocument": "Add document", "newKnowledge.addSource": "Add source", "newKnowledge.addSourceDescription": "Connect a source to this knowledge base — it syncs automatically and stays fresh.", "newKnowledge.addSourceFailed": "We couldn't add this source. Review the settings and try again.", "newKnowledge.addSourceRequiresSelection": "Select at least one crawled page before adding the source.", "newKnowledge.adjustAndRecrawl": "Adjust & re-crawl", + "newKnowledge.allDocumentStatuses": "All status", "newKnowledge.allSources": "All sources", "newKnowledge.appsUnavailable": "— apps", "newKnowledge.authKind.api-key": "API key", "newKnowledge.authKind.endpoint": "Endpoint", "newKnowledge.authenticationMethod": "Authentication method", "newKnowledge.backToList": "Back to knowledge bases", + "newKnowledge.backgroundTasks": "Background tasks", + "newKnowledge.backgroundTasksDescription": "Document adds, source syncs, and re-index jobs.", "newKnowledge.booleanFalse": "No", "newKnowledge.booleanTrue": "Yes", + "newKnowledge.bulkDocumentActions": "Bulk document actions", "newKnowledge.cancelAddSource": "Cancel", "newKnowledge.cardType": "Knowledge space", + "newKnowledge.clearDocumentSelection": "Clear document selection", "newKnowledge.configureProvider": "Configure {{provider}}", "newKnowledge.connectNotion": "Connect Notion", "newKnowledge.connectProvider": "Connect {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Creators", "newKnowledge.customIntervalHours": "Custom interval (hours)", "newKnowledge.customIntervalInvalid": "Enter a whole number from 1 to 720 hours.", + "newKnowledge.deleteDocuments": "Delete…", "newKnowledge.description": "Description", "newKnowledge.descriptionHelp": "A clear description helps apps decide when to search this Knowledge.", "newKnowledge.descriptionPlaceholder": "e.g. Refund policy, shipping FAQs, and escalation steps — use for customer support questions", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "The source configuration and crawl preview will be removed.", "newKnowledge.discardSourceDraftFailed": "We couldn’t discard the source draft. Try again.", "newKnowledge.discardSourceDraftTitle": "Discard this source draft?", + "newKnowledge.documentActions": "Actions for {{name}}", + "newKnowledge.documentActionsUnavailable": "Document actions aren't available for this knowledge base yet.", + "newKnowledge.documentColumn": "Document", + "newKnowledge.documentFilterLabel": "Filter documents by status", + "newKnowledge.documentPermissionRestricted": "Your workspace permissions don't allow document or task actions.", + "newKnowledge.documentStatus.disabled": "Disabled", + "newKnowledge.documentStatus.failed": "Failed", + "newKnowledge.documentStatus.processing": "Processing", + "newKnowledge.documentStatus.queued": "Queued", + "newKnowledge.documentStatus.ready": "Ready", + "newKnowledge.documentUploadExclusion.batchLimit": "batch size limit exceeded", + "newKnowledge.documentUploadExclusion.countLimit": "file count limit exceeded", + "newKnowledge.documentUploadExclusion.fileSize": "file is too large", + "newKnowledge.documentUploadExclusion.fileType": "unsupported or invalid file type", + "newKnowledge.documentUploadExclusion.more": "and {{count}} more", + "newKnowledge.documentUploadExclusion.processing": "processing failed", + "newKnowledge.documentUploadExclusion.quota": "workspace quota exceeded", + "newKnowledge.documentUploadExclusion.target": "document target is no longer valid", + "newKnowledge.documentUploadFailed": "We couldn't upload these documents. Try again.", + "newKnowledge.documentUploadPartial": "{{accepted}} documents started; {{excluded}} could not be added: {{details}}", + "newKnowledge.documentUploadRejected": "No documents were accepted: {{details}}", + "newKnowledge.documentUploadStarted": "Document processing started.", "newKnowledge.documents": "Documents", + "newKnowledge.documentsDescription": "Knowledge assets and their revisions — where each document came from and whether the latest revision is indexed.", + "newKnowledge.documentsDropHint": "or drop PDF, DOCX, Markdown, HTML, XLSX, or TXT to upload", + "newKnowledge.documentsEmptyDescription": "Upload files here, or connect a source on the Sources tab — documents are parsed, chunked, and indexed automatically.", + "newKnowledge.documentsEmptyTitle": "No documents yet", + "newKnowledge.documentsErrorDescription": "We couldn't load documents. Try again.", + "newKnowledge.documentsErrorTitle": "Documents couldn't load", + "newKnowledge.documentsPermissionDescription": "You don't have permission to view documents in this knowledge base.", + "newKnowledge.documentsPermissionTitle": "Documents are restricted", + "newKnowledge.documentsReindexFailed": "We couldn't start re-indexing. Try again.", + "newKnowledge.documentsReindexPartial": "{{queued}} documents queued; {{missing}} no longer exist.", + "newKnowledge.documentsReindexStarted": "Re-indexing started.", + "newKnowledge.documentsSelected": "{{count}} documents selected", "newKnowledge.documentsUnavailable": "— documents", + "newKnowledge.downloadDocuments": "Download", "newKnowledge.editSource": "Edit", "newKnowledge.emptyDescription": "Add your documents and agents can read them directly — no indexing to configure.", "newKnowledge.emptyTitle": "A smarter way to build knowledge bases", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Meet the new Knowledge Base ✨", "newKnowledge.illustrationHeadline": "Connect your content once — every app answers with the freshest knowledge.", "newKnowledge.includeSubpages": "Include sub-pages", + "newKnowledge.interruptTask": "Interrupt", "newKnowledge.invalidRootUrl": "Enter a valid http(s) URL.", "newKnowledge.keepEditing": "Keep editing", + "newKnowledge.lastReadyRevisionHint": "Failed processing never interrupts retrieval — the last ready revision stays available in the content index.", "newKnowledge.lastSyncColumn": "Last sync", "newKnowledge.learnMore": "Learn more", "newKnowledge.leavePartialSetupConfirm": "Leave setup", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Legacy", "newKnowledge.loadMore": "Load more", "newKnowledge.loadingSyncPolicy": "Loading sync policy", + "newKnowledge.manualUpload": "Manual upload", "newKnowledge.maxPages": "Maximum pages", + "newKnowledge.metadata": "Meta Data", "newKnowledge.moreProviders": "More providers", "newKnowledge.name": "Knowledge name", "newKnowledge.namePlaceholder": "e.g. Customer Support Handbook", "newKnowledge.nameRequired": "Enter a knowledge base name.", "newKnowledge.new": "New", + "newKnowledge.noBackgroundTasks": "No background tasks yet.", "newKnowledge.noDescription": "No description", + "newKnowledge.noMatchingDocuments": "No documents match this search.", "newKnowledge.noMatchingSources": "No sources match this search.", "newKnowledge.noPagesFound": "No pages found at {{host}}", "newKnowledge.noPagesFoundDescription": "The crawler reached the site but found no indexable pages. Try a different URL, or expand crawl options.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} page crawled at {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} pages crawled at {{host}}", "newKnowledge.pagesSelected": "{{count}} selected", + "newKnowledge.partialDocumentResults": "More documents may match. Load more to continue searching.", "newKnowledge.permission": "Permission", "newKnowledge.permissionAllMembers": "Workspace · all members can view and edit", + "newKnowledge.permissionLoadFailed": "We couldn't verify your editing permissions. Try again.", "newKnowledge.permissionOnlyMe": "Only me", "newKnowledge.permissionRestricted": "Your workspace permissions don't allow changing this setting.", "newKnowledge.permissionUpdateFailed": "The knowledge base was created, but its permission could not be confirmed. Retry to finish setup.", "newKnowledge.preview": "Preview", "newKnowledge.previewUnavailable": "Preview becomes available after the backend finishes processing this file.", + "newKnowledge.processDocument": "Process {{name}}", + "newKnowledge.processingTaskState.canceled": "Interrupted", + "newKnowledge.processingTaskState.dispatch_pending": "Preparing", + "newKnowledge.processingTaskState.failed": "Failed", + "newKnowledge.processingTaskState.queued": "Queued", + "newKnowledge.processingTaskState.retry_wait": "Waiting to retry", + "newKnowledge.processingTaskState.running": "Processing · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Completed", + "newKnowledge.processingTaskState.superseded": "Superseded", "newKnowledge.providerConnected": "{{provider}} connected", "newKnowledge.providerLabel": "Provider", "newKnowledge.providerLoadFailed": "Providers couldn't load", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Ask a workspace editor to create the first knowledge base.", "newKnowledge.refreshConnectionStatus": "Refresh status", "newKnowledge.refreshingConnection": "Refreshing…", + "newKnowledge.reindexDocuments": "Re-index", "newKnowledge.removeSource": "Remove source", "newKnowledge.retryCrawl": "Retry crawl", "newKnowledge.retryProviderLoad": "Retry", "newKnowledge.retrySyncPolicy": "Retry sync policy", + "newKnowledge.retryTask": "Retry", "newKnowledge.rootUrl": "Root URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "This section will become available in the next setup step.", + "newKnowledge.searchDocuments": "Search documents", "newKnowledge.searchSources": "Search sources", "newKnowledge.selectAll": "Select all", + "newKnowledge.selectAllDocuments": "Select all documents", "newKnowledge.selectAllSources": "Select all sources", "newKnowledge.selectFilesAndFolders": "Select files & folders to sync", "newKnowledge.skippedFailed": "Skipped (failed)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Manual only", "newKnowledge.syncPolicyProvider": "Use provider schedule", "newKnowledge.tags": "Tags", + "newKnowledge.taskActionFailed": "We couldn't update this task. Refresh and try again.", + "newKnowledge.taskAttentionClear": "No background tasks need attention.", + "newKnowledge.taskAttentionCount": "{{count}} background tasks need attention.", + "newKnowledge.taskAttentionErrorCount": "{{count}} background tasks need attention, including failed tasks.", + "newKnowledge.taskFailedNotification": "Document processing failed — retry from Tasks", + "newKnowledge.taskHistoryIncomplete": "More task history is available; the attention count is incomplete.", + "newKnowledge.tasks": "Tasks", + "newKnowledge.tasksErrorDescription": "We couldn't load background tasks. Try again.", + "newKnowledge.tasksWithAttention": "Tasks, {{count}} need attention", "newKnowledge.unavailableDescription": "KnowledgeFS isn't available for this workspace right now.", "newKnowledge.unavailableTitle": "New Knowledge is unavailable", "newKnowledge.updated": "Updated {{date}}", + "newKnowledge.updatedColumn": "Updated", "newKnowledge.uploadCharactersUnavailable": "Characters: —", + "newKnowledge.uploadDocuments": "Upload documents", "newKnowledge.uploadFiles": "Upload files", "newKnowledge.uploadFilesDescription": "Add files from your computer — they're indexed as soon as you create.", "newKnowledge.uploadingFiles": "Uploading…", diff --git a/web/i18n/es-ES/dataset.json b/web/i18n/es-ES/dataset.json index 8ef08a6ad6d..1db7baf6b49 100644 --- a/web/i18n/es-ES/dataset.json +++ b/web/i18n/es-ES/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "El modelo de Rerank es necesario para la mezcla de conocimiento interno y externo.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Acciones", + "newKnowledge.addDocument": "Agregar documento", "newKnowledge.addSource": "Agregar fuente", "newKnowledge.addSourceDescription": "Conecte una fuente a esta base de conocimientos: se sincroniza automáticamente y se mantiene actualizada.", "newKnowledge.addSourceFailed": "No se pudo añadir esta fuente. Revisa la configuración e inténtalo de nuevo.", "newKnowledge.addSourceRequiresSelection": "Selecciona al menos una página rastreada antes de añadir la fuente.", "newKnowledge.adjustAndRecrawl": "Ajustar y volver a rastrear", + "newKnowledge.allDocumentStatuses": "Todo el estado", "newKnowledge.allSources": "Todas las fuentes", "newKnowledge.appsUnavailable": "— aplicaciones", "newKnowledge.authKind.api-key": "Clave API", "newKnowledge.authKind.endpoint": "Punto final", "newKnowledge.authenticationMethod": "Método de autenticación", "newKnowledge.backToList": "Volver a las bases de conocimiento", + "newKnowledge.backgroundTasks": "Tareas en segundo plano", + "newKnowledge.backgroundTasksDescription": "Agrega documentos, sincroniza fuentes y vuelve a indexar trabajos.", "newKnowledge.booleanFalse": "No", "newKnowledge.booleanTrue": "Sí", + "newKnowledge.bulkDocumentActions": "Acciones de documentos masivos", "newKnowledge.cancelAddSource": "Cancelar", "newKnowledge.cardType": "Espacio de conocimiento", + "newKnowledge.clearDocumentSelection": "Borrar selección de documentos", "newKnowledge.configureProvider": "Configurar {{provider}}", "newKnowledge.connectNotion": "Conectar Notion", "newKnowledge.connectProvider": "Conectar {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Creadores", "newKnowledge.customIntervalHours": "Intervalo personalizado (horas)", "newKnowledge.customIntervalInvalid": "Introduce un número entero de 1 a 720 horas.", + "newKnowledge.deleteDocuments": "Eliminar…", "newKnowledge.description": "Descripción", "newKnowledge.descriptionHelp": "Una descripción clara ayuda a las aplicaciones a decidir cuándo buscar este conocimiento.", "newKnowledge.descriptionPlaceholder": "p.ej. Política de reembolso, preguntas frecuentes sobre envíos y pasos a seguir: utilícelos para preguntas de atención al cliente", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Se eliminarán la configuración de la fuente y la vista previa del rastreo.", "newKnowledge.discardSourceDraftFailed": "No se pudo descartar el borrador de la fuente. Vuelve a intentarlo.", "newKnowledge.discardSourceDraftTitle": "¿Descartar este borrador de fuente?", + "newKnowledge.documentActions": "Acciones para {{name}}", + "newKnowledge.documentActionsUnavailable": "Las acciones de documentos aún no están disponibles para esta base de conocimientos.", + "newKnowledge.documentColumn": "Documento", + "newKnowledge.documentFilterLabel": "Filtrar documentos por estado", + "newKnowledge.documentPermissionRestricted": "Los permisos de tu espacio de trabajo no permiten acciones sobre documentos o tareas.", + "newKnowledge.documentStatus.disabled": "Desactivado", + "newKnowledge.documentStatus.failed": "Fallido", + "newKnowledge.documentStatus.processing": "Procesamiento", + "newKnowledge.documentStatus.queued": "En cola", + "newKnowledge.documentStatus.ready": "Listo", + "newKnowledge.documentUploadExclusion.batchLimit": "se superó el límite de tamaño del lote", + "newKnowledge.documentUploadExclusion.countLimit": "se superó el límite de archivos", + "newKnowledge.documentUploadExclusion.fileSize": "el archivo es demasiado grande", + "newKnowledge.documentUploadExclusion.fileType": "tipo de archivo no compatible o no válido", + "newKnowledge.documentUploadExclusion.more": "y {{count}} más", + "newKnowledge.documentUploadExclusion.processing": "error de procesamiento", + "newKnowledge.documentUploadExclusion.quota": "se superó la cuota del espacio de trabajo", + "newKnowledge.documentUploadExclusion.target": "el destino del documento ya no es válido", + "newKnowledge.documentUploadFailed": "No pudimos cargar estos documentos. Intentar otra vez.", + "newKnowledge.documentUploadPartial": "Se inició el procesamiento de {{accepted}} documentos; no se pudieron añadir {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "No se aceptó ningún documento: {{details}}", + "newKnowledge.documentUploadStarted": "Se inició el procesamiento de documentos.", "newKnowledge.documents": "Documentos", + "newKnowledge.documentsDescription": "Activos de conocimiento y sus revisiones: de dónde proviene cada documento y si la última revisión está indexada.", + "newKnowledge.documentsDropHint": "o suelte PDF, DOCX, Markdown, HTML, XLSX o TXT para cargar", + "newKnowledge.documentsEmptyDescription": "Cargue archivos aquí o conecte una fuente en la pestaña Fuentes: los documentos se analizan, fragmentan e indexan automáticamente.", + "newKnowledge.documentsEmptyTitle": "Aún no hay documentos", + "newKnowledge.documentsErrorDescription": "No pudimos cargar documentos. Intentar otra vez.", + "newKnowledge.documentsErrorTitle": "No se pudieron cargar los documentos", + "newKnowledge.documentsPermissionDescription": "No tiene permiso para ver documentos en esta base de conocimientos.", + "newKnowledge.documentsPermissionTitle": "Los documentos están restringidos.", + "newKnowledge.documentsReindexFailed": "No pudimos comenzar a volver a indexar. Intentar otra vez.", + "newKnowledge.documentsReindexPartial": "Documentos {{queued}} en cola; {{missing}} ya no existe.", + "newKnowledge.documentsReindexStarted": "Se inició la reindexación.", + "newKnowledge.documentsSelected": "Documentos {{count}} seleccionados", "newKnowledge.documentsUnavailable": "— documentos", + "newKnowledge.downloadDocuments": "Descargar", "newKnowledge.editSource": "Editar", "newKnowledge.emptyDescription": "Añade tus documentos y los agentes podrán leerlos directamente, sin configurar la indexación.", "newKnowledge.emptyTitle": "Una forma más inteligente de crear bases de conocimiento", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Conoce la nueva base de conocimiento ✨", "newKnowledge.illustrationHeadline": "Conecta tu contenido una vez: cada aplicación responderá con el conocimiento más reciente.", "newKnowledge.includeSubpages": "Incluir subpáginas", + "newKnowledge.interruptTask": "Interrumpir", "newKnowledge.invalidRootUrl": "Introduce una URL http(s) válida.", "newKnowledge.keepEditing": "Seguir editando", + "newKnowledge.lastReadyRevisionHint": "El procesamiento fallido nunca interrumpe la recuperación: la última revisión lista permanece disponible en el índice de contenido.", "newKnowledge.lastSyncColumn": "Última sincronización", "newKnowledge.learnMore": "Más información", "newKnowledge.leavePartialSetupConfirm": "Salir de la configuración", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Anterior", "newKnowledge.loadMore": "Cargar más", "newKnowledge.loadingSyncPolicy": "Cargando la política de sincronización", + "newKnowledge.manualUpload": "Carga manual", "newKnowledge.maxPages": "Máximo de páginas", + "newKnowledge.metadata": "Metadatos", "newKnowledge.moreProviders": "Más proveedores", "newKnowledge.name": "Nombre del conocimiento", "newKnowledge.namePlaceholder": "por ej. Manual de atención al cliente", "newKnowledge.nameRequired": "Introduzca un nombre para la base de conocimientos.", "newKnowledge.new": "Nuevo", + "newKnowledge.noBackgroundTasks": "Aún no hay tareas en segundo plano.", "newKnowledge.noDescription": "Sin descripción", + "newKnowledge.noMatchingDocuments": "Ningún documento coincide con esta búsqueda.", "newKnowledge.noMatchingSources": "Ninguna fuente coincide con esta búsqueda.", "newKnowledge.noPagesFound": "No se encontraron páginas en {{host}}", "newKnowledge.noPagesFoundDescription": "El rastreador accedió al sitio, pero no encontró páginas indexables. Prueba otra URL o amplía las opciones de rastreo.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} páginas rastreadas en {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} páginas rastreadas en {{host}}", "newKnowledge.pagesSelected": "{{count}} seleccionados", + "newKnowledge.partialDocumentResults": "Puede haber más documentos coincidentes. Carga más para continuar la búsqueda.", "newKnowledge.permission": "Permiso", "newKnowledge.permissionAllMembers": "Espacio de trabajo · todos los miembros pueden ver y editar", + "newKnowledge.permissionLoadFailed": "No se pudieron verificar sus permisos de edición. Inténtelo de nuevo.", "newKnowledge.permissionOnlyMe": "solo yo", "newKnowledge.permissionRestricted": "Los permisos de su espacio de trabajo no permiten cambiar esta configuración.", "newKnowledge.permissionUpdateFailed": "La base de conocimientos se creó, pero no se pudo confirmar su permiso. Vuelve a intentarlo para finalizar la configuración.", "newKnowledge.preview": "Vista previa", "newKnowledge.previewUnavailable": "La vista previa estará disponible cuando el backend termine de procesar este archivo.", + "newKnowledge.processDocument": "Proceso {{name}}", + "newKnowledge.processingTaskState.canceled": "interrumpido", + "newKnowledge.processingTaskState.dispatch_pending": "preparando", + "newKnowledge.processingTaskState.failed": "Fallido", + "newKnowledge.processingTaskState.queued": "En cola", + "newKnowledge.processingTaskState.retry_wait": "Esperando para volver a intentarlo", + "newKnowledge.processingTaskState.running": "Procesamiento · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Completado", + "newKnowledge.processingTaskState.superseded": "Reemplazado", "newKnowledge.providerConnected": "{{provider}} conectado", "newKnowledge.providerLabel": "Seleccione un proveedor", "newKnowledge.providerLoadFailed": "Los proveedores no pudieron cargar", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Pide a un editor del espacio de trabajo que cree la primera base de conocimiento.", "newKnowledge.refreshConnectionStatus": "Estado de actualización", "newKnowledge.refreshingConnection": "Refrescante…", + "newKnowledge.reindexDocuments": "Volver a indexar", "newKnowledge.removeSource": "Eliminar fuente", "newKnowledge.retryCrawl": "Reintentar rastreo", "newKnowledge.retryProviderLoad": "Reintentar", "newKnowledge.retrySyncPolicy": "Reintentar la política de sincronización", + "newKnowledge.retryTask": "Reintentar", "newKnowledge.rootUrl": "URL raíz", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Esta sección estará disponible en el siguiente paso de configuración.", + "newKnowledge.searchDocuments": "Buscar documentos", "newKnowledge.searchSources": "Buscar fuentes", "newKnowledge.selectAll": "Seleccionar todo", + "newKnowledge.selectAllDocuments": "Seleccionar todos los documentos", "newKnowledge.selectAllSources": "Seleccionar todos los orígenes", "newKnowledge.selectFilesAndFolders": "Selecciona archivos y carpetas para sincronizar", "newKnowledge.skippedFailed": "Omitido (fallido)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Solo manual", "newKnowledge.syncPolicyProvider": "Usar la programación del proveedor", "newKnowledge.tags": "Etiquetas", + "newKnowledge.taskActionFailed": "No pudimos actualizar esta tarea. Actualiza e inténtalo de nuevo.", + "newKnowledge.taskAttentionClear": "Ninguna tarea en segundo plano necesita atención.", + "newKnowledge.taskAttentionCount": "Las tareas en segundo plano {{count}} necesitan atención.", + "newKnowledge.taskAttentionErrorCount": "Las tareas en segundo plano de {{count}} necesitan atención, incluidas las tareas fallidas.", + "newKnowledge.taskFailedNotification": "Error al procesar el documento: vuelva a intentarlo desde Tareas", + "newKnowledge.taskHistoryIncomplete": "Hay más historial de tareas disponible; el recuento de tareas que requieren atención está incompleto.", + "newKnowledge.tasks": "Tareas", + "newKnowledge.tasksErrorDescription": "No pudimos cargar tareas en segundo plano. Intentar otra vez.", + "newKnowledge.tasksWithAttention": "Tareas, {{count}} necesitan atención", "newKnowledge.unavailableDescription": "KnowledgeFS no está disponible para este espacio de trabajo ahora mismo.", "newKnowledge.unavailableTitle": "El nuevo conocimiento no está disponible", "newKnowledge.updated": "Actualizado el {{date}}", + "newKnowledge.updatedColumn": "Actualizado", "newKnowledge.uploadCharactersUnavailable": "Caracteres: —", + "newKnowledge.uploadDocuments": "Subir documentos", "newKnowledge.uploadFiles": "Subir archivos", "newKnowledge.uploadFilesDescription": "Añade archivos desde tu ordenador; se indexarán en cuanto los crees.", "newKnowledge.uploadingFiles": "Subiendo…", diff --git a/web/i18n/fa-IR/dataset.json b/web/i18n/fa-IR/dataset.json index 05412b7c00d..4240b7324d0 100644 --- a/web/i18n/fa-IR/dataset.json +++ b/web/i18n/fa-IR/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "مدل Rerank برای آمیختگی دانش درونی و بیرونی مورد نیاز است.", "multimodal": "چندوجهی", "newKnowledge.actionsColumn": "اقدامات", + "newKnowledge.addDocument": "سند اضافه کنید", "newKnowledge.addSource": "اضافه کردن منبع", "newKnowledge.addSourceDescription": "منبعی را به این پایگاه دانش متصل کنید - به طور خودکار همگام‌سازی می‌شود و تازه می‌ماند.", "newKnowledge.addSourceFailed": "افزودن این منبع ممکن نشد. تنظیمات را بررسی کنید و دوباره تلاش کنید.", "newKnowledge.addSourceRequiresSelection": "پیش از افزودن منبع، حداقل یک صفحهٔ پیمایش‌شده را انتخاب کنید.", "newKnowledge.adjustAndRecrawl": "تنظیم و پیمایش دوباره", + "newKnowledge.allDocumentStatuses": "همه وضعیت", "newKnowledge.allSources": "همه منابع", "newKnowledge.appsUnavailable": "— برنامه", "newKnowledge.authKind.api-key": "کلید API", "newKnowledge.authKind.endpoint": "نقطه پایانی", "newKnowledge.authenticationMethod": "روش احراز هویت", "newKnowledge.backToList": "بازگشت به پایگاه های دانش", + "newKnowledge.backgroundTasks": "وظایف پس زمینه", + "newKnowledge.backgroundTasksDescription": "افزودن اسناد، همگام سازی منبع، و فهرست مجدد مشاغل.", "newKnowledge.booleanFalse": "نه", "newKnowledge.booleanTrue": "بله", + "newKnowledge.bulkDocumentActions": "اقدامات سند انبوه", "newKnowledge.cancelAddSource": "لغو", "newKnowledge.cardType": "فضای دانش", + "newKnowledge.clearDocumentSelection": "انتخاب سند را پاک کنید", "newKnowledge.configureProvider": "{{provider}} را پیکربندی کنید", "newKnowledge.connectNotion": "اتصال Notion", "newKnowledge.connectProvider": "{{provider}} را وصل کنید", @@ -181,6 +187,7 @@ "newKnowledge.creators": "ایجادکنندگان", "newKnowledge.customIntervalHours": "بازه سفارشی (ساعت)", "newKnowledge.customIntervalInvalid": "یک عدد صحیح بین ۱ تا ۷۲۰ ساعت وارد کنید.", + "newKnowledge.deleteDocuments": "حذف…", "newKnowledge.description": "توضیحات", "newKnowledge.descriptionHelp": "توضیح واضح به برنامه‌ها کمک می‌کند تصمیم بگیرند که چه زمانی این دانش را جستجو کنند.", "newKnowledge.descriptionPlaceholder": "به عنوان مثال خط مشی بازپرداخت، سؤالات متداول حمل و نقل، و مراحل افزایش - برای سؤالات پشتیبانی مشتری استفاده کنید", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "پیکربندی منبع و پیش‌نمایش پیمایش حذف خواهند شد.", "newKnowledge.discardSourceDraftFailed": "حذف پیش‌نویس منبع ممکن نشد. دوباره تلاش کنید.", "newKnowledge.discardSourceDraftTitle": "این پیش‌نویس منبع حذف شود؟", + "newKnowledge.documentActions": "اقدامات برای {{name}}", + "newKnowledge.documentActionsUnavailable": "اقدامات سند هنوز برای این پایگاه دانش در دسترس نیست.", + "newKnowledge.documentColumn": "سند", + "newKnowledge.documentFilterLabel": "اسناد را بر اساس وضعیت فیلتر کنید", + "newKnowledge.documentPermissionRestricted": "مجوزهای فضای کاری شما اجازه انجام عملیات روی اسناد یا وظایف را نمی‌دهد.", + "newKnowledge.documentStatus.disabled": "غیرفعال", + "newKnowledge.documentStatus.failed": "شکست خورد", + "newKnowledge.documentStatus.processing": "در حال پردازش", + "newKnowledge.documentStatus.queued": "در صف", + "newKnowledge.documentStatus.ready": "آماده است", + "newKnowledge.documentUploadExclusion.batchLimit": "محدودیت اندازه دسته رد شده است", + "newKnowledge.documentUploadExclusion.countLimit": "محدودیت تعداد فایل رد شده است", + "newKnowledge.documentUploadExclusion.fileSize": "فایل بیش از حد بزرگ است", + "newKnowledge.documentUploadExclusion.fileType": "نوع فایل پشتیبانی نمی‌شود یا نامعتبر است", + "newKnowledge.documentUploadExclusion.more": "و {{count}} مورد دیگر", + "newKnowledge.documentUploadExclusion.processing": "پردازش ناموفق بود", + "newKnowledge.documentUploadExclusion.quota": "سهمیه فضای کاری رد شده است", + "newKnowledge.documentUploadExclusion.target": "هدف سند دیگر معتبر نیست", + "newKnowledge.documentUploadFailed": "ما نتوانستیم این اسناد را آپلود کنیم. دوباره امتحان کنید.", + "newKnowledge.documentUploadPartial": "پردازش {{accepted}} سند آغاز شد؛ {{excluded}} سند افزوده نشد: {{details}}", + "newKnowledge.documentUploadRejected": "هیچ سندی پذیرفته نشد: {{details}}", + "newKnowledge.documentUploadStarted": "پردازش اسناد آغاز شد.", "newKnowledge.documents": "اسناد", + "newKnowledge.documentsDescription": "دارایی‌های دانش و بازبینی‌های آنها - هر سند از کجا آمده است و آیا آخرین ویرایش نمایه شده است یا خیر.", + "newKnowledge.documentsDropHint": "یا PDF، DOCX، Markdown، HTML، XLSX یا TXT را برای آپلود رها کنید", + "newKnowledge.documentsEmptyDescription": "فایل‌ها را در اینجا آپلود کنید، یا یک منبع را در برگه منابع وصل کنید - اسناد به طور خودکار تجزیه، تکه‌تکه و فهرست‌بندی می‌شوند.", + "newKnowledge.documentsEmptyTitle": "هنوز هیچ سندی وجود ندارد", + "newKnowledge.documentsErrorDescription": "نتوانستیم اسناد را بارگیری کنیم. دوباره امتحان کنید.", + "newKnowledge.documentsErrorTitle": "اسناد بارگیری نشد", + "newKnowledge.documentsPermissionDescription": "شما اجازه مشاهده اسناد در این پایگاه دانش را ندارید.", + "newKnowledge.documentsPermissionTitle": "اسناد محدود شده است", + "newKnowledge.documentsReindexFailed": "نتوانستیم فهرست‌بندی مجدد را شروع کنیم. دوباره امتحان کنید.", + "newKnowledge.documentsReindexPartial": "اسناد {{queued}} در صف قرار گرفتند. {{missing}} دیگر وجود ندارد.", + "newKnowledge.documentsReindexStarted": "فهرست بندی مجدد آغاز شد.", + "newKnowledge.documentsSelected": "اسناد {{count}} انتخاب شد", "newKnowledge.documentsUnavailable": "— سند", + "newKnowledge.downloadDocuments": "دانلود کنید", "newKnowledge.editSource": "ویرایش", "newKnowledge.emptyDescription": "اسناد خود را اضافه کنید تا عامل‌ها بتوانند مستقیماً آن‌ها را بخوانند — بدون نیاز به تنظیم نمایه‌سازی.", "newKnowledge.emptyTitle": "روشی هوشمندتر برای ساخت پایگاه‌های دانش", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "با پایگاه دانش جدید آشنا شوید ✨", "newKnowledge.illustrationHeadline": "محتوای خود را یک‌بار متصل کنید تا هر برنامه با تازه‌ترین دانش پاسخ دهد.", "newKnowledge.includeSubpages": "شامل صفحه‌های فرعی", + "newKnowledge.interruptTask": "توقف", "newKnowledge.invalidRootUrl": "یک نشانی معتبر http(s) وارد کنید.", "newKnowledge.keepEditing": "ادامه ویرایش", + "newKnowledge.lastReadyRevisionHint": "پردازش ناموفق هرگز بازیابی را قطع نمی کند - آخرین نسخه آماده در فهرست محتوا در دسترس باقی می ماند.", "newKnowledge.lastSyncColumn": "آخرین همگام سازی", "newKnowledge.learnMore": "بیشتر بدانید", "newKnowledge.leavePartialSetupConfirm": "خروج از راه‌اندازی", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "قدیمی", "newKnowledge.loadMore": "بارگذاری بیشتر", "newKnowledge.loadingSyncPolicy": "در حال بارگیری سیاست همگام‌سازی", + "newKnowledge.manualUpload": "آپلود دستی", "newKnowledge.maxPages": "حداکثر صفحه‌ها", + "newKnowledge.metadata": "متا دیتا", "newKnowledge.moreProviders": "ارائه‌دهندگان بیشتر", "newKnowledge.name": "نام دانش", "newKnowledge.namePlaceholder": "به عنوان مثال راهنمای پشتیبانی مشتری", "newKnowledge.nameRequired": "یک نام پایگاه دانش وارد کنید.", "newKnowledge.new": "جدید", + "newKnowledge.noBackgroundTasks": "هنوز هیچ کار پس زمینه ای وجود ندارد.", "newKnowledge.noDescription": "بدون توضیح", + "newKnowledge.noMatchingDocuments": "هیچ سندی با این جستجو مطابقت ندارد.", "newKnowledge.noMatchingSources": "هیچ منبعی با این جستجو مطابقت ندارد.", "newKnowledge.noPagesFound": "صفحه‌ای در {{host}} پیدا نشد", "newKnowledge.noPagesFoundDescription": "خزنده به سایت رسید اما صفحهٔ قابل نمایه‌سازی پیدا نکرد. نشانی دیگری را امتحان کنید یا گزینه‌های پیمایش را گسترش دهید.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} صفحه در {{host}} پیمایش شد", "newKnowledge.pagesCrawled_other": "{{count}} صفحه در {{host}} پیمایش شد", "newKnowledge.pagesSelected": "{{count}} مورد انتخاب شد", + "newKnowledge.partialDocumentResults": "ممکن است اسناد بیشتری مطابقت داشته باشند. برای ادامه جستجو موارد بیشتری بارگیری کنید.", "newKnowledge.permission": "اجازه", "newKnowledge.permissionAllMembers": "فضای کاری · همه اعضا می توانند مشاهده و ویرایش کنند", + "newKnowledge.permissionLoadFailed": "تأیید مجوزهای ویرایش ممکن نشد. دوباره تلاش کنید.", "newKnowledge.permissionOnlyMe": "فقط من", "newKnowledge.permissionRestricted": "مجوزهای فضای کاری شما اجازه تغییر این تنظیم را نمی دهد.", "newKnowledge.permissionUpdateFailed": "پایگاه دانش ایجاد شد، اما مجوز آن تأیید نشد. برای تکمیل راه‌اندازی دوباره تلاش کنید.", "newKnowledge.preview": "پیش‌نمایش", "newKnowledge.previewUnavailable": "پس از پایان پردازش این فایل در بخش پشتیبان، پیش‌نمایش در دسترس می‌شود.", + "newKnowledge.processDocument": "فرآیند {{name}}", + "newKnowledge.processingTaskState.canceled": "قطع شد", + "newKnowledge.processingTaskState.dispatch_pending": "آماده سازی", + "newKnowledge.processingTaskState.failed": "شکست خورد", + "newKnowledge.processingTaskState.queued": "در صف", + "newKnowledge.processingTaskState.retry_wait": "در انتظار تلاش مجدد", + "newKnowledge.processingTaskState.running": "پردازش · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "تکمیل شد", + "newKnowledge.processingTaskState.superseded": "جایگزین شد", "newKnowledge.providerConnected": "{{provider}} متصل شد", "newKnowledge.providerLabel": "یک ارائه دهنده را انتخاب کنید", "newKnowledge.providerLoadFailed": "ارائه‌دهندگان نتوانستند بارگیری کنند", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "از یک ویرایشگر فضای کاری بخواهید اولین پایگاه دانش را ایجاد کند.", "newKnowledge.refreshConnectionStatus": "وضعیت بازخوانی", "newKnowledge.refreshingConnection": "تازه‌سازی…", + "newKnowledge.reindexDocuments": "فهرست مجدد", "newKnowledge.removeSource": "حذف منبع", "newKnowledge.retryCrawl": "تلاش دوباره برای پیمایش", "newKnowledge.retryProviderLoad": "دوباره امتحان کنید", "newKnowledge.retrySyncPolicy": "تلاش دوباره برای سیاست همگام‌سازی", + "newKnowledge.retryTask": "دوباره امتحان کنید", "newKnowledge.rootUrl": "نشانی ریشه", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "این بخش در مرحله راه اندازی بعدی در دسترس خواهد بود.", + "newKnowledge.searchDocuments": "جستجوی اسناد", "newKnowledge.searchSources": "منابع جستجو", "newKnowledge.selectAll": "انتخاب همه", + "newKnowledge.selectAllDocuments": "همه اسناد را انتخاب کنید", "newKnowledge.selectAllSources": "انتخاب همه منابع", "newKnowledge.selectFilesAndFolders": "فایل‌ها و پوشه‌ها را برای همگام‌سازی انتخاب کنید", "newKnowledge.skippedFailed": "رد شد (ناموفق)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "فقط دستی", "newKnowledge.syncPolicyProvider": "استفاده از برنامه ارائه‌دهنده", "newKnowledge.tags": "برچسب‌ها", + "newKnowledge.taskActionFailed": "ما نتوانستیم این کار را به روز کنیم. بازخوانی کنید و دوباره امتحان کنید.", + "newKnowledge.taskAttentionClear": "هیچ کار پس زمینه ای نیاز به توجه ندارد.", + "newKnowledge.taskAttentionCount": "وظایف پس زمینه {{count}} نیاز به توجه دارند.", + "newKnowledge.taskAttentionErrorCount": "وظایف پس زمینه {{count}} نیاز به توجه دارند، از جمله وظایف ناموفق.", + "newKnowledge.taskFailedNotification": "پردازش سند ناموفق بود — از بخش وظایف دوباره امتحان کنید", + "newKnowledge.taskHistoryIncomplete": "تاریخچه وظایف بیشتری موجود است؛ تعداد وظایف نیازمند توجه کامل نیست.", + "newKnowledge.tasks": "وظایف", + "newKnowledge.tasksErrorDescription": "نمی‌توانیم وظایف پس‌زمینه را بارگیری کنیم. دوباره امتحان کنید.", + "newKnowledge.tasksWithAttention": "وظایف، {{count}} نیاز به توجه دارد", "newKnowledge.unavailableDescription": "KnowledgeFS اکنون برای این فضای کاری در دسترس نیست.", "newKnowledge.unavailableTitle": "دانش جدید در دسترس نیست", "newKnowledge.updated": "به‌روزرسانی در {{date}}", + "newKnowledge.updatedColumn": "به روز شد", "newKnowledge.uploadCharactersUnavailable": "نویسه‌ها: —", + "newKnowledge.uploadDocuments": "اسناد را بارگذاری کنید", "newKnowledge.uploadFiles": "بارگذاری فایل‌ها", "newKnowledge.uploadFilesDescription": "فایل‌ها را از رایانه اضافه کنید — به‌محض ایجاد نمایه‌سازی می‌شوند.", "newKnowledge.uploadingFiles": "در حال بارگذاری…", diff --git a/web/i18n/fr-FR/dataset.json b/web/i18n/fr-FR/dataset.json index f4ba89e25d6..bd56d147e3e 100644 --- a/web/i18n/fr-FR/dataset.json +++ b/web/i18n/fr-FR/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Le modèle Rerank est nécessaire pour mélanger les connaissances internes et externes.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Actions", + "newKnowledge.addDocument": "Ajouter un document", "newKnowledge.addSource": "Ajouter une source", "newKnowledge.addSourceDescription": "Connectez une source à cette base de connaissances : elle se synchronise automatiquement et reste à jour.", "newKnowledge.addSourceFailed": "Impossible d’ajouter cette source. Vérifiez les paramètres et réessayez.", "newKnowledge.addSourceRequiresSelection": "Sélectionnez au moins une page explorée avant d’ajouter la source.", "newKnowledge.adjustAndRecrawl": "Ajuster et relancer l’exploration", + "newKnowledge.allDocumentStatuses": "Tous les statuts", "newKnowledge.allSources": "Toutes les sources", "newKnowledge.appsUnavailable": "— applications", "newKnowledge.authKind.api-key": "Clé API", "newKnowledge.authKind.endpoint": "Point de terminaison", "newKnowledge.authenticationMethod": "Méthode d'authentification", "newKnowledge.backToList": "Retour aux bases de connaissances", + "newKnowledge.backgroundTasks": "Tâches en arrière-plan", + "newKnowledge.backgroundTasksDescription": "Ajouts de documents, synchronisations de sources et tâches de réindexation.", "newKnowledge.booleanFalse": "Non", "newKnowledge.booleanTrue": "Oui", + "newKnowledge.bulkDocumentActions": "Actions de documents en masse", "newKnowledge.cancelAddSource": "Annuler", "newKnowledge.cardType": "Espace de connaissances", + "newKnowledge.clearDocumentSelection": "Effacer la sélection des documents", "newKnowledge.configureProvider": "Configurer {{provider}}", "newKnowledge.connectNotion": "Connecter Notion", "newKnowledge.connectProvider": "Connecter {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Créateurs", "newKnowledge.customIntervalHours": "Intervalle personnalisé (heures)", "newKnowledge.customIntervalInvalid": "Saisissez un nombre entier compris entre 1 et 720 heures.", + "newKnowledge.deleteDocuments": "Supprimer…", "newKnowledge.description": "Descriptif", "newKnowledge.descriptionHelp": "Une description claire aide les applications à décider quand rechercher ces connaissances.", "newKnowledge.descriptionPlaceholder": "par ex. Politique de remboursement, FAQ sur l'expédition et étapes de remontée d'informations – à utiliser pour les questions d'assistance client", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "La configuration de la source et l’aperçu de l’exploration seront supprimés.", "newKnowledge.discardSourceDraftFailed": "Impossible d’abandonner le brouillon de la source. Réessayez.", "newKnowledge.discardSourceDraftTitle": "Abandonner ce brouillon de source ?", + "newKnowledge.documentActions": "Actions pour {{name}}", + "newKnowledge.documentActionsUnavailable": "Les actions sur les documents ne sont pas encore disponibles pour cette base de connaissances.", + "newKnowledge.documentColumn": "Document", + "newKnowledge.documentFilterLabel": "Filtrer les documents par statut", + "newKnowledge.documentPermissionRestricted": "Les autorisations de votre espace de travail ne permettent pas les actions sur les documents ou les tâches.", + "newKnowledge.documentStatus.disabled": "Désactivé", + "newKnowledge.documentStatus.failed": "Échec", + "newKnowledge.documentStatus.processing": "Traitement", + "newKnowledge.documentStatus.queued": "En file d'attente", + "newKnowledge.documentStatus.ready": "Prêt", + "newKnowledge.documentUploadExclusion.batchLimit": "limite de taille du lot dépassée", + "newKnowledge.documentUploadExclusion.countLimit": "limite du nombre de fichiers dépassée", + "newKnowledge.documentUploadExclusion.fileSize": "le fichier est trop volumineux", + "newKnowledge.documentUploadExclusion.fileType": "type de fichier non pris en charge ou non valide", + "newKnowledge.documentUploadExclusion.more": "et {{count}} autres", + "newKnowledge.documentUploadExclusion.processing": "échec du traitement", + "newKnowledge.documentUploadExclusion.quota": "quota de l’espace de travail dépassé", + "newKnowledge.documentUploadExclusion.target": "la cible du document n’est plus valide", + "newKnowledge.documentUploadFailed": "Nous n'avons pas pu télécharger ces documents. Essayer à nouveau.", + "newKnowledge.documentUploadPartial": "Le traitement de {{accepted}} documents a démarré ; {{excluded}} n’ont pas pu être ajoutés : {{details}}", + "newKnowledge.documentUploadRejected": "Aucun document n’a été accepté : {{details}}", + "newKnowledge.documentUploadStarted": "Le traitement du document a commencé.", "newKnowledge.documents": "Documents", + "newKnowledge.documentsDescription": "Actifs de connaissances et leurs révisions — d'où provient chaque document et si la dernière révision est indexée.", + "newKnowledge.documentsDropHint": "ou déposez un PDF, DOCX, Markdown, HTML, XLSX ou TXT pour le télécharger", + "newKnowledge.documentsEmptyDescription": "Téléchargez des fichiers ici ou connectez une source dans l'onglet Sources : les documents sont analysés, fragmentés et indexés automatiquement.", + "newKnowledge.documentsEmptyTitle": "Aucun document pour l'instant", + "newKnowledge.documentsErrorDescription": "Nous n'avons pas pu charger les documents. Essayer à nouveau.", + "newKnowledge.documentsErrorTitle": "Les documents n'ont pas pu être chargés", + "newKnowledge.documentsPermissionDescription": "Vous n'êtes pas autorisé à afficher les documents de cette base de connaissances.", + "newKnowledge.documentsPermissionTitle": "Les documents sont restreints", + "newKnowledge.documentsReindexFailed": "Nous n'avons pas pu commencer la réindexation. Essayer à nouveau.", + "newKnowledge.documentsReindexPartial": "Documents {{queued}} mis en file d'attente ; {{missing}} n'existe plus.", + "newKnowledge.documentsReindexStarted": "La réindexation a commencé.", + "newKnowledge.documentsSelected": "Documents {{count}} sélectionnés", "newKnowledge.documentsUnavailable": "— documents", + "newKnowledge.downloadDocuments": "Télécharger", "newKnowledge.editSource": "Modifier", "newKnowledge.emptyDescription": "Ajoutez vos documents et les agents pourront les lire directement, sans indexation à configurer.", "newKnowledge.emptyTitle": "Une façon plus intelligente de créer des bases de connaissances", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Découvrez la nouvelle base de connaissances ✨", "newKnowledge.illustrationHeadline": "Connectez votre contenu une fois : chaque application répond avec les connaissances les plus récentes.", "newKnowledge.includeSubpages": "Inclure les sous-pages", + "newKnowledge.interruptTask": "Interrompre", "newKnowledge.invalidRootUrl": "Saisissez une URL http(s) valide.", "newKnowledge.keepEditing": "Continuer la modification", + "newKnowledge.lastReadyRevisionHint": "Un traitement échoué n'interrompt jamais la récupération : la dernière révision prête reste disponible dans l'index de contenu.", "newKnowledge.lastSyncColumn": "Dernière synchronisation", "newKnowledge.learnMore": "En savoir plus", "newKnowledge.leavePartialSetupConfirm": "Quitter la configuration", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Ancien", "newKnowledge.loadMore": "Charger plus", "newKnowledge.loadingSyncPolicy": "Chargement de la politique de synchronisation", + "newKnowledge.manualUpload": "Téléchargement manuel", "newKnowledge.maxPages": "Nombre maximal de pages", + "newKnowledge.metadata": "Métadonnées", "newKnowledge.moreProviders": "Plus de fournisseurs", "newKnowledge.name": "Nom de la connaissance", "newKnowledge.namePlaceholder": "par ex. Manuel de support client", "newKnowledge.nameRequired": "Entrez un nom de base de connaissances.", "newKnowledge.new": "Nouveau", + "newKnowledge.noBackgroundTasks": "Aucune tâche en arrière-plan pour l'instant.", "newKnowledge.noDescription": "Aucune description", + "newKnowledge.noMatchingDocuments": "Aucun document ne correspond à cette recherche.", "newKnowledge.noMatchingSources": "Aucune source ne correspond à cette recherche.", "newKnowledge.noPagesFound": "Aucune page trouvée sur {{host}}", "newKnowledge.noPagesFoundDescription": "Le robot a atteint le site, mais n’a trouvé aucune page indexable. Essayez une autre URL ou élargissez les options d’exploration.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} pages explorées sur {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} pages explorées sur {{host}}", "newKnowledge.pagesSelected": "{{count}} sélectionné(s)", + "newKnowledge.partialDocumentResults": "D’autres documents peuvent correspondre. Chargez-en davantage pour poursuivre la recherche.", "newKnowledge.permission": "Autorisation", "newKnowledge.permissionAllMembers": "Espace de travail · tous les membres peuvent consulter et modifier", + "newKnowledge.permissionLoadFailed": "Impossible de vérifier vos autorisations de modification. Réessayez.", "newKnowledge.permissionOnlyMe": "Seulement moi", "newKnowledge.permissionRestricted": "Les autorisations de votre espace de travail ne permettent pas de modifier ce paramètre.", "newKnowledge.permissionUpdateFailed": "La base de connaissances a été créée, mais son autorisation n’a pas pu être confirmée. Réessayez pour terminer la configuration.", "newKnowledge.preview": "Aperçu", "newKnowledge.previewUnavailable": "L’aperçu sera disponible une fois le traitement de ce fichier terminé par le backend.", + "newKnowledge.processDocument": "Processus {{name}}", + "newKnowledge.processingTaskState.canceled": "Interrompu", + "newKnowledge.processingTaskState.dispatch_pending": "Préparation", + "newKnowledge.processingTaskState.failed": "Échec", + "newKnowledge.processingTaskState.queued": "En file d'attente", + "newKnowledge.processingTaskState.retry_wait": "En attendant de réessayer", + "newKnowledge.processingTaskState.running": "Traitement · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Terminé", + "newKnowledge.processingTaskState.superseded": "Remplacé", "newKnowledge.providerConnected": "{{provider}} connecté", "newKnowledge.providerLabel": "Sélectionnez un fournisseur", "newKnowledge.providerLoadFailed": "Les fournisseurs n'ont pas pu charger", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Demandez à un éditeur de l’espace de travail de créer la première base de connaissances.", "newKnowledge.refreshConnectionStatus": "Actualiser l'état", "newKnowledge.refreshingConnection": "Rafraîchissant…", + "newKnowledge.reindexDocuments": "Réindexer", "newKnowledge.removeSource": "Supprimer la source", "newKnowledge.retryCrawl": "Réessayer l’exploration", "newKnowledge.retryProviderLoad": "Réessayer", "newKnowledge.retrySyncPolicy": "Réessayer la politique de synchronisation", + "newKnowledge.retryTask": "Réessayer", "newKnowledge.rootUrl": "URL racine", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Cette section deviendra disponible lors de la prochaine étape de configuration.", + "newKnowledge.searchDocuments": "Rechercher des documents", "newKnowledge.searchSources": "Sources de recherche", "newKnowledge.selectAll": "Tout sélectionner", + "newKnowledge.selectAllDocuments": "Sélectionnez tous les documents", "newKnowledge.selectAllSources": "Sélectionner toutes les sources", "newKnowledge.selectFilesAndFolders": "Sélectionnez les fichiers et dossiers à synchroniser", "newKnowledge.skippedFailed": "Ignoré (échec)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Manuel uniquement", "newKnowledge.syncPolicyProvider": "Utiliser la planification du fournisseur", "newKnowledge.tags": "Étiquettes", + "newKnowledge.taskActionFailed": "Nous n'avons pas pu mettre à jour cette tâche. Actualisez et réessayez.", + "newKnowledge.taskAttentionClear": "Aucune tâche en arrière-plan ne nécessite d'attention.", + "newKnowledge.taskAttentionCount": "Les tâches en arrière-plan {{count}} nécessitent une attention particulière.", + "newKnowledge.taskAttentionErrorCount": "Les tâches en arrière-plan {{count}} nécessitent une attention particulière, y compris les tâches ayant échoué.", + "newKnowledge.taskFailedNotification": "Échec du traitement du document — réessayez depuis Tâches", + "newKnowledge.taskHistoryIncomplete": "D’autres tâches sont disponibles ; le nombre de tâches nécessitant une attention est incomplet.", + "newKnowledge.tasks": "Tâches", + "newKnowledge.tasksErrorDescription": "Nous ne pouvions pas charger les tâches en arrière-plan. Essayer à nouveau.", + "newKnowledge.tasksWithAttention": "Tâches, {{count}} nécessitent une attention particulière", "newKnowledge.unavailableDescription": "KnowledgeFS n’est pas disponible pour cet espace de travail actuellement.", "newKnowledge.unavailableTitle": "Les nouvelles connaissances sont indisponibles", "newKnowledge.updated": "Mis à jour le {{date}}", + "newKnowledge.updatedColumn": "Mis à jour", "newKnowledge.uploadCharactersUnavailable": "Caractères : —", + "newKnowledge.uploadDocuments": "Télécharger des documents", "newKnowledge.uploadFiles": "Importer des fichiers", "newKnowledge.uploadFilesDescription": "Ajoutez des fichiers depuis votre ordinateur : ils sont indexés dès leur création.", "newKnowledge.uploadingFiles": "Téléversement…", diff --git a/web/i18n/hi-IN/dataset.json b/web/i18n/hi-IN/dataset.json index d33d9984d3f..fdd28274534 100644 --- a/web/i18n/hi-IN/dataset.json +++ b/web/i18n/hi-IN/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "आंतरिक और बाहरी ज्ञान के मिश्रण के लिए रीरैंक मॉडल की आवश्यकता होती है।", "multimodal": "बहु-मोडल", "newKnowledge.actionsColumn": "क्रियाएँ", + "newKnowledge.addDocument": "दस्तावेज़ जोड़ें", "newKnowledge.addSource": "स्रोत जोड़ें", "newKnowledge.addSourceDescription": "इस ज्ञानकोष से एक स्रोत कनेक्ट करें - यह स्वचालित रूप से समन्वयित होता है और ताज़ा रहता है।", "newKnowledge.addSourceFailed": "यह स्रोत जोड़ा नहीं जा सका। सेटिंग की समीक्षा करें और फिर से कोशिश करें।", "newKnowledge.addSourceRequiresSelection": "स्रोत जोड़ने से पहले कम से कम एक क्रॉल किया गया पेज चुनें।", "newKnowledge.adjustAndRecrawl": "समायोजित करें और फिर से क्रॉल करें", + "newKnowledge.allDocumentStatuses": "सभी स्थिति", "newKnowledge.allSources": "सभी स्रोत", "newKnowledge.appsUnavailable": "— ऐप", "newKnowledge.authKind.api-key": "एपीआई कुंजी", "newKnowledge.authKind.endpoint": "समापन बिंदु", "newKnowledge.authenticationMethod": "प्रमाणीकरण विधि", "newKnowledge.backToList": "ज्ञानकोष पर वापस जाएँ", + "newKnowledge.backgroundTasks": "पृष्ठभूमि कार्य", + "newKnowledge.backgroundTasksDescription": "दस्तावेज़ जोड़ता है, स्रोत सिंक करता है, और नौकरियों को पुन: अनुक्रमित करता है।", "newKnowledge.booleanFalse": "नहीं", "newKnowledge.booleanTrue": "हाँ", + "newKnowledge.bulkDocumentActions": "थोक दस्तावेज़ क्रियाएँ", "newKnowledge.cancelAddSource": "रद्द करें", "newKnowledge.cardType": "नॉलेज स्पेस", + "newKnowledge.clearDocumentSelection": "दस्तावेज़ चयन साफ़ करें", "newKnowledge.configureProvider": "{{provider}} कॉन्फ़िगर करें", "newKnowledge.connectNotion": "Notion कनेक्ट करें", "newKnowledge.connectProvider": "{{provider}} कनेक्ट करें", @@ -181,6 +187,7 @@ "newKnowledge.creators": "निर्माता", "newKnowledge.customIntervalHours": "कस्टम अंतराल (घंटे)", "newKnowledge.customIntervalInvalid": "1 से 720 घंटे तक की पूर्ण संख्या दर्ज करें।", + "newKnowledge.deleteDocuments": "हटाएं...", "newKnowledge.description": "विवरण", "newKnowledge.descriptionHelp": "एक स्पष्ट विवरण ऐप्स को यह निर्णय लेने में मदद करता है कि इस ज्ञान को कब खोजना है।", "newKnowledge.descriptionPlaceholder": "जैसे रिफंड नीति, शिपिंग एफएक्यू और एस्केलेशन चरण - ग्राहक सहायता प्रश्नों के लिए उपयोग करें", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "स्रोत कॉन्फ़िगरेशन और क्रॉल पूर्वावलोकन हटा दिए जाएँगे।", "newKnowledge.discardSourceDraftFailed": "स्रोत ड्राफ़्ट हटाया नहीं जा सका। फिर से प्रयास करें।", "newKnowledge.discardSourceDraftTitle": "यह स्रोत ड्राफ़्ट हटाएँ?", + "newKnowledge.documentActions": "{{name}} के लिए क्रियाएँ", + "newKnowledge.documentActionsUnavailable": "इस ज्ञानकोष के लिए दस्तावेज़ क्रियाएँ अभी तक उपलब्ध नहीं हैं।", + "newKnowledge.documentColumn": "दस्तावेज़", + "newKnowledge.documentFilterLabel": "स्थिति के अनुसार दस्तावेज़ फ़िल्टर करें", + "newKnowledge.documentPermissionRestricted": "आपके कार्यस्थान की अनुमतियाँ दस्तावेज़ या कार्य संबंधी कार्रवाइयों की अनुमति नहीं देती हैं।", + "newKnowledge.documentStatus.disabled": "निष्क्रिय", + "newKnowledge.documentStatus.failed": "असफल", + "newKnowledge.documentStatus.processing": "प्रोसेस हो रहा है", + "newKnowledge.documentStatus.queued": "कतारबद्ध", + "newKnowledge.documentStatus.ready": "तैयार", + "newKnowledge.documentUploadExclusion.batchLimit": "बैच आकार सीमा पार हो गई", + "newKnowledge.documentUploadExclusion.countLimit": "फ़ाइल संख्या सीमा पार हो गई", + "newKnowledge.documentUploadExclusion.fileSize": "फ़ाइल बहुत बड़ी है", + "newKnowledge.documentUploadExclusion.fileType": "फ़ाइल प्रकार असमर्थित या अमान्य है", + "newKnowledge.documentUploadExclusion.more": "और {{count}} अन्य", + "newKnowledge.documentUploadExclusion.processing": "प्रोसेसिंग विफल रही", + "newKnowledge.documentUploadExclusion.quota": "कार्यस्थान कोटा पार हो गया", + "newKnowledge.documentUploadExclusion.target": "दस्तावेज़ लक्ष्य अब मान्य नहीं है", + "newKnowledge.documentUploadFailed": "हम ये दस्तावेज़ अपलोड नहीं कर सके. पुनः प्रयास करें।", + "newKnowledge.documentUploadPartial": "{{accepted}} दस्तावेज़ों की प्रोसेसिंग शुरू हुई; {{excluded}} जोड़े नहीं जा सके: {{details}}", + "newKnowledge.documentUploadRejected": "कोई दस्तावेज़ स्वीकार नहीं किया गया: {{details}}", + "newKnowledge.documentUploadStarted": "दस्तावेज़ प्रसंस्करण शुरू हुआ.", "newKnowledge.documents": "दस्तावेज़", + "newKnowledge.documentsDescription": "ज्ञान परिसंपत्तियाँ और उनके संशोधन - प्रत्येक दस्तावेज़ कहाँ से आया और क्या नवीनतम संशोधन अनुक्रमित है।", + "newKnowledge.documentsDropHint": "या अपलोड करने के लिए PDF, DOCX, Markdown, HTML, XLSX, या TXT छोड़ें", + "newKnowledge.documentsEmptyDescription": "यहां फ़ाइलें अपलोड करें, या स्रोत टैब पर एक स्रोत कनेक्ट करें - दस्तावेज़ों को स्वचालित रूप से पार्स, खंडित और अनुक्रमित किया जाता है।", + "newKnowledge.documentsEmptyTitle": "अभी तक कोई दस्तावेज़ नहीं", + "newKnowledge.documentsErrorDescription": "हम दस्तावेज़ लोड नहीं कर सके. पुनः प्रयास करें।", + "newKnowledge.documentsErrorTitle": "दस्तावेज़ लोड नहीं हो सके", + "newKnowledge.documentsPermissionDescription": "आपको इस ज्ञानकोष में दस्तावेज़ देखने की अनुमति नहीं है.", + "newKnowledge.documentsPermissionTitle": "दस्तावेज़ प्रतिबंधित हैं", + "newKnowledge.documentsReindexFailed": "हम पुनः अनुक्रमणिका प्रारंभ नहीं कर सके. पुनः प्रयास करें।", + "newKnowledge.documentsReindexPartial": "{{queued}} दस्तावेज़ कतारबद्ध; {{missing}} अब मौजूद नहीं है।", + "newKnowledge.documentsReindexStarted": "पुन: अनुक्रमणिका प्रारंभ हुई.", + "newKnowledge.documentsSelected": "{{count}} दस्तावेज़ चयनित", "newKnowledge.documentsUnavailable": "— दस्तावेज़", + "newKnowledge.downloadDocuments": "डाउनलोड करें", "newKnowledge.editSource": "संपादित करें", "newKnowledge.emptyDescription": "अपने दस्तावेज़ जोड़ें ताकि एजेंट उन्हें सीधे पढ़ सकें — इंडेक्सिंग कॉन्फ़िगर करने की ज़रूरत नहीं।", "newKnowledge.emptyTitle": "नॉलेज बेस बनाने का अधिक स्मार्ट तरीका", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "नए नॉलेज बेस से मिलें ✨", "newKnowledge.illustrationHeadline": "अपनी सामग्री एक बार कनेक्ट करें — हर ऐप नवीनतम ज्ञान के साथ जवाब देगा।", "newKnowledge.includeSubpages": "उप-पेज शामिल करें", + "newKnowledge.interruptTask": "रोकें", "newKnowledge.invalidRootUrl": "मान्य http(s) URL दर्ज करें।", "newKnowledge.keepEditing": "संपादन जारी रखें", + "newKnowledge.lastReadyRevisionHint": "विफल प्रसंस्करण कभी भी पुनर्प्राप्ति को बाधित नहीं करता है - अंतिम तैयार संशोधन सामग्री सूचकांक में उपलब्ध रहता है।", "newKnowledge.lastSyncColumn": "अंतिम सिंक", "newKnowledge.learnMore": "और जानें", "newKnowledge.leavePartialSetupConfirm": "सेटअप छोड़ें", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "पुराना", "newKnowledge.loadMore": "और लोड करें", "newKnowledge.loadingSyncPolicy": "सिंक नीति लोड हो रही है", + "newKnowledge.manualUpload": "मैन्युअल अपलोड", "newKnowledge.maxPages": "अधिकतम पेज", + "newKnowledge.metadata": "मेटा डेटा", "newKnowledge.moreProviders": "और प्रदाता", "newKnowledge.name": "ज्ञान नाम", "newKnowledge.namePlaceholder": "जैसे ग्राहक सहायता पुस्तिका", "newKnowledge.nameRequired": "नॉलेज बेस नाम दर्ज करें.", "newKnowledge.new": "नया", + "newKnowledge.noBackgroundTasks": "अभी तक कोई पृष्ठभूमि कार्य नहीं.", "newKnowledge.noDescription": "कोई विवरण नहीं", + "newKnowledge.noMatchingDocuments": "इस खोज से कोई दस्तावेज़ मेल नहीं खाता.", "newKnowledge.noMatchingSources": "इस खोज से कोई स्रोत मेल नहीं खाता।", "newKnowledge.noPagesFound": "{{host}} पर कोई पेज नहीं मिला", "newKnowledge.noPagesFoundDescription": "क्रॉलर साइट तक पहुँचा, लेकिन कोई इंडेक्स करने योग्य पेज नहीं मिला। कोई दूसरा URL आज़माएँ या क्रॉल विकल्प बढ़ाएँ।", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{host}} पर {{count}} पेज क्रॉल किए गए", "newKnowledge.pagesCrawled_other": "{{host}} पर {{count}} पेज क्रॉल किए गए", "newKnowledge.pagesSelected": "{{count}} चयनित", + "newKnowledge.partialDocumentResults": "और दस्तावेज़ मेल खा सकते हैं। खोज जारी रखने के लिए और लोड करें।", "newKnowledge.permission": "अनुमति", "newKnowledge.permissionAllMembers": "कार्यक्षेत्र · सभी सदस्य देख और संपादित कर सकते हैं", + "newKnowledge.permissionLoadFailed": "आपकी संपादन अनुमतियाँ सत्यापित नहीं की जा सकीं। फिर से कोशिश करें।", "newKnowledge.permissionOnlyMe": "केवल मैं", "newKnowledge.permissionRestricted": "आपकी कार्यस्थान अनुमतियाँ इस सेटिंग को बदलने की अनुमति नहीं देतीं।", "newKnowledge.permissionUpdateFailed": "नॉलेज बेस बन गया है, लेकिन उसकी अनुमति की पुष्टि नहीं हो सकी। सेटअप पूरा करने के लिए फिर से कोशिश करें।", "newKnowledge.preview": "पूर्वावलोकन", "newKnowledge.previewUnavailable": "बैकएंड द्वारा इस फ़ाइल की प्रोसेसिंग पूरी होने के बाद पूर्वावलोकन उपलब्ध होगा।", + "newKnowledge.processDocument": "प्रक्रिया {{name}}", + "newKnowledge.processingTaskState.canceled": "बाधित", + "newKnowledge.processingTaskState.dispatch_pending": "तैयारी", + "newKnowledge.processingTaskState.failed": "असफल", + "newKnowledge.processingTaskState.queued": "कतारबद्ध", + "newKnowledge.processingTaskState.retry_wait": "पुनः प्रयास करने की प्रतीक्षा की जा रही है", + "newKnowledge.processingTaskState.running": "प्रसंस्करण · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "पूरा हुआ", + "newKnowledge.processingTaskState.superseded": "अधिक्रमण किया हुआ", "newKnowledge.providerConnected": "{{provider}} जुड़ा", "newKnowledge.providerLabel": "एक प्रदाता का चयन करें", "newKnowledge.providerLoadFailed": "प्रदाता लोड नहीं कर सके", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "पहला नॉलेज बेस बनाने के लिए कार्यस्थान संपादक से कहें।", "newKnowledge.refreshConnectionStatus": "स्थिति ताज़ा करें", "newKnowledge.refreshingConnection": "ताज़ा…", + "newKnowledge.reindexDocuments": "पुन: अनुक्रमणिका", "newKnowledge.removeSource": "स्रोत हटाएं", "newKnowledge.retryCrawl": "क्रॉल का फिर से प्रयास करें", "newKnowledge.retryProviderLoad": "पुनः प्रयास करें", "newKnowledge.retrySyncPolicy": "सिंक नीति फिर से लोड करें", + "newKnowledge.retryTask": "पुनः प्रयास करें", "newKnowledge.rootUrl": "रूट URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "यह अनुभाग अगले सेटअप चरण में उपलब्ध हो जाएगा.", + "newKnowledge.searchDocuments": "दस्तावेज़ खोजें", "newKnowledge.searchSources": "स्रोत खोजें", "newKnowledge.selectAll": "सभी चुनें", + "newKnowledge.selectAllDocuments": "सभी दस्तावेज़ चुनें", "newKnowledge.selectAllSources": "सभी स्रोत चुनें", "newKnowledge.selectFilesAndFolders": "सिंक करने के लिए फ़ाइलें और फ़ोल्डर चुनें", "newKnowledge.skippedFailed": "छोड़ा गया (विफल)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "केवल मैन्युअल", "newKnowledge.syncPolicyProvider": "प्रदाता का शेड्यूल उपयोग करें", "newKnowledge.tags": "टैग", + "newKnowledge.taskActionFailed": "हम इस कार्य को अद्यतन नहीं कर सके. ताज़ा करें और पुनः प्रयास करें.", + "newKnowledge.taskAttentionClear": "किसी भी पृष्ठभूमि कार्य पर ध्यान देने की आवश्यकता नहीं है।", + "newKnowledge.taskAttentionCount": "{{count}} पृष्ठभूमि कार्यों पर ध्यान देने की आवश्यकता है।", + "newKnowledge.taskAttentionErrorCount": "{{count}} पृष्ठभूमि कार्यों पर ध्यान देने की आवश्यकता है, जिसमें विफल कार्य भी शामिल हैं।", + "newKnowledge.taskFailedNotification": "दस्तावेज़ प्रोसेसिंग विफल — कार्य से दोबारा कोशिश करें", + "newKnowledge.taskHistoryIncomplete": "और कार्य इतिहास उपलब्ध है; ध्यान देने वाले कार्यों की संख्या अधूरी है।", + "newKnowledge.tasks": "कार्य", + "newKnowledge.tasksErrorDescription": "हम पृष्ठभूमि कार्य लोड नहीं कर सके. पुनः प्रयास करें।", + "newKnowledge.tasksWithAttention": "कार्य, {{count}} पर ध्यान देने की आवश्यकता है", "newKnowledge.unavailableDescription": "KnowledgeFS अभी इस कार्यस्थान के लिए उपलब्ध नहीं है।", "newKnowledge.unavailableTitle": "नया नॉलेज उपलब्ध नहीं है", "newKnowledge.updated": "{{date}} को अपडेट किया गया", + "newKnowledge.updatedColumn": "अद्यतन किया गया", "newKnowledge.uploadCharactersUnavailable": "वर्ण: —", + "newKnowledge.uploadDocuments": "दस्तावेज़ अपलोड करें", "newKnowledge.uploadFiles": "फ़ाइलें अपलोड करें", "newKnowledge.uploadFilesDescription": "अपने कंप्यूटर से फ़ाइलें जोड़ें — बनाते ही वे इंडेक्स हो जाएँगी।", "newKnowledge.uploadingFiles": "अपलोड हो रहा है…", diff --git a/web/i18n/id-ID/dataset.json b/web/i18n/id-ID/dataset.json index 020d8825cf9..843b8f3963a 100644 --- a/web/i18n/id-ID/dataset.json +++ b/web/i18n/id-ID/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Model Rerank diperlukan untuk campuran pengetahuan internal dan eksternal.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Tindakan", + "newKnowledge.addDocument": "Tambahkan dokumen", "newKnowledge.addSource": "Tambahkan sumber", "newKnowledge.addSourceDescription": "Hubungkan sumber ke basis pengetahuan ini — sumber tersebut akan disinkronkan secara otomatis dan tetap segar.", "newKnowledge.addSourceFailed": "Sumber ini tidak dapat ditambahkan. Tinjau pengaturan lalu coba lagi.", "newKnowledge.addSourceRequiresSelection": "Pilih setidaknya satu halaman yang dirayapi sebelum menambahkan sumber.", "newKnowledge.adjustAndRecrawl": "Sesuaikan dan rayapi ulang", + "newKnowledge.allDocumentStatuses": "Semua status", "newKnowledge.allSources": "Semua sumber", "newKnowledge.appsUnavailable": "— aplikasi", "newKnowledge.authKind.api-key": "kunci API", "newKnowledge.authKind.endpoint": "Titik akhir", "newKnowledge.authenticationMethod": "Metode otentikasi", "newKnowledge.backToList": "Kembali ke basis pengetahuan", + "newKnowledge.backgroundTasks": "Tugas latar belakang", + "newKnowledge.backgroundTasksDescription": "Penambahan dokumen, sinkronisasi sumber, dan mengindeks ulang pekerjaan.", "newKnowledge.booleanFalse": "Tidak", "newKnowledge.booleanTrue": "Ya", + "newKnowledge.bulkDocumentActions": "Tindakan dokumen massal", "newKnowledge.cancelAddSource": "Batal", "newKnowledge.cardType": "Ruang pengetahuan", + "newKnowledge.clearDocumentSelection": "Hapus pilihan dokumen", "newKnowledge.configureProvider": "Konfigurasikan {{provider}}", "newKnowledge.connectNotion": "Hubungkan Notion", "newKnowledge.connectProvider": "Hubungkan {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Pembuat", "newKnowledge.customIntervalHours": "Interval khusus (jam)", "newKnowledge.customIntervalInvalid": "Masukkan bilangan bulat antara 1 hingga 720 jam.", + "newKnowledge.deleteDocuments": "Hapus…", "newKnowledge.description": "Deskripsi", "newKnowledge.descriptionHelp": "Deskripsi yang jelas membantu aplikasi memutuskan kapan harus mencari Pengetahuan ini.", "newKnowledge.descriptionPlaceholder": "misalnya Kebijakan pengembalian dana, FAQ pengiriman, dan langkah eskalasi — digunakan untuk pertanyaan dukungan pelanggan", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Konfigurasi sumber dan pratinjau perayapan akan dihapus.", "newKnowledge.discardSourceDraftFailed": "Draf sumber tidak dapat dibuang. Coba lagi.", "newKnowledge.discardSourceDraftTitle": "Buang draf sumber ini?", + "newKnowledge.documentActions": "Tindakan untuk {{name}}", + "newKnowledge.documentActionsUnavailable": "Tindakan dokumen belum tersedia untuk basis pengetahuan ini.", + "newKnowledge.documentColumn": "Dokumen", + "newKnowledge.documentFilterLabel": "Filter dokumen berdasarkan status", + "newKnowledge.documentPermissionRestricted": "Izin ruang kerja Anda tidak mengizinkan tindakan pada dokumen atau tugas.", + "newKnowledge.documentStatus.disabled": "Dinonaktifkan", + "newKnowledge.documentStatus.failed": "Gagal", + "newKnowledge.documentStatus.processing": "Pemrosesan", + "newKnowledge.documentStatus.queued": "Mengantri", + "newKnowledge.documentStatus.ready": "Siap", + "newKnowledge.documentUploadExclusion.batchLimit": "batas ukuran batch terlampaui", + "newKnowledge.documentUploadExclusion.countLimit": "batas jumlah file terlampaui", + "newKnowledge.documentUploadExclusion.fileSize": "file terlalu besar", + "newKnowledge.documentUploadExclusion.fileType": "jenis file tidak didukung atau tidak valid", + "newKnowledge.documentUploadExclusion.more": "dan {{count}} lainnya", + "newKnowledge.documentUploadExclusion.processing": "pemrosesan gagal", + "newKnowledge.documentUploadExclusion.quota": "kuota ruang kerja terlampaui", + "newKnowledge.documentUploadExclusion.target": "target dokumen tidak lagi valid", + "newKnowledge.documentUploadFailed": "Kami tidak dapat mengunggah dokumen-dokumen ini. Coba lagi.", + "newKnowledge.documentUploadPartial": "Pemrosesan {{accepted}} dokumen dimulai; {{excluded}} tidak dapat ditambahkan: {{details}}", + "newKnowledge.documentUploadRejected": "Tidak ada dokumen yang diterima: {{details}}", + "newKnowledge.documentUploadStarted": "Pemrosesan dokumen dimulai.", "newKnowledge.documents": "Dokumen", + "newKnowledge.documentsDescription": "Aset pengetahuan dan revisinya — dari mana setiap dokumen berasal dan apakah revisi terbaru diindeks.", + "newKnowledge.documentsDropHint": "atau letakkan PDF, DOCX, Markdown, HTML, XLSX, atau TXT untuk diunggah", + "newKnowledge.documentsEmptyDescription": "Unggah file di sini, atau sambungkan sumber di tab Sumber — dokumen diurai, dikelompokkan, dan diindeks secara otomatis.", + "newKnowledge.documentsEmptyTitle": "Belum ada dokumen", + "newKnowledge.documentsErrorDescription": "Kami tidak dapat memuat dokumen. Coba lagi.", + "newKnowledge.documentsErrorTitle": "Dokumen tidak dapat dimuat", + "newKnowledge.documentsPermissionDescription": "Anda tidak memiliki izin untuk melihat dokumen di basis pengetahuan ini.", + "newKnowledge.documentsPermissionTitle": "Dokumen dibatasi", + "newKnowledge.documentsReindexFailed": "Kami tidak dapat memulai pengindeksan ulang. Coba lagi.", + "newKnowledge.documentsReindexPartial": "Dokumen {{queued}} antri; {{missing}} sudah tidak ada lagi.", + "newKnowledge.documentsReindexStarted": "Pengindeksan ulang dimulai.", + "newKnowledge.documentsSelected": "Dokumen {{count}} dipilih", "newKnowledge.documentsUnavailable": "— dokumen", + "newKnowledge.downloadDocuments": "Unduh", "newKnowledge.editSource": "Sunting", "newKnowledge.emptyDescription": "Tambahkan dokumen agar agen dapat membacanya langsung — tanpa perlu mengatur pengindeksan.", "newKnowledge.emptyTitle": "Cara yang lebih cerdas untuk membuat basis pengetahuan", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Kenali Basis Pengetahuan baru ✨", "newKnowledge.illustrationHeadline": "Hubungkan konten Anda sekali — setiap aplikasi menjawab dengan pengetahuan terbaru.", "newKnowledge.includeSubpages": "Sertakan subhalaman", + "newKnowledge.interruptTask": "Hentikan", "newKnowledge.invalidRootUrl": "Masukkan URL http(s) yang valid.", "newKnowledge.keepEditing": "Lanjutkan mengedit", + "newKnowledge.lastReadyRevisionHint": "Pemrosesan yang gagal tidak pernah mengganggu pengambilan — revisi terakhir yang siap tetap tersedia di indeks konten.", "newKnowledge.lastSyncColumn": "Sinkronisasi terakhir", "newKnowledge.learnMore": "Pelajari lebih lanjut", "newKnowledge.leavePartialSetupConfirm": "Keluar dari penyiapan", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Lama", "newKnowledge.loadMore": "Muat lebih banyak", "newKnowledge.loadingSyncPolicy": "Memuat kebijakan sinkronisasi", + "newKnowledge.manualUpload": "Unggah secara manual", "newKnowledge.maxPages": "Halaman maksimum", + "newKnowledge.metadata": "Data Meta", "newKnowledge.moreProviders": "Penyedia lainnya", "newKnowledge.name": "Nama pengetahuan", "newKnowledge.namePlaceholder": "misalnya Buku Pegangan Dukungan Pelanggan", "newKnowledge.nameRequired": "Masukkan nama basis pengetahuan.", "newKnowledge.new": "Baru", + "newKnowledge.noBackgroundTasks": "Belum ada tugas latar belakang.", "newKnowledge.noDescription": "Tidak ada deskripsi", + "newKnowledge.noMatchingDocuments": "Tidak ada dokumen yang cocok dengan pencarian ini.", "newKnowledge.noMatchingSources": "Tidak ada sumber yang cocok dengan pencarian ini.", "newKnowledge.noPagesFound": "Tidak ada halaman yang ditemukan di {{host}}", "newKnowledge.noPagesFoundDescription": "Perayap mencapai situs tetapi tidak menemukan halaman yang dapat diindeks. Coba URL lain atau perluas opsi perayapan.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} halaman dirayapi di {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} halaman dirayapi di {{host}}", "newKnowledge.pagesSelected": "{{count}} dipilih", + "newKnowledge.partialDocumentResults": "Mungkin ada dokumen lain yang cocok. Muat lebih banyak untuk melanjutkan pencarian.", "newKnowledge.permission": "Izin", "newKnowledge.permissionAllMembers": "Ruang Kerja · semua anggota dapat melihat dan mengedit", + "newKnowledge.permissionLoadFailed": "Izin pengeditan Anda tidak dapat diverifikasi. Coba lagi.", "newKnowledge.permissionOnlyMe": "Hanya aku", "newKnowledge.permissionRestricted": "Izin ruang kerja Anda tidak mengizinkan perubahan pengaturan ini.", "newKnowledge.permissionUpdateFailed": "Basis pengetahuan telah dibuat, tetapi izinnya tidak dapat dikonfirmasi. Coba lagi untuk menyelesaikan penyiapan.", "newKnowledge.preview": "Pratinjau", "newKnowledge.previewUnavailable": "Pratinjau tersedia setelah backend selesai memproses file ini.", + "newKnowledge.processDocument": "Proses {{name}}", + "newKnowledge.processingTaskState.canceled": "Terganggu", + "newKnowledge.processingTaskState.dispatch_pending": "Mempersiapkan", + "newKnowledge.processingTaskState.failed": "Gagal", + "newKnowledge.processingTaskState.queued": "Mengantri", + "newKnowledge.processingTaskState.retry_wait": "Menunggu untuk mencoba lagi", + "newKnowledge.processingTaskState.running": "Pemrosesan · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Selesai", + "newKnowledge.processingTaskState.superseded": "Digantikan", "newKnowledge.providerConnected": "{{provider}} terhubung", "newKnowledge.providerLabel": "Pilih penyedia", "newKnowledge.providerLoadFailed": "Penyedia tidak dapat memuat", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Minta editor ruang kerja membuat basis pengetahuan pertama.", "newKnowledge.refreshConnectionStatus": "Segarkan status", "newKnowledge.refreshingConnection": "Menyegarkan…", + "newKnowledge.reindexDocuments": "Indeks ulang", "newKnowledge.removeSource": "Hapus sumber", "newKnowledge.retryCrawl": "Coba perayapan lagi", "newKnowledge.retryProviderLoad": "Coba lagi", "newKnowledge.retrySyncPolicy": "Coba lagi kebijakan sinkronisasi", + "newKnowledge.retryTask": "Coba lagi", "newKnowledge.rootUrl": "URL root", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Bagian ini akan tersedia pada langkah penyiapan berikutnya.", + "newKnowledge.searchDocuments": "Cari dokumen", "newKnowledge.searchSources": "Cari sumber", "newKnowledge.selectAll": "Pilih semua", + "newKnowledge.selectAllDocuments": "Pilih semua dokumen", "newKnowledge.selectAllSources": "Pilih semua sumber", "newKnowledge.selectFilesAndFolders": "Pilih file dan folder untuk disinkronkan", "newKnowledge.skippedFailed": "Dilewati (gagal)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Hanya manual", "newKnowledge.syncPolicyProvider": "Gunakan jadwal penyedia", "newKnowledge.tags": "Tag", + "newKnowledge.taskActionFailed": "Kami tidak dapat memperbarui tugas ini. Segarkan dan coba lagi.", + "newKnowledge.taskAttentionClear": "Tidak ada tugas latar belakang yang memerlukan perhatian.", + "newKnowledge.taskAttentionCount": "Tugas latar belakang {{count}} perlu diperhatikan.", + "newKnowledge.taskAttentionErrorCount": "Tugas latar belakang {{count}} memerlukan perhatian, termasuk tugas yang gagal.", + "newKnowledge.taskFailedNotification": "Pemrosesan dokumen gagal — coba lagi dari Tugas", + "newKnowledge.taskHistoryIncomplete": "Riwayat tugas lainnya tersedia; jumlah tugas yang perlu diperhatikan belum lengkap.", + "newKnowledge.tasks": "Tugas", + "newKnowledge.tasksErrorDescription": "Kami tidak dapat memuat tugas latar belakang. Coba lagi.", + "newKnowledge.tasksWithAttention": "Tugas, {{count}} perlu perhatian", "newKnowledge.unavailableDescription": "KnowledgeFS saat ini tidak tersedia untuk ruang kerja ini.", "newKnowledge.unavailableTitle": "Pengetahuan Baru tidak tersedia", "newKnowledge.updated": "Diperbarui {{date}}", + "newKnowledge.updatedColumn": "Diperbarui", "newKnowledge.uploadCharactersUnavailable": "Karakter: —", + "newKnowledge.uploadDocuments": "Unggah dokumen", "newKnowledge.uploadFiles": "Unggah file", "newKnowledge.uploadFilesDescription": "Tambahkan file dari komputer — file akan diindeks segera setelah dibuat.", "newKnowledge.uploadingFiles": "Mengunggah…", diff --git a/web/i18n/it-IT/dataset.json b/web/i18n/it-IT/dataset.json index 16f6d733e00..e38310e1b06 100644 --- a/web/i18n/it-IT/dataset.json +++ b/web/i18n/it-IT/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Il modello Rerank è necessario per la combinazione di conoscenze interne ed esterne.", "multimodal": "Multimodale", "newKnowledge.actionsColumn": "Azioni", + "newKnowledge.addDocument": "Aggiungi documento", "newKnowledge.addSource": "Aggiungi fonte", "newKnowledge.addSourceDescription": "Collega una fonte a questa knowledge base: si sincronizza automaticamente e rimane aggiornata.", "newKnowledge.addSourceFailed": "Impossibile aggiungere questa origine. Controlla le impostazioni e riprova.", "newKnowledge.addSourceRequiresSelection": "Seleziona almeno una pagina sottoposta a scansione prima di aggiungere la fonte.", "newKnowledge.adjustAndRecrawl": "Modifica e ripeti la scansione", + "newKnowledge.allDocumentStatuses": "Tutto lo stato", "newKnowledge.allSources": "Tutte le fonti", "newKnowledge.appsUnavailable": "— app", "newKnowledge.authKind.api-key": "Chiave API", "newKnowledge.authKind.endpoint": "Punto finale", "newKnowledge.authenticationMethod": "Metodo di autenticazione", "newKnowledge.backToList": "Torniamo alle basi di conoscenza", + "newKnowledge.backgroundTasks": "Attività in background", + "newKnowledge.backgroundTasksDescription": "Aggiunte di documenti, sincronizzazioni di origini e processi di reindicizzazione.", "newKnowledge.booleanFalse": "No", "newKnowledge.booleanTrue": "Sì", + "newKnowledge.bulkDocumentActions": "Azioni di documenti in blocco", "newKnowledge.cancelAddSource": "Annulla", "newKnowledge.cardType": "Spazio di conoscenza", + "newKnowledge.clearDocumentSelection": "Cancella la selezione del documento", "newKnowledge.configureProvider": "Configura {{provider}}", "newKnowledge.connectNotion": "Connetti Notion", "newKnowledge.connectProvider": "Connetti {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Autori", "newKnowledge.customIntervalHours": "Intervallo personalizzato (ore)", "newKnowledge.customIntervalInvalid": "Inserisci un numero intero compreso tra 1 e 720 ore.", + "newKnowledge.deleteDocuments": "Elimina…", "newKnowledge.description": "Descrizione", "newKnowledge.descriptionHelp": "Una descrizione chiara aiuta le app a decidere quando cercare questa conoscenza.", "newKnowledge.descriptionPlaceholder": "per esempio. Politica di rimborso, domande frequenti sulla spedizione e passaggi di escalation: utilizzali per domande sull'assistenza clienti", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "La configurazione della fonte e l’anteprima della scansione verranno rimosse.", "newKnowledge.discardSourceDraftFailed": "Impossibile eliminare la bozza della fonte. Riprova.", "newKnowledge.discardSourceDraftTitle": "Eliminare questa bozza della fonte?", + "newKnowledge.documentActions": "Azioni per {{name}}", + "newKnowledge.documentActionsUnavailable": "Le azioni del documento non sono ancora disponibili per questa knowledge base.", + "newKnowledge.documentColumn": "Documento", + "newKnowledge.documentFilterLabel": "Filtra i documenti per stato", + "newKnowledge.documentPermissionRestricted": "Le autorizzazioni del workspace non consentono azioni su documenti o attività.", + "newKnowledge.documentStatus.disabled": "Disattivato", + "newKnowledge.documentStatus.failed": "Fallito", + "newKnowledge.documentStatus.processing": "Elaborazione", + "newKnowledge.documentStatus.queued": "In coda", + "newKnowledge.documentStatus.ready": "Pronto", + "newKnowledge.documentUploadExclusion.batchLimit": "limite delle dimensioni del batch superato", + "newKnowledge.documentUploadExclusion.countLimit": "limite del numero di file superato", + "newKnowledge.documentUploadExclusion.fileSize": "il file è troppo grande", + "newKnowledge.documentUploadExclusion.fileType": "tipo di file non supportato o non valido", + "newKnowledge.documentUploadExclusion.more": "e altri {{count}}", + "newKnowledge.documentUploadExclusion.processing": "elaborazione non riuscita", + "newKnowledge.documentUploadExclusion.quota": "quota dell'area di lavoro superata", + "newKnowledge.documentUploadExclusion.target": "la destinazione del documento non è più valida", + "newKnowledge.documentUploadFailed": "Non è stato possibile caricare questi documenti. Riprova.", + "newKnowledge.documentUploadPartial": "Elaborazione avviata per {{accepted}} documenti; impossibile aggiungerne {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "Nessun documento è stato accettato: {{details}}", + "newKnowledge.documentUploadStarted": "È iniziata l'elaborazione del documento.", "newKnowledge.documents": "Documenti", + "newKnowledge.documentsDescription": "Risorse di conoscenza e relative revisioni: da dove proviene ciascun documento e se l'ultima revisione è indicizzata.", + "newKnowledge.documentsDropHint": "oppure rilascia PDF, DOCX, Markdown, HTML, XLSX o TXT da caricare", + "newKnowledge.documentsEmptyDescription": "Carica i file qui o collega una fonte nella scheda Fonti: i documenti vengono analizzati, suddivisi in blocchi e indicizzati automaticamente.", + "newKnowledge.documentsEmptyTitle": "Nessun documento ancora", + "newKnowledge.documentsErrorDescription": "Non è stato possibile caricare i documenti. Riprova.", + "newKnowledge.documentsErrorTitle": "Impossibile caricare i documenti", + "newKnowledge.documentsPermissionDescription": "Non hai l'autorizzazione per visualizzare i documenti in questa knowledge base.", + "newKnowledge.documentsPermissionTitle": "I documenti sono limitati", + "newKnowledge.documentsReindexFailed": "Impossibile avviare la reindicizzazione. Riprova.", + "newKnowledge.documentsReindexPartial": "{{queued}} documenti in coda; {{missing}} non esistono più.", + "newKnowledge.documentsReindexStarted": "È iniziata la reindicizzazione.", + "newKnowledge.documentsSelected": "Documenti {{count}} selezionati", "newKnowledge.documentsUnavailable": "— documenti", + "newKnowledge.downloadDocuments": "Scarica", "newKnowledge.editSource": "Modifica", "newKnowledge.emptyDescription": "Aggiungi i tuoi documenti e gli agenti potranno leggerli direttamente, senza configurare l’indicizzazione.", "newKnowledge.emptyTitle": "Un modo più intelligente per creare knowledge base", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Scopri la nuova Knowledge Base ✨", "newKnowledge.illustrationHeadline": "Collega i tuoi contenuti una sola volta: ogni app risponderà con le conoscenze più aggiornate.", "newKnowledge.includeSubpages": "Includi sottopagine", + "newKnowledge.interruptTask": "Interrompere", "newKnowledge.invalidRootUrl": "Inserisci un URL http(s) valido.", "newKnowledge.keepEditing": "Continua a modificare", + "newKnowledge.lastReadyRevisionHint": "L'elaborazione non riuscita non interrompe mai il recupero: l'ultima revisione pronta rimane disponibile nell'indice del contenuto.", "newKnowledge.lastSyncColumn": "Ultima sincronizzazione", "newKnowledge.learnMore": "Scopri di più", "newKnowledge.leavePartialSetupConfirm": "Esci dalla configurazione", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Legacy", "newKnowledge.loadMore": "Carica altro", "newKnowledge.loadingSyncPolicy": "Caricamento dei criteri di sincronizzazione", + "newKnowledge.manualUpload": "Caricamento manuale", "newKnowledge.maxPages": "Numero massimo di pagine", + "newKnowledge.metadata": "Metadati", "newKnowledge.moreProviders": "Altri provider", "newKnowledge.name": "Nome della conoscenza", "newKnowledge.namePlaceholder": "ad es. Manuale dell'assistenza clienti", "newKnowledge.nameRequired": "Inserisci un nome per la knowledge base.", "newKnowledge.new": "Nuovo", + "newKnowledge.noBackgroundTasks": "Ancora nessuna attività in background.", "newKnowledge.noDescription": "Nessuna descrizione", + "newKnowledge.noMatchingDocuments": "Nessun documento corrisponde a questa ricerca.", "newKnowledge.noMatchingSources": "Nessuna fonte corrisponde a questa ricerca.", "newKnowledge.noPagesFound": "Nessuna pagina trovata su {{host}}", "newKnowledge.noPagesFoundDescription": "Il crawler ha raggiunto il sito ma non ha trovato pagine indicizzabili. Prova un URL diverso o amplia le opzioni di scansione.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} pagine sottoposte a scansione su {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} pagine sottoposte a scansione su {{host}}", "newKnowledge.pagesSelected": "{{count}} selezionati", + "newKnowledge.partialDocumentResults": "Potrebbero esserci altri documenti corrispondenti. Caricane altri per continuare la ricerca.", "newKnowledge.permission": "Autorizzazione", "newKnowledge.permissionAllMembers": "Area di lavoro · tutti i membri possono visualizzare e modificare", + "newKnowledge.permissionLoadFailed": "Impossibile verificare le autorizzazioni di modifica. Riprova.", "newKnowledge.permissionOnlyMe": "Solo io", "newKnowledge.permissionRestricted": "Le autorizzazioni dell'area di lavoro non consentono di modificare questa impostazione.", "newKnowledge.permissionUpdateFailed": "La knowledge base è stata creata, ma non è stato possibile confermarne l’autorizzazione. Riprova per completare la configurazione.", "newKnowledge.preview": "Anteprima", "newKnowledge.previewUnavailable": "L’anteprima sarà disponibile quando il backend avrà elaborato questo file.", + "newKnowledge.processDocument": "Processo {{name}}", + "newKnowledge.processingTaskState.canceled": "Interrotto", + "newKnowledge.processingTaskState.dispatch_pending": "Preparazione", + "newKnowledge.processingTaskState.failed": "Fallito", + "newKnowledge.processingTaskState.queued": "In coda", + "newKnowledge.processingTaskState.retry_wait": "In attesa di riprovare", + "newKnowledge.processingTaskState.running": "Elaborazione · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Completato", + "newKnowledge.processingTaskState.superseded": "Sostituito", "newKnowledge.providerConnected": "{{provider}} connesso", "newKnowledge.providerLabel": "Seleziona un fornitore", "newKnowledge.providerLoadFailed": "Impossibile caricare i provider", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Chiedi a un editor dello spazio di lavoro di creare la prima knowledge base.", "newKnowledge.refreshConnectionStatus": "Aggiorna lo stato", "newKnowledge.refreshingConnection": "Aggiornamento…", + "newKnowledge.reindexDocuments": "Reindicizzare", "newKnowledge.removeSource": "Rimuovi la fonte", "newKnowledge.retryCrawl": "Riprova la scansione", "newKnowledge.retryProviderLoad": "Riprova", "newKnowledge.retrySyncPolicy": "Riprova i criteri di sincronizzazione", + "newKnowledge.retryTask": "Riprova", "newKnowledge.rootUrl": "URL principale", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Questa sezione diventerà disponibile nella fase di configurazione successiva.", + "newKnowledge.searchDocuments": "Cerca documenti", "newKnowledge.searchSources": "Cerca fonti", "newKnowledge.selectAll": "Seleziona tutto", + "newKnowledge.selectAllDocuments": "Seleziona tutti i documenti", "newKnowledge.selectAllSources": "Seleziona tutte le origini", "newKnowledge.selectFilesAndFolders": "Seleziona file e cartelle da sincronizzare", "newKnowledge.skippedFailed": "Ignorato (errore)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Solo manuale", "newKnowledge.syncPolicyProvider": "Usa la pianificazione del provider", "newKnowledge.tags": "Tag", + "newKnowledge.taskActionFailed": "Impossibile aggiornare questa attività. Aggiorna e riprova.", + "newKnowledge.taskAttentionClear": "Nessuna attività in background richiede attenzione.", + "newKnowledge.taskAttentionCount": "Le attività in background {{count}} richiedono attenzione.", + "newKnowledge.taskAttentionErrorCount": "Le attività in background {{count}} richiedono attenzione, comprese le attività non riuscite.", + "newKnowledge.taskFailedNotification": "Elaborazione del documento non riuscita — riprova da Attività", + "newKnowledge.taskHistoryIncomplete": "È disponibile altra cronologia delle attività; il conteggio delle attività che richiedono attenzione è incompleto.", + "newKnowledge.tasks": "Compiti", + "newKnowledge.tasksErrorDescription": "Non è stato possibile caricare le attività in background. Riprova.", + "newKnowledge.tasksWithAttention": "I compiti, {{count}} richiedono attenzione", "newKnowledge.unavailableDescription": "KnowledgeFS non è al momento disponibile per questo spazio di lavoro.", "newKnowledge.unavailableTitle": "La nuova conoscenza non è disponibile", "newKnowledge.updated": "Aggiornato il {{date}}", + "newKnowledge.updatedColumn": "Aggiornato", "newKnowledge.uploadCharactersUnavailable": "Caratteri: —", + "newKnowledge.uploadDocuments": "Carica documenti", "newKnowledge.uploadFiles": "Carica file", "newKnowledge.uploadFilesDescription": "Aggiungi file dal computer: vengono indicizzati appena li crei.", "newKnowledge.uploadingFiles": "Caricamento…", diff --git a/web/i18n/ja-JP/dataset.json b/web/i18n/ja-JP/dataset.json index 8dbfc1e10fc..20faf3d04d8 100644 --- a/web/i18n/ja-JP/dataset.json +++ b/web/i18n/ja-JP/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "内部と外部のナレッジベースを混在させる場合、Rerank モデルが必要です。", "multimodal": "マルチモーダル", "newKnowledge.actionsColumn": "アクション", + "newKnowledge.addDocument": "ドキュメントの追加", "newKnowledge.addSource": "ソースを追加", "newKnowledge.addSourceDescription": "ソースをこのナレッジ ベースに接続すると、自動的に同期され、最新の状態が保たれます。", "newKnowledge.addSourceFailed": "このソースを追加できませんでした。設定を確認して、もう一度お試しください。", "newKnowledge.addSourceRequiresSelection": "追加前にクロール済みページを1つ以上選択してください。", "newKnowledge.adjustAndRecrawl": "調整して再クロール", + "newKnowledge.allDocumentStatuses": "すべてのステータス", "newKnowledge.allSources": "すべてのソース", "newKnowledge.appsUnavailable": "— 個のアプリ", "newKnowledge.authKind.api-key": "APIキー", "newKnowledge.authKind.endpoint": "エンドポイント", "newKnowledge.authenticationMethod": "認証方法", "newKnowledge.backToList": "ナレッジベースに戻る", + "newKnowledge.backgroundTasks": "バックグラウンドタスク", + "newKnowledge.backgroundTasksDescription": "ドキュメントの追加、ソースの同期、ジョブの再インデックス。", "newKnowledge.booleanFalse": "いいえ", "newKnowledge.booleanTrue": "はい", + "newKnowledge.bulkDocumentActions": "一括ドキュメントアクション", "newKnowledge.cancelAddSource": "キャンセル", "newKnowledge.cardType": "ナレッジスペース", + "newKnowledge.clearDocumentSelection": "ドキュメントの選択をクリアする", "newKnowledge.configureProvider": "{{provider}} を構成する", "newKnowledge.connectNotion": "Notion に接続", "newKnowledge.connectProvider": "{{provider}} を接続する", @@ -181,6 +187,7 @@ "newKnowledge.creators": "作成者", "newKnowledge.customIntervalHours": "カスタム間隔(時間)", "newKnowledge.customIntervalInvalid": "1~720 の整数を入力してください。", + "newKnowledge.deleteDocuments": "削除…", "newKnowledge.description": "説明", "newKnowledge.descriptionHelp": "明確な説明は、アプリがこのナレッジをいつ検索するかを決定するのに役立ちます。", "newKnowledge.descriptionPlaceholder": "例:返金ポリシー、配送に関する FAQ、エスカレーション手順 - カスタマー サポートの質問に使用します", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "ソース設定とクロールプレビューは削除されます。", "newKnowledge.discardSourceDraftFailed": "ソースの下書きを破棄できませんでした。もう一度お試しください。", "newKnowledge.discardSourceDraftTitle": "このソースの下書きを破棄しますか?", + "newKnowledge.documentActions": "{{name}} のアクション", + "newKnowledge.documentActionsUnavailable": "ドキュメント アクションは、このナレッジ ベースではまだ利用できません。", + "newKnowledge.documentColumn": "文書", + "newKnowledge.documentFilterLabel": "ドキュメントをステータスでフィルタリングする", + "newKnowledge.documentPermissionRestricted": "ワークスペースの権限では、ドキュメントまたはタスクの操作を実行できません。", + "newKnowledge.documentStatus.disabled": "無効", + "newKnowledge.documentStatus.failed": "失敗しました", + "newKnowledge.documentStatus.processing": "処理中", + "newKnowledge.documentStatus.queued": "キューに入れられました", + "newKnowledge.documentStatus.ready": "準備完了", + "newKnowledge.documentUploadExclusion.batchLimit": "バッチサイズの上限を超えています", + "newKnowledge.documentUploadExclusion.countLimit": "ファイル数の上限を超えています", + "newKnowledge.documentUploadExclusion.fileSize": "ファイルが大きすぎます", + "newKnowledge.documentUploadExclusion.fileType": "サポートされていないか無効なファイル形式です", + "newKnowledge.documentUploadExclusion.more": "ほか {{count}} 件", + "newKnowledge.documentUploadExclusion.processing": "処理に失敗しました", + "newKnowledge.documentUploadExclusion.quota": "ワークスペースのクォータを超えています", + "newKnowledge.documentUploadExclusion.target": "ドキュメントの対象が無効になっています", + "newKnowledge.documentUploadFailed": "これらの書類をアップロードできませんでした。もう一度やり直してください。", + "newKnowledge.documentUploadPartial": "{{accepted}} 件のドキュメントの処理を開始しました。{{excluded}} 件は追加できませんでした:{{details}}", + "newKnowledge.documentUploadRejected": "ドキュメントを受け付けられませんでした:{{details}}", + "newKnowledge.documentUploadStarted": "文書処理が開始されました。", "newKnowledge.documents": "書類", + "newKnowledge.documentsDescription": "ナレッジ資産とそのリビジョン - 各ドキュメントの出所と、最新のリビジョンがインデックス付けされているかどうか。", + "newKnowledge.documentsDropHint": "または、PDF、DOCX、Markdown、HTML、XLSX、または TXT をドロップしてアップロードします", + "newKnowledge.documentsEmptyDescription": "ここでファイルをアップロードするか、「ソース」タブでソースを接続します。ドキュメントは自動的に解析され、チャンク化され、インデックスが作成されます。", + "newKnowledge.documentsEmptyTitle": "まだドキュメントがありません", + "newKnowledge.documentsErrorDescription": "書類を読み込めませんでした。もう一度やり直してください。", + "newKnowledge.documentsErrorTitle": "ドキュメントを読み込めませんでした", + "newKnowledge.documentsPermissionDescription": "このナレッジ ベースのドキュメントを表示する権限がありません。", + "newKnowledge.documentsPermissionTitle": "書類が制限されている", + "newKnowledge.documentsReindexFailed": "インデックスの再作成を開始できませんでした。もう一度やり直してください。", + "newKnowledge.documentsReindexPartial": "{{queued}} ドキュメントがキューに入れられました。 {{missing}} は存在しません。", + "newKnowledge.documentsReindexStarted": "再インデックス作成が開始されました。", + "newKnowledge.documentsSelected": "{{count}} ドキュメントが選択されました", "newKnowledge.documentsUnavailable": "— 件のドキュメント", + "newKnowledge.downloadDocuments": "ダウンロード", "newKnowledge.editSource": "編集", "newKnowledge.emptyDescription": "ドキュメントを追加すると、エージェントが直接読み取れます。インデックス設定は不要です。", "newKnowledge.emptyTitle": "ナレッジベースをよりスマートに構築", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "新しいナレッジベースへようこそ ✨", "newKnowledge.illustrationHeadline": "コンテンツを一度接続すれば、すべてのアプリが最新のナレッジで回答します。", "newKnowledge.includeSubpages": "サブページを含める", + "newKnowledge.interruptTask": "中断", "newKnowledge.invalidRootUrl": "有効なhttp(s) URLを入力してください。", "newKnowledge.keepEditing": "編集を続ける", + "newKnowledge.lastReadyRevisionHint": "処理が失敗しても取得が中断されることはありません。準備ができた最後のリビジョンはコンテンツ インデックス内で利用可能なままになります。", "newKnowledge.lastSyncColumn": "最終同期", "newKnowledge.learnMore": "詳細を見る", "newKnowledge.leavePartialSetupConfirm": "セットアップを終了", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "旧バージョン", "newKnowledge.loadMore": "さらに読み込む", "newKnowledge.loadingSyncPolicy": "同期ポリシーを読み込み中", + "newKnowledge.manualUpload": "手動アップロード", "newKnowledge.maxPages": "最大ページ数", + "newKnowledge.metadata": "メタデータ", "newKnowledge.moreProviders": "その他のプロバイダー", "newKnowledge.name": "ナレッジ名", "newKnowledge.namePlaceholder": "例:カスタマーサポートハンドブック", "newKnowledge.nameRequired": "ナレッジベース名を入力します。", "newKnowledge.new": "新バージョン", + "newKnowledge.noBackgroundTasks": "バックグラウンドタスクはまだありません。", "newKnowledge.noDescription": "説明なし", + "newKnowledge.noMatchingDocuments": "この検索に一致するドキュメントはありません。", "newKnowledge.noMatchingSources": "この検索に一致するソースはありません。", "newKnowledge.noPagesFound": "{{host}} でページが見つかりませんでした", "newKnowledge.noPagesFoundDescription": "サイトには到達しましたが、インデックス可能なページが見つかりませんでした。別のURLを試すか、クロールオプションを広げてください。", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{host}} で{{count}}ページをクロールしました", "newKnowledge.pagesCrawled_other": "{{host}} で{{count}}ページをクロールしました", "newKnowledge.pagesSelected": "{{count}} 件選択済み", + "newKnowledge.partialDocumentResults": "一致するドキュメントがほかにもある可能性があります。検索を続けるにはさらに読み込んでください。", "newKnowledge.permission": "許可", "newKnowledge.permissionAllMembers": "ワークスペース · すべてのメンバーが表示および編集できます", + "newKnowledge.permissionLoadFailed": "編集権限を確認できませんでした。もう一度お試しください。", "newKnowledge.permissionOnlyMe": "私だけ", "newKnowledge.permissionRestricted": "ワークスペースの権限では、この設定を変更することはできません。", "newKnowledge.permissionUpdateFailed": "ナレッジベースは作成されましたが、権限を確認できませんでした。再試行してセットアップを完了してください。", "newKnowledge.preview": "プレビュー", "newKnowledge.previewUnavailable": "バックエンドでこのファイルの処理が完了すると、プレビューを利用できます。", + "newKnowledge.processDocument": "プロセス {{name}}", + "newKnowledge.processingTaskState.canceled": "中断されました", + "newKnowledge.processingTaskState.dispatch_pending": "準備中", + "newKnowledge.processingTaskState.failed": "失敗しました", + "newKnowledge.processingTaskState.queued": "キューに入れられました", + "newKnowledge.processingTaskState.retry_wait": "再試行を待っています", + "newKnowledge.processingTaskState.running": "処理中・{{progress}}%", + "newKnowledge.processingTaskState.succeeded": "完了しました", + "newKnowledge.processingTaskState.superseded": "置き換えられました", "newKnowledge.providerConnected": "{{provider}} が接続されました", "newKnowledge.providerLabel": "プロバイダーを選択する", "newKnowledge.providerLoadFailed": "プロバイダーを読み込めませんでした", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "最初のナレッジベースの作成をワークスペース編集者に依頼してください。", "newKnowledge.refreshConnectionStatus": "ステータスの更新", "newKnowledge.refreshingConnection": "爽やか…", + "newKnowledge.reindexDocuments": "インデックスを再作成する", "newKnowledge.removeSource": "ソースを削除", "newKnowledge.retryCrawl": "クロールを再試行", "newKnowledge.retryProviderLoad": "再試行", "newKnowledge.retrySyncPolicy": "同期ポリシーを再読み込み", + "newKnowledge.retryTask": "再試行", "newKnowledge.rootUrl": "ルートURL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "このセクションは、次のセットアップ手順で使用できるようになります。", + "newKnowledge.searchDocuments": "文書の検索", "newKnowledge.searchSources": "ソースを検索", "newKnowledge.selectAll": "すべて選択", + "newKnowledge.selectAllDocuments": "すべてのドキュメントを選択します", "newKnowledge.selectAllSources": "すべてのソースを選択", "newKnowledge.selectFilesAndFolders": "同期するファイルとフォルダーを選択", "newKnowledge.skippedFailed": "スキップ(失敗)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "手動のみ", "newKnowledge.syncPolicyProvider": "プロバイダーのスケジュールを使用", "newKnowledge.tags": "タグ", + "newKnowledge.taskActionFailed": "このタスクを更新できませんでした。更新して再試行してください。", + "newKnowledge.taskAttentionClear": "注意を必要とするバックグラウンド タスクはありません。", + "newKnowledge.taskAttentionCount": "{{count}} バックグラウンド タスクには注意が必要です。", + "newKnowledge.taskAttentionErrorCount": "{{count}} バックグラウンド タスク (失敗したタスクも含む) には注意が必要です。", + "newKnowledge.taskFailedNotification": "ドキュメントの処理に失敗しました —「タスク」から再試行してください", + "newKnowledge.taskHistoryIncomplete": "さらにタスク履歴があります。対応が必要なタスク数は確定していません。", + "newKnowledge.tasks": "タスク", + "newKnowledge.tasksErrorDescription": "バックグラウンドタスクをロードできませんでした。もう一度やり直してください。", + "newKnowledge.tasksWithAttention": "タスク、{{count}} は注意が必要です", "newKnowledge.unavailableDescription": "このワークスペースでは現在 KnowledgeFS を利用できません。", "newKnowledge.unavailableTitle": "新しいナレッジは利用できません", "newKnowledge.updated": "{{date}} に更新", + "newKnowledge.updatedColumn": "更新されました", "newKnowledge.uploadCharactersUnavailable": "文字数:—", + "newKnowledge.uploadDocuments": "書類のアップロード", "newKnowledge.uploadFiles": "ファイルをアップロード", "newKnowledge.uploadFilesDescription": "コンピューターからファイルを追加すると、作成時にすぐインデックス化されます。", "newKnowledge.uploadingFiles": "アップロード中…", diff --git a/web/i18n/ko-KR/dataset.json b/web/i18n/ko-KR/dataset.json index ccb7239968e..9b102f3b97c 100644 --- a/web/i18n/ko-KR/dataset.json +++ b/web/i18n/ko-KR/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "리랭크 모델은 내부 및 외부 지식의 혼합에 필요합니다.", "multimodal": "멀티모달", "newKnowledge.actionsColumn": "작업", + "newKnowledge.addDocument": "문서 추가", "newKnowledge.addSource": "소스 추가", "newKnowledge.addSourceDescription": "이 지식 베이스에 소스를 연결하세요. 자동으로 동기화되고 최신 상태로 유지됩니다.", "newKnowledge.addSourceFailed": "이 소스를 추가하지 못했습니다. 설정을 검토한 후 다시 시도하세요.", "newKnowledge.addSourceRequiresSelection": "소스를 추가하기 전에 크롤링된 페이지를 하나 이상 선택하세요.", "newKnowledge.adjustAndRecrawl": "조정 후 다시 크롤링", + "newKnowledge.allDocumentStatuses": "모든 상태", "newKnowledge.allSources": "모든 소스", "newKnowledge.appsUnavailable": "앱 —개", "newKnowledge.authKind.api-key": "API 키", "newKnowledge.authKind.endpoint": "종점", "newKnowledge.authenticationMethod": "인증 방법", "newKnowledge.backToList": "지식 베이스로 돌아가기", + "newKnowledge.backgroundTasks": "백그라운드 작업", + "newKnowledge.backgroundTasksDescription": "문서 추가, 소스 동기화 및 재색인 작업.", "newKnowledge.booleanFalse": "아니요", "newKnowledge.booleanTrue": "예", + "newKnowledge.bulkDocumentActions": "대량 문서 작업", "newKnowledge.cancelAddSource": "취소", "newKnowledge.cardType": "지식 공간", + "newKnowledge.clearDocumentSelection": "문서 선택 지우기", "newKnowledge.configureProvider": "{{provider}} 구성", "newKnowledge.connectNotion": "Notion 연결", "newKnowledge.connectProvider": "{{provider}} 연결", @@ -181,6 +187,7 @@ "newKnowledge.creators": "생성자", "newKnowledge.customIntervalHours": "사용자 지정 간격(시간)", "newKnowledge.customIntervalInvalid": "1~720 사이의 정수를 입력하세요.", + "newKnowledge.deleteDocuments": "삭제…", "newKnowledge.description": "설명", "newKnowledge.descriptionHelp": "명확한 설명은 앱이 이 지식을 검색할 시기를 결정하는 데 도움이 됩니다.", "newKnowledge.descriptionPlaceholder": "예를 들어 환불 정책, 배송 FAQ, 에스컬레이션 단계 - 고객 지원 질문에 사용", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "소스 구성과 크롤링 미리보기가 삭제됩니다.", "newKnowledge.discardSourceDraftFailed": "소스 초안을 삭제할 수 없습니다. 다시 시도하세요.", "newKnowledge.discardSourceDraftTitle": "이 소스 초안을 삭제할까요?", + "newKnowledge.documentActions": "{{name}}에 대한 작업", + "newKnowledge.documentActionsUnavailable": "이 기술 자료에는 아직 문서 작업을 사용할 수 없습니다.", + "newKnowledge.documentColumn": "문서", + "newKnowledge.documentFilterLabel": "상태별로 문서 필터링", + "newKnowledge.documentPermissionRestricted": "워크스페이스 권한으로는 문서 또는 작업 동작을 수행할 수 없습니다.", + "newKnowledge.documentStatus.disabled": "비활성화", + "newKnowledge.documentStatus.failed": "실패", + "newKnowledge.documentStatus.processing": "처리 중", + "newKnowledge.documentStatus.queued": "대기 중", + "newKnowledge.documentStatus.ready": "준비됨", + "newKnowledge.documentUploadExclusion.batchLimit": "배치 크기 제한을 초과했습니다", + "newKnowledge.documentUploadExclusion.countLimit": "파일 수 제한을 초과했습니다", + "newKnowledge.documentUploadExclusion.fileSize": "파일이 너무 큽니다", + "newKnowledge.documentUploadExclusion.fileType": "지원되지 않거나 잘못된 파일 형식입니다", + "newKnowledge.documentUploadExclusion.more": "외 {{count}}개", + "newKnowledge.documentUploadExclusion.processing": "처리에 실패했습니다", + "newKnowledge.documentUploadExclusion.quota": "워크스페이스 할당량을 초과했습니다", + "newKnowledge.documentUploadExclusion.target": "문서 대상이 더 이상 유효하지 않습니다", + "newKnowledge.documentUploadFailed": "이 문서를 업로드할 수 없습니다. 다시 시도해 보세요.", + "newKnowledge.documentUploadPartial": "문서 {{accepted}}개의 처리를 시작했습니다. {{excluded}}개는 추가하지 못했습니다: {{details}}", + "newKnowledge.documentUploadRejected": "수락된 문서가 없습니다: {{details}}", + "newKnowledge.documentUploadStarted": "문서 처리가 시작되었습니다.", "newKnowledge.documents": "문서", + "newKnowledge.documentsDescription": "지식 자산 및 해당 개정판 - 각 문서의 출처와 최신 개정판의 색인 여부.", + "newKnowledge.documentsDropHint": "또는 PDF, DOCX, Markdown, HTML, XLSX 또는 TXT를 드롭하여 업로드하세요.", + "newKnowledge.documentsEmptyDescription": "여기에 파일을 업로드하거나 소스 탭에서 소스를 연결하세요. 문서는 자동으로 구문 분석, 청크화 및 색인화됩니다.", + "newKnowledge.documentsEmptyTitle": "아직 문서가 없습니다.", + "newKnowledge.documentsErrorDescription": "문서를 로드할 수 없습니다. 다시 시도해 보세요.", + "newKnowledge.documentsErrorTitle": "문서를 로드할 수 없습니다.", + "newKnowledge.documentsPermissionDescription": "이 기술 자료의 문서를 볼 권한이 없습니다.", + "newKnowledge.documentsPermissionTitle": "문서가 제한되어 있습니다.", + "newKnowledge.documentsReindexFailed": "색인 다시 생성을 시작할 수 없습니다. 다시 시도해 보세요.", + "newKnowledge.documentsReindexPartial": "{{queued}} 문서가 대기 중입니다. {{missing}}는 더 이상 존재하지 않습니다.", + "newKnowledge.documentsReindexStarted": "재인덱싱이 시작되었습니다.", + "newKnowledge.documentsSelected": "{{count}} 문서가 선택됨", "newKnowledge.documentsUnavailable": "문서 —개", + "newKnowledge.downloadDocuments": "다운로드", "newKnowledge.editSource": "편집", "newKnowledge.emptyDescription": "문서를 추가하면 에이전트가 직접 읽을 수 있습니다. 인덱싱 설정은 필요하지 않습니다.", "newKnowledge.emptyTitle": "지식 베이스를 구축하는 더 스마트한 방법", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "새로운 지식 베이스를 만나보세요 ✨", "newKnowledge.illustrationHeadline": "콘텐츠를 한 번 연결하면 모든 앱이 최신 지식으로 답변합니다.", "newKnowledge.includeSubpages": "하위 페이지 포함", + "newKnowledge.interruptTask": "중단", "newKnowledge.invalidRootUrl": "유효한 http(s) URL을 입력하세요.", "newKnowledge.keepEditing": "계속 편집", + "newKnowledge.lastReadyRevisionHint": "실패한 처리는 검색을 중단하지 않습니다. 마지막으로 준비된 개정은 콘텐츠 인덱스에서 계속 사용할 수 있습니다.", "newKnowledge.lastSyncColumn": "마지막 동기화", "newKnowledge.learnMore": "자세히 알아보기", "newKnowledge.leavePartialSetupConfirm": "설정 나가기", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "레거시", "newKnowledge.loadMore": "더 보기", "newKnowledge.loadingSyncPolicy": "동기화 정책 불러오는 중", + "newKnowledge.manualUpload": "수동 업로드", "newKnowledge.maxPages": "최대 페이지 수", + "newKnowledge.metadata": "메타데이터", "newKnowledge.moreProviders": "더 많은 공급자", "newKnowledge.name": "지식 이름", "newKnowledge.namePlaceholder": "예를 들어 고객 지원 핸드북", "newKnowledge.nameRequired": "기술 자료 이름을 입력하세요.", "newKnowledge.new": "신규", + "newKnowledge.noBackgroundTasks": "아직 백그라운드 작업이 없습니다.", "newKnowledge.noDescription": "설명 없음", + "newKnowledge.noMatchingDocuments": "이 검색과 일치하는 문서가 없습니다.", "newKnowledge.noMatchingSources": "이 검색과 일치하는 소스가 없습니다.", "newKnowledge.noPagesFound": "{{host}}에서 페이지를 찾을 수 없습니다", "newKnowledge.noPagesFoundDescription": "사이트에 접속했지만 색인 가능한 페이지를 찾지 못했습니다. 다른 URL을 사용하거나 크롤링 옵션을 확장하세요.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{host}}에서 {{count}}개 페이지를 크롤링했습니다", "newKnowledge.pagesCrawled_other": "{{host}}에서 {{count}}개 페이지를 크롤링했습니다", "newKnowledge.pagesSelected": "{{count}}개 선택됨", + "newKnowledge.partialDocumentResults": "일치하는 문서가 더 있을 수 있습니다. 검색을 계속하려면 더 불러오세요.", "newKnowledge.permission": "허가", "newKnowledge.permissionAllMembers": "작업공간 · 모든 구성원이 보고 편집할 수 있습니다.", + "newKnowledge.permissionLoadFailed": "편집 권한을 확인하지 못했습니다. 다시 시도해 주세요.", "newKnowledge.permissionOnlyMe": "나만", "newKnowledge.permissionRestricted": "작업공간 권한으로 인해 이 설정을 변경할 수 없습니다.", "newKnowledge.permissionUpdateFailed": "지식 베이스가 생성되었지만 권한을 확인하지 못했습니다. 다시 시도하여 설정을 완료하세요.", "newKnowledge.preview": "미리보기", "newKnowledge.previewUnavailable": "백엔드에서 이 파일 처리를 완료하면 미리보기를 사용할 수 있습니다.", + "newKnowledge.processDocument": "프로세스 {{name}}", + "newKnowledge.processingTaskState.canceled": "중단됨", + "newKnowledge.processingTaskState.dispatch_pending": "준비 중", + "newKnowledge.processingTaskState.failed": "실패", + "newKnowledge.processingTaskState.queued": "대기 중", + "newKnowledge.processingTaskState.retry_wait": "재시도 대기 중", + "newKnowledge.processingTaskState.running": "처리 중 · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "완료됨", + "newKnowledge.processingTaskState.superseded": "대체됨", "newKnowledge.providerConnected": "{{provider}} 연결됨", "newKnowledge.providerLabel": "제공자 선택", "newKnowledge.providerLoadFailed": "제공자를 로드할 수 없습니다.", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "워크스페이스 편집자에게 첫 번째 지식 베이스 생성을 요청하세요.", "newKnowledge.refreshConnectionStatus": "새로 고침 상태", "newKnowledge.refreshingConnection": "상쾌하다…", + "newKnowledge.reindexDocuments": "재인덱싱", "newKnowledge.removeSource": "소스 제거", "newKnowledge.retryCrawl": "크롤링 다시 시도", "newKnowledge.retryProviderLoad": "재시도", "newKnowledge.retrySyncPolicy": "동기화 정책 다시 시도", + "newKnowledge.retryTask": "재시도", "newKnowledge.rootUrl": "루트 URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "이 섹션은 다음 설정 단계에서 사용할 수 있습니다.", + "newKnowledge.searchDocuments": "문서 검색", "newKnowledge.searchSources": "소스 검색", "newKnowledge.selectAll": "모두 선택", + "newKnowledge.selectAllDocuments": "모든 문서 선택", "newKnowledge.selectAllSources": "모든 소스 선택", "newKnowledge.selectFilesAndFolders": "동기화할 파일과 폴더 선택", "newKnowledge.skippedFailed": "건너뜀(실패)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "수동 전용", "newKnowledge.syncPolicyProvider": "제공업체 일정 사용", "newKnowledge.tags": "태그", + "newKnowledge.taskActionFailed": "이 작업을 업데이트할 수 없습니다. 새로고침하고 다시 시도하세요.", + "newKnowledge.taskAttentionClear": "주의가 필요한 백그라운드 작업은 없습니다.", + "newKnowledge.taskAttentionCount": "{{count}} 백그라운드 작업에 주의가 필요합니다.", + "newKnowledge.taskAttentionErrorCount": "{{count}} 실패한 작업을 포함하여 백그라운드 작업에 주의가 필요합니다.", + "newKnowledge.taskFailedNotification": "문서 처리 실패 — 작업에서 다시 시도하세요", + "newKnowledge.taskHistoryIncomplete": "더 많은 작업 기록이 있습니다. 주의가 필요한 작업 수는 아직 완전하지 않습니다.", + "newKnowledge.tasks": "작업", + "newKnowledge.tasksErrorDescription": "백그라운드 작업을 로드할 수 없습니다. 다시 시도해 보세요.", + "newKnowledge.tasksWithAttention": "작업, {{count}}에 주의가 필요합니다.", "newKnowledge.unavailableDescription": "현재 이 워크스페이스에서 KnowledgeFS를 사용할 수 없습니다.", "newKnowledge.unavailableTitle": "새 지식을 사용할 수 없음", "newKnowledge.updated": "{{date}} 업데이트", + "newKnowledge.updatedColumn": "업데이트됨", "newKnowledge.uploadCharactersUnavailable": "문자 수: —", + "newKnowledge.uploadDocuments": "문서 업로드", "newKnowledge.uploadFiles": "파일 업로드", "newKnowledge.uploadFilesDescription": "컴퓨터에서 파일을 추가하면 생성 즉시 인덱싱됩니다.", "newKnowledge.uploadingFiles": "업로드 중…", diff --git a/web/i18n/nl-NL/dataset.json b/web/i18n/nl-NL/dataset.json index a13f71037cb..3a799a53955 100644 --- a/web/i18n/nl-NL/dataset.json +++ b/web/i18n/nl-NL/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "The Rerank model is required for mixture of internal and external knowledge.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Acties", + "newKnowledge.addDocument": "Document toevoegen", "newKnowledge.addSource": "Bron toevoegen", "newKnowledge.addSourceDescription": "Verbind een bron met deze kennisbank: deze synchroniseert automatisch en blijft actueel.", "newKnowledge.addSourceFailed": "Deze bron kon niet worden toegevoegd. Controleer de instellingen en probeer het opnieuw.", "newKnowledge.addSourceRequiresSelection": "Selecteer minstens één gecrawlde pagina voordat je de bron toevoegt.", "newKnowledge.adjustAndRecrawl": "Aanpassen en opnieuw crawlen", + "newKnowledge.allDocumentStatuses": "Alle status", "newKnowledge.allSources": "Alle bronnen", "newKnowledge.appsUnavailable": "— apps", "newKnowledge.authKind.api-key": "API-sleutel", "newKnowledge.authKind.endpoint": "Eindpunt", "newKnowledge.authenticationMethod": "Authenticatiemethode", "newKnowledge.backToList": "Terug naar kennisbanken", + "newKnowledge.backgroundTasks": "Achtergrondtaken", + "newKnowledge.backgroundTasksDescription": "Documenten toevoegen, bronsynchronisaties en taken opnieuw indexeren.", "newKnowledge.booleanFalse": "Nee", "newKnowledge.booleanTrue": "Ja", + "newKnowledge.bulkDocumentActions": "Bulkdocumentacties", "newKnowledge.cancelAddSource": "Annuleren", "newKnowledge.cardType": "Kennisruimte", + "newKnowledge.clearDocumentSelection": "Documentselectie wissen", "newKnowledge.configureProvider": "{{provider}} configureren", "newKnowledge.connectNotion": "Notion verbinden", "newKnowledge.connectProvider": "{{provider}} verbinden", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Makers", "newKnowledge.customIntervalHours": "Aangepast interval (uren)", "newKnowledge.customIntervalInvalid": "Voer een geheel getal van 1 tot 720 uur in.", + "newKnowledge.deleteDocuments": "Verwijderen…", "newKnowledge.description": "Beschrijving", "newKnowledge.descriptionHelp": "Een duidelijke beschrijving helpt apps beslissen wanneer ze deze kennis moeten doorzoeken.", "newKnowledge.descriptionPlaceholder": "bijv. Terugbetalingsbeleid, veelgestelde vragen over verzending en escalatiestappen - gebruik deze voor vragen over klantenondersteuning", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "De bronconfiguratie en het crawlvoorbeeld worden verwijderd.", "newKnowledge.discardSourceDraftFailed": "Het bronconcept kon niet worden verwijderd. Probeer het opnieuw.", "newKnowledge.discardSourceDraftTitle": "Dit bronconcept verwijderen?", + "newKnowledge.documentActions": "Acties voor {{name}}", + "newKnowledge.documentActionsUnavailable": "Documentacties zijn nog niet beschikbaar voor deze kennisbank.", + "newKnowledge.documentColumn": "Document", + "newKnowledge.documentFilterLabel": "Documenten filteren op status", + "newKnowledge.documentPermissionRestricted": "Uw werkruimtemachtigingen staan geen document- of taakacties toe.", + "newKnowledge.documentStatus.disabled": "Uitgeschakeld", + "newKnowledge.documentStatus.failed": "Mislukt", + "newKnowledge.documentStatus.processing": "Verwerking", + "newKnowledge.documentStatus.queued": "In de wachtrij", + "newKnowledge.documentStatus.ready": "Klaar", + "newKnowledge.documentUploadExclusion.batchLimit": "limiet voor batchgrootte overschreden", + "newKnowledge.documentUploadExclusion.countLimit": "limiet voor aantal bestanden overschreden", + "newKnowledge.documentUploadExclusion.fileSize": "bestand is te groot", + "newKnowledge.documentUploadExclusion.fileType": "niet-ondersteund of ongeldig bestandstype", + "newKnowledge.documentUploadExclusion.more": "en nog {{count}}", + "newKnowledge.documentUploadExclusion.processing": "verwerking mislukt", + "newKnowledge.documentUploadExclusion.quota": "werkruimtequotum overschreden", + "newKnowledge.documentUploadExclusion.target": "documentdoel is niet meer geldig", + "newKnowledge.documentUploadFailed": "We konden deze documenten niet uploaden. Probeer het opnieuw.", + "newKnowledge.documentUploadPartial": "Verwerking van {{accepted}} documenten gestart; {{excluded}} konden niet worden toegevoegd: {{details}}", + "newKnowledge.documentUploadRejected": "Er zijn geen documenten geaccepteerd: {{details}}", + "newKnowledge.documentUploadStarted": "Documentverwerking gestart.", "newKnowledge.documents": "Documenten", + "newKnowledge.documentsDescription": "Kennismiddelen en hun herzieningen – waar elk document vandaan komt en of de laatste herziening is geïndexeerd.", + "newKnowledge.documentsDropHint": "of zet PDF, DOCX, Markdown, HTML, XLSX of TXT neer om te uploaden", + "newKnowledge.documentsEmptyDescription": "Upload hier bestanden of sluit een bron aan op het tabblad Bronnen: documenten worden automatisch geparseerd, gesegmenteerd en geïndexeerd.", + "newKnowledge.documentsEmptyTitle": "Nog geen documenten", + "newKnowledge.documentsErrorDescription": "We konden geen documenten laden. Probeer het opnieuw.", + "newKnowledge.documentsErrorTitle": "Documenten konden niet worden geladen", + "newKnowledge.documentsPermissionDescription": "U heeft geen toestemming om documenten in deze kennisbank te bekijken.", + "newKnowledge.documentsPermissionTitle": "Documenten zijn beperkt", + "newKnowledge.documentsReindexFailed": "We konden niet beginnen met herindexeren. Probeer het opnieuw.", + "newKnowledge.documentsReindexPartial": "{{queued}}-documenten in wachtrij; {{missing}} bestaat niet meer.", + "newKnowledge.documentsReindexStarted": "Het herindexeren is gestart.", + "newKnowledge.documentsSelected": "{{count}}-documenten geselecteerd", "newKnowledge.documentsUnavailable": "— documenten", + "newKnowledge.downloadDocuments": "Downloaden", "newKnowledge.editSource": "Bewerken", "newKnowledge.emptyDescription": "Voeg je documenten toe zodat agents ze direct kunnen lezen, zonder indexering te configureren.", "newKnowledge.emptyTitle": "Een slimmere manier om kennisbanken te bouwen", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Maak kennis met de nieuwe Kennisbank ✨", "newKnowledge.illustrationHeadline": "Koppel je content één keer — elke app antwoordt met de meest actuele kennis.", "newKnowledge.includeSubpages": "Subpagina’s opnemen", + "newKnowledge.interruptTask": "Onderbreken", "newKnowledge.invalidRootUrl": "Voer een geldige http(s)-URL in.", "newKnowledge.keepEditing": "Doorgaan met bewerken", + "newKnowledge.lastReadyRevisionHint": "Een mislukte verwerking onderbreekt het ophalen nooit; de laatste gereedstaande revisie blijft beschikbaar in de inhoudsindex.", "newKnowledge.lastSyncColumn": "Laatste synchronisatie", "newKnowledge.learnMore": "Meer informatie", "newKnowledge.leavePartialSetupConfirm": "Configuratie verlaten", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Oud", "newKnowledge.loadMore": "Meer laden", "newKnowledge.loadingSyncPolicy": "Synchronisatiebeleid laden", + "newKnowledge.manualUpload": "Handmatig uploaden", "newKnowledge.maxPages": "Maximumaantal pagina’s", + "newKnowledge.metadata": "Metagegevens", "newKnowledge.moreProviders": "Meer providers", "newKnowledge.name": "Kennis naam", "newKnowledge.namePlaceholder": "bijv. Handboek voor klantenondersteuning", "newKnowledge.nameRequired": "Voer een kennisbanknaam in.", "newKnowledge.new": "Nieuw", + "newKnowledge.noBackgroundTasks": "Nog geen achtergrondtaken.", "newKnowledge.noDescription": "Geen beschrijving", + "newKnowledge.noMatchingDocuments": "Er zijn geen documenten die overeenkomen met deze zoekopdracht.", "newKnowledge.noMatchingSources": "Er zijn geen bronnen die overeenkomen met deze zoekopdracht.", "newKnowledge.noPagesFound": "Geen pagina’s gevonden op {{host}}", "newKnowledge.noPagesFoundDescription": "De crawler bereikte de site, maar vond geen indexeerbare pagina’s. Probeer een andere URL of breid de crawlopties uit.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} pagina’s gecrawld op {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} pagina’s gecrawld op {{host}}", "newKnowledge.pagesSelected": "{{count}} geselecteerd", + "newKnowledge.partialDocumentResults": "Er kunnen meer overeenkomende documenten zijn. Laad meer om verder te zoeken.", "newKnowledge.permission": "Toestemming", "newKnowledge.permissionAllMembers": "Werkruimte · alle leden kunnen bekijken en bewerken", + "newKnowledge.permissionLoadFailed": "Uw bewerkingsmachtigingen konden niet worden gecontroleerd. Probeer het opnieuw.", "newKnowledge.permissionOnlyMe": "Alleen ik", "newKnowledge.permissionRestricted": "Uw werkruimtemachtigingen staan het wijzigen van deze instelling niet toe.", "newKnowledge.permissionUpdateFailed": "De kennisbank is gemaakt, maar de machtiging kon niet worden bevestigd. Probeer opnieuw om de configuratie te voltooien.", "newKnowledge.preview": "Voorbeeld", "newKnowledge.previewUnavailable": "Het voorbeeld is beschikbaar nadat de backend dit bestand heeft verwerkt.", + "newKnowledge.processDocument": "Proces {{name}}", + "newKnowledge.processingTaskState.canceled": "Onderbroken", + "newKnowledge.processingTaskState.dispatch_pending": "Voorbereiden", + "newKnowledge.processingTaskState.failed": "Mislukt", + "newKnowledge.processingTaskState.queued": "In de wachtrij", + "newKnowledge.processingTaskState.retry_wait": "Wachten op nieuwe poging", + "newKnowledge.processingTaskState.running": "Verwerking · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Voltooid", + "newKnowledge.processingTaskState.superseded": "Vervangen", "newKnowledge.providerConnected": "{{provider}} aangesloten", "newKnowledge.providerLabel": "Select a provider", "newKnowledge.providerLoadFailed": "Providers konden niet laden", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Vraag een werkruimte-editor om de eerste kennisbank te maken.", "newKnowledge.refreshConnectionStatus": "Vernieuwingsstatus", "newKnowledge.refreshingConnection": "Verfrissen…", + "newKnowledge.reindexDocuments": "Opnieuw indexeren", "newKnowledge.removeSource": "Bron verwijderen", "newKnowledge.retryCrawl": "Crawl opnieuw proberen", "newKnowledge.retryProviderLoad": "Probeer het opnieuw", "newKnowledge.retrySyncPolicy": "Synchronisatiebeleid opnieuw proberen", + "newKnowledge.retryTask": "Opnieuw proberen", "newKnowledge.rootUrl": "Hoofd-URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Deze sectie komt beschikbaar in de volgende installatiestap.", + "newKnowledge.searchDocuments": "Documenten zoeken", "newKnowledge.searchSources": "Zoek bronnen", "newKnowledge.selectAll": "Alles selecteren", + "newKnowledge.selectAllDocuments": "Selecteer alle documenten", "newKnowledge.selectAllSources": "Alle bronnen selecteren", "newKnowledge.selectFilesAndFolders": "Selecteer bestanden en mappen om te synchroniseren", "newKnowledge.skippedFailed": "Overgeslagen (mislukt)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Alleen handmatig", "newKnowledge.syncPolicyProvider": "Planning van provider gebruiken", "newKnowledge.tags": "Tags", + "newKnowledge.taskActionFailed": "We kunnen deze taak niet updaten. Vernieuw en probeer het opnieuw.", + "newKnowledge.taskAttentionClear": "Er zijn geen achtergrondtaken die aandacht behoeven.", + "newKnowledge.taskAttentionCount": "{{count}}-achtergrondtaken vereisen aandacht.", + "newKnowledge.taskAttentionErrorCount": "{{count}}-achtergrondtaken hebben aandacht nodig, inclusief mislukte taken.", + "newKnowledge.taskFailedNotification": "Documentverwerking mislukt — probeer opnieuw via Taken", + "newKnowledge.taskHistoryIncomplete": "Er is meer taakgeschiedenis beschikbaar; het aantal taken dat aandacht vereist is onvolledig.", + "newKnowledge.tasks": "Taken", + "newKnowledge.tasksErrorDescription": "We konden achtergrondtaken niet laden. Probeer het opnieuw.", + "newKnowledge.tasksWithAttention": "Taken, {{count}} hebben aandacht nodig", "newKnowledge.unavailableDescription": "KnowledgeFS is momenteel niet beschikbaar voor deze werkruimte.", "newKnowledge.unavailableTitle": "Nieuwe kennis is niet beschikbaar", "newKnowledge.updated": "Bijgewerkt op {{date}}", + "newKnowledge.updatedColumn": "Bijgewerkt", "newKnowledge.uploadCharactersUnavailable": "Tekens: —", + "newKnowledge.uploadDocuments": "Documenten uploaden", "newKnowledge.uploadFiles": "Bestanden uploaden", "newKnowledge.uploadFilesDescription": "Voeg bestanden van je computer toe; ze worden geïndexeerd zodra je ze aanmaakt.", "newKnowledge.uploadingFiles": "Uploaden…", diff --git a/web/i18n/pl-PL/dataset.json b/web/i18n/pl-PL/dataset.json index 353348d56c5..f8eee855eb3 100644 --- a/web/i18n/pl-PL/dataset.json +++ b/web/i18n/pl-PL/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Model Rerank jest wymagany do połączenia wiedzy wewnętrznej i zewnętrznej.", "multimodal": "Multimodalny", "newKnowledge.actionsColumn": "Działania", + "newKnowledge.addDocument": "Dodaj dokument", "newKnowledge.addSource": "Dodaj źródło", "newKnowledge.addSourceDescription": "Podłącz źródło do tej bazy wiedzy — synchronizuje się ona automatycznie i pozostaje aktualna.", "newKnowledge.addSourceFailed": "Nie udało się dodać tego źródła. Sprawdź ustawienia i spróbuj ponownie.", "newKnowledge.addSourceRequiresSelection": "Przed dodaniem źródła wybierz co najmniej jedną zindeksowaną stronę.", "newKnowledge.adjustAndRecrawl": "Dostosuj i przeskanuj ponownie", + "newKnowledge.allDocumentStatuses": "Wszystkie stany", "newKnowledge.allSources": "Wszystkie źródła", "newKnowledge.appsUnavailable": "— aplikacji", "newKnowledge.authKind.api-key": "Klucz API", "newKnowledge.authKind.endpoint": "Punkt końcowy", "newKnowledge.authenticationMethod": "Metoda uwierzytelniania", "newKnowledge.backToList": "Powrót do baz wiedzy", + "newKnowledge.backgroundTasks": "Zadania w tle", + "newKnowledge.backgroundTasksDescription": "Dodawanie dokumentów, synchronizacja źródeł i ponowne indeksowanie zadań.", "newKnowledge.booleanFalse": "Nie", "newKnowledge.booleanTrue": "Tak", + "newKnowledge.bulkDocumentActions": "Zbiorcze akcje dokumentów", "newKnowledge.cancelAddSource": "Anuluj", "newKnowledge.cardType": "Przestrzeń wiedzy", + "newKnowledge.clearDocumentSelection": "Wyczyść wybór dokumentu", "newKnowledge.configureProvider": "Skonfiguruj {{provider}}", "newKnowledge.connectNotion": "Połącz z Notion", "newKnowledge.connectProvider": "Połącz {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Twórcy", "newKnowledge.customIntervalHours": "Niestandardowy interwał (godziny)", "newKnowledge.customIntervalInvalid": "Wprowadź liczbę całkowitą od 1 do 720 godzin.", + "newKnowledge.deleteDocuments": "Usuń…", "newKnowledge.description": "Opis", "newKnowledge.descriptionHelp": "Przejrzysty opis pomaga aplikacjom zdecydować, kiedy przeszukiwać tę wiedzę.", "newKnowledge.descriptionPlaceholder": "np. Polityka zwrotów, często zadawane pytania dotyczące wysyłki i etapy eskalacji — używaj w przypadku pytań do obsługi klienta", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Konfiguracja źródła i podgląd skanowania zostaną usunięte.", "newKnowledge.discardSourceDraftFailed": "Nie udało się odrzucić wersji roboczej źródła. Spróbuj ponownie.", "newKnowledge.discardSourceDraftTitle": "Odrzucić tę wersję roboczą źródła?", + "newKnowledge.documentActions": "Działania dla {{name}}", + "newKnowledge.documentActionsUnavailable": "Działania dotyczące dokumentów nie są jeszcze dostępne dla tej bazy wiedzy.", + "newKnowledge.documentColumn": "Dokument", + "newKnowledge.documentFilterLabel": "Filtruj dokumenty według statusu", + "newKnowledge.documentPermissionRestricted": "Uprawnienia Twojego obszaru roboczego nie pozwalają na działania na dokumentach ani zadaniach.", + "newKnowledge.documentStatus.disabled": "Wyłączony", + "newKnowledge.documentStatus.failed": "Nie udało się", + "newKnowledge.documentStatus.processing": "Przetwarzanie", + "newKnowledge.documentStatus.queued": "W kolejce", + "newKnowledge.documentStatus.ready": "Gotowy", + "newKnowledge.documentUploadExclusion.batchLimit": "przekroczono limit rozmiaru partii", + "newKnowledge.documentUploadExclusion.countLimit": "przekroczono limit liczby plików", + "newKnowledge.documentUploadExclusion.fileSize": "plik jest zbyt duży", + "newKnowledge.documentUploadExclusion.fileType": "nieobsługiwany lub nieprawidłowy typ pliku", + "newKnowledge.documentUploadExclusion.more": "i jeszcze {{count}}", + "newKnowledge.documentUploadExclusion.processing": "przetwarzanie nie powiodło się", + "newKnowledge.documentUploadExclusion.quota": "przekroczono limit obszaru roboczego", + "newKnowledge.documentUploadExclusion.target": "miejsce docelowe dokumentu jest już nieprawidłowe", + "newKnowledge.documentUploadFailed": "Nie mogliśmy przesłać tych dokumentów. Spróbuj ponownie.", + "newKnowledge.documentUploadPartial": "Rozpoczęto przetwarzanie {{accepted}} dokumentów; nie udało się dodać {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "Nie zaakceptowano żadnych dokumentów: {{details}}", + "newKnowledge.documentUploadStarted": "Rozpoczęto przetwarzanie dokumentu.", "newKnowledge.documents": "Dokumenty", + "newKnowledge.documentsDescription": "Zasoby wiedzy i ich wersje — skąd pochodzi każdy dokument i czy najnowsza wersja jest indeksowana.", + "newKnowledge.documentsDropHint": "lub upuść pliki PDF, DOCX, Markdown, HTML, XLSX lub TXT, aby je przesłać", + "newKnowledge.documentsEmptyDescription": "Prześlij pliki tutaj lub podłącz źródło na karcie Źródła — dokumenty są automatycznie analizowane, dzielone na porcje i indeksowane.", + "newKnowledge.documentsEmptyTitle": "Nie ma jeszcze żadnych dokumentów", + "newKnowledge.documentsErrorDescription": "Nie mogliśmy załadować dokumentów. Spróbuj ponownie.", + "newKnowledge.documentsErrorTitle": "Nie udało się wczytać dokumentów", + "newKnowledge.documentsPermissionDescription": "Nie masz uprawnień do przeglądania dokumentów w tej bazie wiedzy.", + "newKnowledge.documentsPermissionTitle": "Dokumenty są zastrzeżone", + "newKnowledge.documentsReindexFailed": "Nie można rozpocząć ponownego indeksowania. Spróbuj ponownie.", + "newKnowledge.documentsReindexPartial": "{{queued}} dokumenty w kolejce; {{missing}} już nie istnieje.", + "newKnowledge.documentsReindexStarted": "Rozpoczęło się ponowne indeksowanie.", + "newKnowledge.documentsSelected": "Wybrano dokumenty {{count}}", "newKnowledge.documentsUnavailable": "— dokumentów", + "newKnowledge.downloadDocuments": "Pobierz", "newKnowledge.editSource": "Edytuj", "newKnowledge.emptyDescription": "Dodaj dokumenty, aby agenci mogli je czytać bezpośrednio — bez konfigurowania indeksowania.", "newKnowledge.emptyTitle": "Inteligentniejszy sposób tworzenia baz wiedzy", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Poznaj nową bazę wiedzy ✨", "newKnowledge.illustrationHeadline": "Połącz treści raz — każda aplikacja odpowie, korzystając z najnowszej wiedzy.", "newKnowledge.includeSubpages": "Uwzględnij podstrony", + "newKnowledge.interruptTask": "Przerwij", "newKnowledge.invalidRootUrl": "Wprowadź prawidłowy adres URL http(s).", "newKnowledge.keepEditing": "Kontynuuj edycję", + "newKnowledge.lastReadyRevisionHint": "Nieudane przetwarzanie nigdy nie przerywa pobierania — ostatnia gotowa wersja pozostaje dostępna w indeksie zawartości.", "newKnowledge.lastSyncColumn": "Ostatnia synchronizacja", "newKnowledge.learnMore": "Dowiedz się więcej", "newKnowledge.leavePartialSetupConfirm": "Opuść konfigurację", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Starsza wersja", "newKnowledge.loadMore": "Załaduj więcej", "newKnowledge.loadingSyncPolicy": "Ładowanie zasad synchronizacji", + "newKnowledge.manualUpload": "Przesyłanie ręczne", "newKnowledge.maxPages": "Maksymalna liczba stron", + "newKnowledge.metadata": "Metadane", "newKnowledge.moreProviders": "Więcej dostawców", "newKnowledge.name": "Nazwa wiedzy", "newKnowledge.namePlaceholder": "np. Podręcznik obsługi klienta", "newKnowledge.nameRequired": "Wprowadź nazwę bazy wiedzy.", "newKnowledge.new": "Nowa wersja", + "newKnowledge.noBackgroundTasks": "Nie ma jeszcze żadnych zadań w tle.", "newKnowledge.noDescription": "Brak opisu", + "newKnowledge.noMatchingDocuments": "Żadne dokumenty nie pasują do tego wyszukiwania.", "newKnowledge.noMatchingSources": "Żadne źródła nie pasują do tego wyszukiwania.", "newKnowledge.noPagesFound": "Nie znaleziono stron w {{host}}", "newKnowledge.noPagesFoundDescription": "Robot dotarł do witryny, ale nie znalazł stron możliwych do zindeksowania. Użyj innego adresu URL lub rozszerz opcje skanowania.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "Przeskanowano {{count}} stron w {{host}}", "newKnowledge.pagesCrawled_other": "Przeskanowano {{count}} stron w {{host}}", "newKnowledge.pagesSelected": "Wybrano: {{count}}", + "newKnowledge.partialDocumentResults": "Więcej dokumentów może pasować. Wczytaj więcej, aby kontynuować wyszukiwanie.", "newKnowledge.permission": "Pozwolenie", "newKnowledge.permissionAllMembers": "Obszar roboczy · wszyscy członkowie mogą przeglądać i edytować", + "newKnowledge.permissionLoadFailed": "Nie udało się zweryfikować uprawnień do edycji. Spróbuj ponownie.", "newKnowledge.permissionOnlyMe": "Tylko ja", "newKnowledge.permissionRestricted": "Uprawnienia Twojego obszaru roboczego nie pozwalają na zmianę tego ustawienia.", "newKnowledge.permissionUpdateFailed": "Baza wiedzy została utworzona, ale nie udało się potwierdzić jej uprawnień. Spróbuj ponownie, aby ukończyć konfigurację.", "newKnowledge.preview": "Podgląd", "newKnowledge.previewUnavailable": "Podgląd będzie dostępny po zakończeniu przetwarzania tego pliku przez backend.", + "newKnowledge.processDocument": "Proces {{name}}", + "newKnowledge.processingTaskState.canceled": "Przerwano", + "newKnowledge.processingTaskState.dispatch_pending": "Przygotowanie", + "newKnowledge.processingTaskState.failed": "Nie udało się", + "newKnowledge.processingTaskState.queued": "W kolejce", + "newKnowledge.processingTaskState.retry_wait": "Oczekiwanie na ponowną próbę", + "newKnowledge.processingTaskState.running": "Przetwarzanie · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Ukończono", + "newKnowledge.processingTaskState.superseded": "Zastąpione", "newKnowledge.providerConnected": "{{provider}} podłączony", "newKnowledge.providerLabel": "Wybierz dostawcę", "newKnowledge.providerLoadFailed": "Dostawcy nie mogli załadować", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Poproś edytora obszaru roboczego o utworzenie pierwszej bazy wiedzy.", "newKnowledge.refreshConnectionStatus": "Odśwież status", "newKnowledge.refreshingConnection": "Odświeżanie…", + "newKnowledge.reindexDocuments": "Ponowne indeksowanie", "newKnowledge.removeSource": "Usuń źródło", "newKnowledge.retryCrawl": "Ponów skanowanie", "newKnowledge.retryProviderLoad": "Spróbuj ponownie", "newKnowledge.retrySyncPolicy": "Ponów ładowanie zasad synchronizacji", + "newKnowledge.retryTask": "Spróbuj ponownie", "newKnowledge.rootUrl": "Główny adres URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Ta sekcja będzie dostępna w następnym kroku konfiguracji.", + "newKnowledge.searchDocuments": "Szukaj dokumentów", "newKnowledge.searchSources": "Wyszukaj źródła", "newKnowledge.selectAll": "Zaznacz wszystko", + "newKnowledge.selectAllDocuments": "Wybierz wszystkie dokumenty", "newKnowledge.selectAllSources": "Wybierz wszystkie źródła", "newKnowledge.selectFilesAndFolders": "Wybierz pliki i foldery do synchronizacji", "newKnowledge.skippedFailed": "Pominięto (błąd)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Tylko ręcznie", "newKnowledge.syncPolicyProvider": "Użyj harmonogramu dostawcy", "newKnowledge.tags": "Tagi", + "newKnowledge.taskActionFailed": "Nie mogliśmy zaktualizować tego zadania. Odśwież i spróbuj ponownie.", + "newKnowledge.taskAttentionClear": "Żadne zadania w tle nie wymagają uwagi.", + "newKnowledge.taskAttentionCount": "Zadania w tle {{count}} wymagają uwagi.", + "newKnowledge.taskAttentionErrorCount": "Zadania w tle {{count}} wymagają uwagi, w tym zadania zakończone niepowodzeniem.", + "newKnowledge.taskFailedNotification": "Przetwarzanie dokumentu nie powiodło się — spróbuj ponownie w Zadaniach", + "newKnowledge.taskHistoryIncomplete": "Dostępna jest dalsza historia zadań; liczba zadań wymagających uwagi jest niepełna.", + "newKnowledge.tasks": "Zadania", + "newKnowledge.tasksErrorDescription": "Nie udało się wczytać zadań w tle. Spróbuj ponownie.", + "newKnowledge.tasksWithAttention": "Zadania, {{count}} wymagają uwagi", "newKnowledge.unavailableDescription": "KnowledgeFS nie jest obecnie dostępny dla tego obszaru roboczego.", "newKnowledge.unavailableTitle": "Nowa wiedza jest niedostępna", "newKnowledge.updated": "Zaktualizowano {{date}}", + "newKnowledge.updatedColumn": "Zaktualizowano", "newKnowledge.uploadCharactersUnavailable": "Znaki: —", + "newKnowledge.uploadDocuments": "Prześlij dokumenty", "newKnowledge.uploadFiles": "Prześlij pliki", "newKnowledge.uploadFilesDescription": "Dodaj pliki z komputera — zostaną zindeksowane od razu po utworzeniu.", "newKnowledge.uploadingFiles": "Przesyłanie…", diff --git a/web/i18n/pt-BR/dataset.json b/web/i18n/pt-BR/dataset.json index 2dc97abbd11..3cd3f65a6d9 100644 --- a/web/i18n/pt-BR/dataset.json +++ b/web/i18n/pt-BR/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "O modelo de Reclassificação é necessário para a mistura de conhecimento interno e externo.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Ações", + "newKnowledge.addDocument": "Adicionar documento", "newKnowledge.addSource": "Adicionar fonte", "newKnowledge.addSourceDescription": "Conecte uma fonte a esta base de conhecimento — ela sincroniza automaticamente e permanece atualizada.", "newKnowledge.addSourceFailed": "Não foi possível adicionar esta fonte. Revise as configurações e tente novamente.", "newKnowledge.addSourceRequiresSelection": "Selecione pelo menos uma página rastreada antes de adicionar a fonte.", "newKnowledge.adjustAndRecrawl": "Ajustar e rastrear novamente", + "newKnowledge.allDocumentStatuses": "Todos os status", "newKnowledge.allSources": "Todas as fontes", "newKnowledge.appsUnavailable": "— apps", "newKnowledge.authKind.api-key": "Chave de API", "newKnowledge.authKind.endpoint": "Ponto final", "newKnowledge.authenticationMethod": "Método de autenticação", "newKnowledge.backToList": "Voltar às bases de conhecimento", + "newKnowledge.backgroundTasks": "Tarefas em segundo plano", + "newKnowledge.backgroundTasksDescription": "Adições de documentos, sincronizações de origem e reindexação de trabalhos.", "newKnowledge.booleanFalse": "Não", "newKnowledge.booleanTrue": "Sim", + "newKnowledge.bulkDocumentActions": "Ações de documentos em massa", "newKnowledge.cancelAddSource": "Cancelar", "newKnowledge.cardType": "Espaço de conhecimento", + "newKnowledge.clearDocumentSelection": "Limpar seleção de documentos", "newKnowledge.configureProvider": "Configurar {{provider}}", "newKnowledge.connectNotion": "Conectar o Notion", "newKnowledge.connectProvider": "Conectar {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Criadores", "newKnowledge.customIntervalHours": "Intervalo personalizado (horas)", "newKnowledge.customIntervalInvalid": "Digite um número inteiro de 1 a 720 horas.", + "newKnowledge.deleteDocuments": "Excluir…", "newKnowledge.description": "Descrição", "newKnowledge.descriptionHelp": "Uma descrição clara ajuda os aplicativos a decidir quando pesquisar esse conhecimento.", "newKnowledge.descriptionPlaceholder": "por exemplo Política de reembolso, perguntas frequentes sobre envio e etapas de escalonamento — use para perguntas de suporte ao cliente", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "A configuração da fonte e a visualização do rastreamento serão removidas.", "newKnowledge.discardSourceDraftFailed": "Não foi possível descartar o rascunho da fonte. Tente novamente.", "newKnowledge.discardSourceDraftTitle": "Descartar este rascunho de fonte?", + "newKnowledge.documentActions": "Ações para {{name}}", + "newKnowledge.documentActionsUnavailable": "As ações do documento ainda não estão disponíveis para esta base de conhecimento.", + "newKnowledge.documentColumn": "Documento", + "newKnowledge.documentFilterLabel": "Filtrar documentos por status", + "newKnowledge.documentPermissionRestricted": "As permissões do seu workspace não permitem ações em documentos ou tarefas.", + "newKnowledge.documentStatus.disabled": "Desativado", + "newKnowledge.documentStatus.failed": "Falha", + "newKnowledge.documentStatus.processing": "Processamento", + "newKnowledge.documentStatus.queued": "Na fila", + "newKnowledge.documentStatus.ready": "Pronto", + "newKnowledge.documentUploadExclusion.batchLimit": "limite de tamanho do lote excedido", + "newKnowledge.documentUploadExclusion.countLimit": "limite de quantidade de arquivos excedido", + "newKnowledge.documentUploadExclusion.fileSize": "o arquivo é muito grande", + "newKnowledge.documentUploadExclusion.fileType": "tipo de arquivo não suportado ou inválido", + "newKnowledge.documentUploadExclusion.more": "e mais {{count}}", + "newKnowledge.documentUploadExclusion.processing": "falha no processamento", + "newKnowledge.documentUploadExclusion.quota": "cota do espaço de trabalho excedida", + "newKnowledge.documentUploadExclusion.target": "o destino do documento não é mais válido", + "newKnowledge.documentUploadFailed": "Não foi possível fazer upload desses documentos. Tente novamente.", + "newKnowledge.documentUploadPartial": "O processamento de {{accepted}} documentos foi iniciado; não foi possível adicionar {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "Nenhum documento foi aceito: {{details}}", + "newKnowledge.documentUploadStarted": "O processamento do documento foi iniciado.", "newKnowledge.documents": "Documentos", + "newKnowledge.documentsDescription": "Ativos de conhecimento e suas revisões — de onde veio cada documento e se a revisão mais recente está indexada.", + "newKnowledge.documentsDropHint": "ou solte PDF, DOCX, Markdown, HTML, XLSX ou TXT para fazer upload", + "newKnowledge.documentsEmptyDescription": "Faça upload de arquivos aqui ou conecte uma fonte na guia Fontes — os documentos são analisados, agrupados e indexados automaticamente.", + "newKnowledge.documentsEmptyTitle": "Ainda não há documentos", + "newKnowledge.documentsErrorDescription": "Não foi possível carregar os documentos. Tente novamente.", + "newKnowledge.documentsErrorTitle": "Não foi possível carregar os documentos", + "newKnowledge.documentsPermissionDescription": "Você não tem permissão para visualizar documentos nesta base de conhecimento.", + "newKnowledge.documentsPermissionTitle": "Documentos são restritos", + "newKnowledge.documentsReindexFailed": "Não foi possível iniciar a reindexação. Tente novamente.", + "newKnowledge.documentsReindexPartial": "Documentos {{queued}} enfileirados; {{missing}} não existe mais.", + "newKnowledge.documentsReindexStarted": "A reindexação foi iniciada.", + "newKnowledge.documentsSelected": "Documentos {{count}} selecionados", "newKnowledge.documentsUnavailable": "— documentos", + "newKnowledge.downloadDocuments": "Baixar", "newKnowledge.editSource": "Editar", "newKnowledge.emptyDescription": "Adicione seus documentos para que os agentes possam lê-los diretamente, sem configurar a indexação.", "newKnowledge.emptyTitle": "Uma forma mais inteligente de criar bases de conhecimento", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Conheça a nova Base de Conhecimento ✨", "newKnowledge.illustrationHeadline": "Conecte seu conteúdo uma vez — cada aplicativo responderá com o conhecimento mais recente.", "newKnowledge.includeSubpages": "Incluir subpáginas", + "newKnowledge.interruptTask": "Interromper", "newKnowledge.invalidRootUrl": "Insira uma URL http(s) válida.", "newKnowledge.keepEditing": "Continuar editando", + "newKnowledge.lastReadyRevisionHint": "O processamento com falha nunca interrompe a recuperação — a última revisão pronta permanece disponível no índice de conteúdo.", "newKnowledge.lastSyncColumn": "Última sincronização", "newKnowledge.learnMore": "Saiba mais", "newKnowledge.leavePartialSetupConfirm": "Sair da configuração", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Legado", "newKnowledge.loadMore": "Carregar mais", "newKnowledge.loadingSyncPolicy": "Carregando política de sincronização", + "newKnowledge.manualUpload": "Carregamento manual", "newKnowledge.maxPages": "Máximo de páginas", + "newKnowledge.metadata": "Metadados", "newKnowledge.moreProviders": "Mais provedores", "newKnowledge.name": "Nome do conhecimento", "newKnowledge.namePlaceholder": "por exemplo Manual de Suporte ao Cliente", "newKnowledge.nameRequired": "Insira um nome para a base de conhecimento.", "newKnowledge.new": "Novo", + "newKnowledge.noBackgroundTasks": "Nenhuma tarefa em segundo plano ainda.", "newKnowledge.noDescription": "Sem descrição", + "newKnowledge.noMatchingDocuments": "Nenhum documento corresponde a esta pesquisa.", "newKnowledge.noMatchingSources": "Nenhuma fonte corresponde a esta pesquisa.", "newKnowledge.noPagesFound": "Nenhuma página encontrada em {{host}}", "newKnowledge.noPagesFoundDescription": "O rastreador acessou o site, mas não encontrou páginas indexáveis. Tente outra URL ou amplie as opções de rastreamento.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} páginas rastreadas em {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} páginas rastreadas em {{host}}", "newKnowledge.pagesSelected": "{{count}} selecionado(s)", + "newKnowledge.partialDocumentResults": "Pode haver mais documentos correspondentes. Carregue mais para continuar a pesquisa.", "newKnowledge.permission": "Permissão", "newKnowledge.permissionAllMembers": "Espaço de trabalho · todos os membros podem visualizar e editar", + "newKnowledge.permissionLoadFailed": "Não foi possível verificar suas permissões de edição. Tente novamente.", "newKnowledge.permissionOnlyMe": "Só eu", "newKnowledge.permissionRestricted": "As permissões do seu espaço de trabalho não permitem alterar essa configuração.", "newKnowledge.permissionUpdateFailed": "A base de conhecimento foi criada, mas não foi possível confirmar sua permissão. Tente novamente para concluir a configuração.", "newKnowledge.preview": "Prévia", "newKnowledge.previewUnavailable": "A prévia estará disponível quando o backend terminar de processar este arquivo.", + "newKnowledge.processDocument": "Processo {{name}}", + "newKnowledge.processingTaskState.canceled": "Interrompido", + "newKnowledge.processingTaskState.dispatch_pending": "Preparando", + "newKnowledge.processingTaskState.failed": "Falha", + "newKnowledge.processingTaskState.queued": "Na fila", + "newKnowledge.processingTaskState.retry_wait": "Esperando para tentar novamente", + "newKnowledge.processingTaskState.running": "Processando · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Concluído", + "newKnowledge.processingTaskState.superseded": "Substituído", "newKnowledge.providerConnected": "{{provider}} conectado", "newKnowledge.providerLabel": "Selecione um provedor", "newKnowledge.providerLoadFailed": "Os provedores não conseguiram carregar", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Peça a um editor do espaço de trabalho para criar a primeira base de conhecimento.", "newKnowledge.refreshConnectionStatus": "Atualizar status", "newKnowledge.refreshingConnection": "Refrescante…", + "newKnowledge.reindexDocuments": "Reindexar", "newKnowledge.removeSource": "Remover fonte", "newKnowledge.retryCrawl": "Tentar rastreamento novamente", "newKnowledge.retryProviderLoad": "Tentar novamente", "newKnowledge.retrySyncPolicy": "Tentar novamente a política de sincronização", + "newKnowledge.retryTask": "Tentar novamente", "newKnowledge.rootUrl": "URL raiz", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Esta seção estará disponível na próxima etapa de configuração.", + "newKnowledge.searchDocuments": "Pesquisar documentos", "newKnowledge.searchSources": "Fontes de pesquisa", "newKnowledge.selectAll": "Selecionar tudo", + "newKnowledge.selectAllDocuments": "Selecione todos os documentos", "newKnowledge.selectAllSources": "Selecionar todas as origens", "newKnowledge.selectFilesAndFolders": "Selecione arquivos e pastas para sincronizar", "newKnowledge.skippedFailed": "Ignorado (falha)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Somente manual", "newKnowledge.syncPolicyProvider": "Usar agenda do provedor", "newKnowledge.tags": "Tags", + "newKnowledge.taskActionFailed": "Não foi possível atualizar esta tarefa. Atualize e tente novamente.", + "newKnowledge.taskAttentionClear": "Nenhuma tarefa em segundo plano precisa de atenção.", + "newKnowledge.taskAttentionCount": "As tarefas em segundo plano do {{count}} precisam de atenção.", + "newKnowledge.taskAttentionErrorCount": "As tarefas em segundo plano {{count}} precisam de atenção, incluindo tarefas com falha.", + "newKnowledge.taskFailedNotification": "Falha no processamento do documento — tente novamente em Tarefas", + "newKnowledge.taskHistoryIncomplete": "Há mais histórico de tarefas disponível; a contagem de tarefas que precisam de atenção está incompleta.", + "newKnowledge.tasks": "Tarefas", + "newKnowledge.tasksErrorDescription": "Não foi possível carregar tarefas em segundo plano. Tente novamente.", + "newKnowledge.tasksWithAttention": "Tarefas, {{count}} precisam de atenção", "newKnowledge.unavailableDescription": "O KnowledgeFS não está disponível para este espaço de trabalho no momento.", "newKnowledge.unavailableTitle": "O Novo Conhecimento não está disponível", "newKnowledge.updated": "Atualizado em {{date}}", + "newKnowledge.updatedColumn": "Atualizado", "newKnowledge.uploadCharactersUnavailable": "Caracteres: —", + "newKnowledge.uploadDocuments": "Carregar documentos", "newKnowledge.uploadFiles": "Enviar arquivos", "newKnowledge.uploadFilesDescription": "Adicione arquivos do seu computador; eles são indexados assim que você os cria.", "newKnowledge.uploadingFiles": "Enviando…", diff --git a/web/i18n/ro-RO/dataset.json b/web/i18n/ro-RO/dataset.json index e7a915ca3c2..7647dd195c2 100644 --- a/web/i18n/ro-RO/dataset.json +++ b/web/i18n/ro-RO/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Modelul Rerank este necesar pentru amestecul de cunoștințe interne și externe.", "multimodal": "Multimodal", "newKnowledge.actionsColumn": "Acțiuni", + "newKnowledge.addDocument": "Adăugați document", "newKnowledge.addSource": "Adăugați sursa", "newKnowledge.addSourceDescription": "Conectați o sursă la această bază de cunoștințe — se sincronizează automat și rămâne actuală.", "newKnowledge.addSourceFailed": "Această sursă nu a putut fi adăugată. Verifică setările și încearcă din nou.", "newKnowledge.addSourceRequiresSelection": "Selectează cel puțin o pagină explorată înainte de a adăuga sursa.", "newKnowledge.adjustAndRecrawl": "Ajustează și explorează din nou", + "newKnowledge.allDocumentStatuses": "Toate statutul", "newKnowledge.allSources": "Toate sursele", "newKnowledge.appsUnavailable": "— aplicații", "newKnowledge.authKind.api-key": "cheie API", "newKnowledge.authKind.endpoint": "Punct final", "newKnowledge.authenticationMethod": "Metoda de autentificare", "newKnowledge.backToList": "Înapoi la bazele de cunoștințe", + "newKnowledge.backgroundTasks": "Sarcini de fundal", + "newKnowledge.backgroundTasksDescription": "Adăugări de documente, sincronizare sursă și reindexare lucrări.", "newKnowledge.booleanFalse": "Nu", "newKnowledge.booleanTrue": "Da", + "newKnowledge.bulkDocumentActions": "Acțiuni în bloc pentru documente", "newKnowledge.cancelAddSource": "Anula", "newKnowledge.cardType": "Spațiu de cunoștințe", + "newKnowledge.clearDocumentSelection": "Ștergeți selecția documentului", "newKnowledge.configureProvider": "Configurați {{provider}}", "newKnowledge.connectNotion": "Conectează Notion", "newKnowledge.connectProvider": "Conectați {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Creatori", "newKnowledge.customIntervalHours": "Interval personalizat (ore)", "newKnowledge.customIntervalInvalid": "Introdu un număr întreg între 1 și 720 de ore.", + "newKnowledge.deleteDocuments": "Șterge…", "newKnowledge.description": "Descriere", "newKnowledge.descriptionHelp": "O descriere clară ajută aplicațiile să decidă când să caute aceste cunoștințe.", "newKnowledge.descriptionPlaceholder": "de ex. Politica de rambursare, întrebări frecvente despre expediere și pași de escaladare — utilizați pentru întrebările de asistență pentru clienți", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Configurația sursei și previzualizarea explorării vor fi eliminate.", "newKnowledge.discardSourceDraftFailed": "Schița sursei nu a putut fi eliminată. Încearcă din nou.", "newKnowledge.discardSourceDraftTitle": "Elimini această schiță de sursă?", + "newKnowledge.documentActions": "Acțiuni pentru {{name}}", + "newKnowledge.documentActionsUnavailable": "Acțiunile documentului nu sunt încă disponibile pentru această bază de cunoștințe.", + "newKnowledge.documentColumn": "Fișier", + "newKnowledge.documentFilterLabel": "Filtrați documentele după stare", + "newKnowledge.documentPermissionRestricted": "Permisiunile spațiului de lucru nu permit acțiuni asupra documentelor sau sarcinilor.", + "newKnowledge.documentStatus.disabled": "Dezactivat", + "newKnowledge.documentStatus.failed": "A eșuat", + "newKnowledge.documentStatus.processing": "Prelucrare", + "newKnowledge.documentStatus.queued": "În coadă", + "newKnowledge.documentStatus.ready": "Gata", + "newKnowledge.documentUploadExclusion.batchLimit": "limita dimensiunii lotului a fost depășită", + "newKnowledge.documentUploadExclusion.countLimit": "limita numărului de fișiere a fost depășită", + "newKnowledge.documentUploadExclusion.fileSize": "fișierul este prea mare", + "newKnowledge.documentUploadExclusion.fileType": "tip de fișier neacceptat sau nevalid", + "newKnowledge.documentUploadExclusion.more": "și încă {{count}}", + "newKnowledge.documentUploadExclusion.processing": "procesarea a eșuat", + "newKnowledge.documentUploadExclusion.quota": "cota spațiului de lucru a fost depășită", + "newKnowledge.documentUploadExclusion.target": "destinația documentului nu mai este validă", + "newKnowledge.documentUploadFailed": "Nu am putut încărca aceste documente. Încearcă din nou.", + "newKnowledge.documentUploadPartial": "Procesarea a {{accepted}} documente a început; {{excluded}} nu au putut fi adăugate: {{details}}", + "newKnowledge.documentUploadRejected": "Nu a fost acceptat niciun document: {{details}}", + "newKnowledge.documentUploadStarted": "Procesarea documentelor a început.", "newKnowledge.documents": "Documente", + "newKnowledge.documentsDescription": "Activele de cunoștințe și revizuirile acestora — de unde provine fiecare document și dacă cea mai recentă revizuire este indexată.", + "newKnowledge.documentsDropHint": "sau plasați PDF, DOCX, Markdown, HTML, XLSX sau TXT pentru a încărca", + "newKnowledge.documentsEmptyDescription": "Încărcați fișiere aici sau conectați o sursă în fila Surse - documentele sunt analizate, fragmentate și indexate automat.", + "newKnowledge.documentsEmptyTitle": "Încă nu există documente", + "newKnowledge.documentsErrorDescription": "Nu am putut încărca documente. Încearcă din nou.", + "newKnowledge.documentsErrorTitle": "Documentele nu s-au putut încărca", + "newKnowledge.documentsPermissionDescription": "Nu aveți permisiunea de a vizualiza documente din această bază de cunoștințe.", + "newKnowledge.documentsPermissionTitle": "Documentele sunt restricționate", + "newKnowledge.documentsReindexFailed": "Nu am putut începe reindexarea. Încearcă din nou.", + "newKnowledge.documentsReindexPartial": "{{queued}} documente puse în coadă; {{missing}} nu mai există.", + "newKnowledge.documentsReindexStarted": "A început reindexarea.", + "newKnowledge.documentsSelected": "{{count}} documente selectate", "newKnowledge.documentsUnavailable": "— documente", + "newKnowledge.downloadDocuments": "Descărcați", "newKnowledge.editSource": "Edita", "newKnowledge.emptyDescription": "Adaugă documentele, iar agenții le pot citi direct — fără indexare de configurat.", "newKnowledge.emptyTitle": "O modalitate mai inteligentă de a crea baze de cunoștințe", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Descoperă noua bază de cunoștințe ✨", "newKnowledge.illustrationHeadline": "Conectează conținutul o singură dată — fiecare aplicație răspunde cu cele mai noi informații.", "newKnowledge.includeSubpages": "Include subpaginile", + "newKnowledge.interruptTask": "Întrerupe", "newKnowledge.invalidRootUrl": "Introdu o adresă URL http(s) validă.", "newKnowledge.keepEditing": "Continuă editarea", + "newKnowledge.lastReadyRevisionHint": "Procesarea eșuată nu întrerupe niciodată recuperarea — ultima revizuire gata rămâne disponibilă în indexul de conținut.", "newKnowledge.lastSyncColumn": "Ultima sincronizare", "newKnowledge.learnMore": "Află mai multe", "newKnowledge.leavePartialSetupConfirm": "Ieși din configurare", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Versiune veche", "newKnowledge.loadMore": "Încarcă mai multe", "newKnowledge.loadingSyncPolicy": "Se încarcă politica de sincronizare", + "newKnowledge.manualUpload": "Încărcare manuală", "newKnowledge.maxPages": "Număr maxim de pagini", + "newKnowledge.metadata": "Metadate", "newKnowledge.moreProviders": "Mai mulți furnizori", "newKnowledge.name": "Nume de cunoștințe", "newKnowledge.namePlaceholder": "de ex. Manual de asistență pentru clienți", "newKnowledge.nameRequired": "Introduceți un nume de bază de cunoștințe.", "newKnowledge.new": "Versiune nouă", + "newKnowledge.noBackgroundTasks": "Nu există încă sarcini de fundal.", "newKnowledge.noDescription": "Fără descriere", + "newKnowledge.noMatchingDocuments": "Niciun document nu corespunde acestei căutări.", "newKnowledge.noMatchingSources": "Nicio sursă nu corespunde acestei căutări.", "newKnowledge.noPagesFound": "Nu s-au găsit pagini la {{host}}", "newKnowledge.noPagesFoundDescription": "Crawlerul a ajuns la site, dar nu a găsit pagini care pot fi indexate. Încearcă altă adresă URL sau extinde opțiunile de explorare.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{count}} pagini explorate la {{host}}", "newKnowledge.pagesCrawled_other": "{{count}} pagini explorate la {{host}}", "newKnowledge.pagesSelected": "{{count}} selectate", + "newKnowledge.partialDocumentResults": "Este posibil să existe mai multe documente potrivite. Încarcă mai multe pentru a continua căutarea.", "newKnowledge.permission": "Permisiune", "newKnowledge.permissionAllMembers": "Spațiu de lucru · toți membrii pot vizualiza și edita", + "newKnowledge.permissionLoadFailed": "Permisiunile de editare nu au putut fi verificate. Încercați din nou.", "newKnowledge.permissionOnlyMe": "Doar eu", "newKnowledge.permissionRestricted": "Permisiunile pentru spațiul de lucru nu permit modificarea acestei setări.", "newKnowledge.permissionUpdateFailed": "Baza de cunoștințe a fost creată, dar permisiunea nu a putut fi confirmată. Încercați din nou pentru a finaliza configurarea.", "newKnowledge.preview": "Previzualizare", "newKnowledge.previewUnavailable": "Previzualizarea va fi disponibilă după ce backendul finalizează procesarea fișierului.", + "newKnowledge.processDocument": "Procesează {{name}}", + "newKnowledge.processingTaskState.canceled": "Întrerupt", + "newKnowledge.processingTaskState.dispatch_pending": "Pregătirea", + "newKnowledge.processingTaskState.failed": "A eșuat", + "newKnowledge.processingTaskState.queued": "În coadă", + "newKnowledge.processingTaskState.retry_wait": "Aștept să încerci din nou", + "newKnowledge.processingTaskState.running": "Procesare · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Terminat", + "newKnowledge.processingTaskState.superseded": "Inlocuit", "newKnowledge.providerConnected": "{{provider}} conectat", "newKnowledge.providerLabel": "Selectați un furnizor", "newKnowledge.providerLoadFailed": "Furnizorii nu s-au putut încărca", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Cere unui editor al spațiului de lucru să creeze prima bază de cunoștințe.", "newKnowledge.refreshConnectionStatus": "Starea de reîmprospătare", "newKnowledge.refreshingConnection": "Înviorare…", + "newKnowledge.reindexDocuments": "Reindexați", "newKnowledge.removeSource": "Eliminați sursa", "newKnowledge.retryCrawl": "Reîncearcă explorarea", "newKnowledge.retryProviderLoad": "Reîncercați", "newKnowledge.retrySyncPolicy": "Reîncearcă politica de sincronizare", + "newKnowledge.retryTask": "Reîncercați", "newKnowledge.rootUrl": "URL rădăcină", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Această secțiune va deveni disponibilă în următorul pas de configurare.", + "newKnowledge.searchDocuments": "Căutați documente", "newKnowledge.searchSources": "Căutați surse", "newKnowledge.selectAll": "Selectează tot", + "newKnowledge.selectAllDocuments": "Selectați toate documentele", "newKnowledge.selectAllSources": "Selectează toate sursele", "newKnowledge.selectFilesAndFolders": "Selectează fișierele și folderele de sincronizat", "newKnowledge.skippedFailed": "Omis (eșuat)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Doar manual", "newKnowledge.syncPolicyProvider": "Folosește programul furnizorului", "newKnowledge.tags": "Etichete", + "newKnowledge.taskActionFailed": "Nu am putut actualiza această sarcină. Actualizați și încercați din nou.", + "newKnowledge.taskAttentionClear": "Nicio activitate de fundal nu necesită atenție.", + "newKnowledge.taskAttentionCount": "{{count}} sarcinile de fundal necesită atenție.", + "newKnowledge.taskAttentionErrorCount": "{{count}} sarcinile de fundal necesită atenție, inclusiv sarcinile eșuate.", + "newKnowledge.taskFailedNotification": "Procesarea documentului a eșuat — reîncercați din Sarcini", + "newKnowledge.taskHistoryIncomplete": "Este disponibil mai mult istoric al sarcinilor; numărul sarcinilor care necesită atenție este incomplet.", + "newKnowledge.tasks": "Sarcini", + "newKnowledge.tasksErrorDescription": "Nu am putut încărca sarcinile de fundal. Încearcă din nou.", + "newKnowledge.tasksWithAttention": "Sarcini, {{count}} necesită atenție", "newKnowledge.unavailableDescription": "KnowledgeFS nu este disponibil momentan pentru acest spațiu de lucru.", "newKnowledge.unavailableTitle": "Noua bază de cunoștințe nu este disponibilă", "newKnowledge.updated": "Actualizat la {{date}}", + "newKnowledge.updatedColumn": "Actualizat", "newKnowledge.uploadCharactersUnavailable": "Caractere: —", + "newKnowledge.uploadDocuments": "Încărcați documente", "newKnowledge.uploadFiles": "Încarcă fișiere", "newKnowledge.uploadFilesDescription": "Adaugă fișiere de pe computer — sunt indexate imediat ce le creezi.", "newKnowledge.uploadingFiles": "Se încarcă…", diff --git a/web/i18n/ru-RU/dataset.json b/web/i18n/ru-RU/dataset.json index 14e0edf85fc..682c7882dfd 100644 --- a/web/i18n/ru-RU/dataset.json +++ b/web/i18n/ru-RU/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Модель Rerank необходима для смешивания внутренних и внешних знаний.", "multimodal": "Мультимодальный", "newKnowledge.actionsColumn": "Действия", + "newKnowledge.addDocument": "Добавить документ", "newKnowledge.addSource": "Добавить источник", "newKnowledge.addSourceDescription": "Подключите источник к этой базе знаний — она синхронизируется автоматически и остается актуальной.", "newKnowledge.addSourceFailed": "Не удалось добавить этот источник. Проверьте настройки и повторите попытку.", "newKnowledge.addSourceRequiresSelection": "Перед добавлением источника выберите хотя бы одну просканированную страницу.", "newKnowledge.adjustAndRecrawl": "Настроить и просканировать снова", + "newKnowledge.allDocumentStatuses": "Все статусы", "newKnowledge.allSources": "Все источники", "newKnowledge.appsUnavailable": "— приложений", "newKnowledge.authKind.api-key": "API-ключ", "newKnowledge.authKind.endpoint": "Конечная точка", "newKnowledge.authenticationMethod": "Метод аутентификации", "newKnowledge.backToList": "Вернуться к базам знаний", + "newKnowledge.backgroundTasks": "Фоновые задачи", + "newKnowledge.backgroundTasksDescription": "Добавление документов, синхронизация источников и переиндексация заданий.", "newKnowledge.booleanFalse": "Нет", "newKnowledge.booleanTrue": "Да", + "newKnowledge.bulkDocumentActions": "Массовые действия с документами", "newKnowledge.cancelAddSource": "Отмена", "newKnowledge.cardType": "Пространство знаний", + "newKnowledge.clearDocumentSelection": "Очистить выбор документа", "newKnowledge.configureProvider": "Настроить {{provider}}", "newKnowledge.connectNotion": "Подключить Notion", "newKnowledge.connectProvider": "Подключите {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Создатели", "newKnowledge.customIntervalHours": "Пользовательский интервал (часы)", "newKnowledge.customIntervalInvalid": "Введите целое число от 1 до 720 часов.", + "newKnowledge.deleteDocuments": "Удалить…", "newKnowledge.description": "Описание", "newKnowledge.descriptionHelp": "Четкое описание помогает приложениям решить, когда следует искать эти знания.", "newKnowledge.descriptionPlaceholder": "например Политика возврата, часто задаваемые вопросы о доставке и шаги эскалации — используйте для вопросов поддержки клиентов.", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Конфигурация источника и предварительное сканирование будут удалены.", "newKnowledge.discardSourceDraftFailed": "Не удалось удалить черновик источника. Повторите попытку.", "newKnowledge.discardSourceDraftTitle": "Удалить этот черновик источника?", + "newKnowledge.documentActions": "Действия для {{name}}", + "newKnowledge.documentActionsUnavailable": "Действия с документами пока недоступны для этой базы знаний.", + "newKnowledge.documentColumn": "Документ", + "newKnowledge.documentFilterLabel": "Фильтровать документы по статусу", + "newKnowledge.documentPermissionRestricted": "Разрешения вашего рабочего пространства не позволяют выполнять действия с документами или задачами.", + "newKnowledge.documentStatus.disabled": "Отключено", + "newKnowledge.documentStatus.failed": "Не удалось", + "newKnowledge.documentStatus.processing": "Обработка", + "newKnowledge.documentStatus.queued": "В очереди", + "newKnowledge.documentStatus.ready": "Готово", + "newKnowledge.documentUploadExclusion.batchLimit": "превышен предел размера пакета", + "newKnowledge.documentUploadExclusion.countLimit": "превышен предел количества файлов", + "newKnowledge.documentUploadExclusion.fileSize": "файл слишком большой", + "newKnowledge.documentUploadExclusion.fileType": "неподдерживаемый или недопустимый тип файла", + "newKnowledge.documentUploadExclusion.more": "и ещё {{count}}", + "newKnowledge.documentUploadExclusion.processing": "ошибка обработки", + "newKnowledge.documentUploadExclusion.quota": "превышена квота рабочей области", + "newKnowledge.documentUploadExclusion.target": "назначение документа больше недействительно", + "newKnowledge.documentUploadFailed": "Нам не удалось загрузить эти документы. Попробуйте еще раз.", + "newKnowledge.documentUploadPartial": "Начата обработка {{accepted}} документов; не удалось добавить {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "Ни один документ не принят: {{details}}", + "newKnowledge.documentUploadStarted": "Началась обработка документов.", "newKnowledge.documents": "Документы", + "newKnowledge.documentsDescription": "Активы знаний и их версии — откуда взялся каждый документ и индексируется ли последняя версия.", + "newKnowledge.documentsDropHint": "или перетащите PDF, DOCX, Markdown, HTML, XLSX или TXT для загрузки.", + "newKnowledge.documentsEmptyDescription": "Загрузите файлы сюда или подключите источник на вкладке «Источники» — документы анализируются, разбиваются на части и индексируются автоматически.", + "newKnowledge.documentsEmptyTitle": "Документов пока нет", + "newKnowledge.documentsErrorDescription": "Не удалось загрузить документы. Попробуйте еще раз.", + "newKnowledge.documentsErrorTitle": "Не удалось загрузить документы", + "newKnowledge.documentsPermissionDescription": "У вас нет разрешения на просмотр документов в этой базе знаний.", + "newKnowledge.documentsPermissionTitle": "Документы ограничены", + "newKnowledge.documentsReindexFailed": "Не удалось начать переиндексацию. Попробуйте еще раз.", + "newKnowledge.documentsReindexPartial": "Документы {{queued}} поставлены в очередь; {{missing}} больше не существует.", + "newKnowledge.documentsReindexStarted": "Началась переиндексация.", + "newKnowledge.documentsSelected": "{{count}} выбраны документы", "newKnowledge.documentsUnavailable": "— документов", + "newKnowledge.downloadDocuments": "Скачать", "newKnowledge.editSource": "Править", "newKnowledge.emptyDescription": "Добавьте документы, и агенты смогут читать их напрямую — без настройки индексации.", "newKnowledge.emptyTitle": "Более умный способ создавать базы знаний", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Познакомьтесь с новой базой знаний ✨", "newKnowledge.illustrationHeadline": "Подключите контент один раз — и каждое приложение будет отвечать на основе самых свежих знаний.", "newKnowledge.includeSubpages": "Включать вложенные страницы", + "newKnowledge.interruptTask": "Прерывание", "newKnowledge.invalidRootUrl": "Введите корректный URL с http(s).", "newKnowledge.keepEditing": "Продолжить редактирование", + "newKnowledge.lastReadyRevisionHint": "Неудачная обработка никогда не прерывает извлечение — последняя готовая редакция остается доступной в индексе контента.", "newKnowledge.lastSyncColumn": "Последняя синхронизация", "newKnowledge.learnMore": "Подробнее", "newKnowledge.leavePartialSetupConfirm": "Выйти из настройки", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Предыдущая версия", "newKnowledge.loadMore": "Загрузить ещё", "newKnowledge.loadingSyncPolicy": "Загрузка политики синхронизации", + "newKnowledge.manualUpload": "Загрузка вручную", "newKnowledge.maxPages": "Максимум страниц", + "newKnowledge.metadata": "Метаданные", "newKnowledge.moreProviders": "Другие провайдеры", "newKnowledge.name": "Название знания", "newKnowledge.namePlaceholder": "например Руководство по поддержке клиентов", "newKnowledge.nameRequired": "Введите название базы знаний.", "newKnowledge.new": "Новая версия", + "newKnowledge.noBackgroundTasks": "Фоновых задач пока нет.", "newKnowledge.noDescription": "Нет описания", + "newKnowledge.noMatchingDocuments": "Нет документов, соответствующих этому запросу.", "newKnowledge.noMatchingSources": "Ни один источник не соответствует этому запросу.", "newKnowledge.noPagesFound": "На {{host}} не найдено страниц", "newKnowledge.noPagesFoundDescription": "Робот получил доступ к сайту, но не нашёл страниц для индексирования. Попробуйте другой URL или расширьте параметры сканирования.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "Просканировано страниц на {{host}}: {{count}}", "newKnowledge.pagesCrawled_other": "Просканировано страниц на {{host}}: {{count}}", "newKnowledge.pagesSelected": "Выбрано: {{count}}", + "newKnowledge.partialDocumentResults": "Подходящих документов может быть больше. Загрузите ещё, чтобы продолжить поиск.", "newKnowledge.permission": "Разрешение", "newKnowledge.permissionAllMembers": "Рабочая область · все участники могут просматривать и редактировать", + "newKnowledge.permissionLoadFailed": "Не удалось проверить права на редактирование. Повторите попытку.", "newKnowledge.permissionOnlyMe": "Только я", "newKnowledge.permissionRestricted": "Разрешения вашей рабочей области не позволяют изменить этот параметр.", "newKnowledge.permissionUpdateFailed": "База знаний создана, но подтвердить её разрешения не удалось. Повторите попытку, чтобы завершить настройку.", "newKnowledge.preview": "Предпросмотр", "newKnowledge.previewUnavailable": "Предпросмотр станет доступен после завершения обработки файла сервером.", + "newKnowledge.processDocument": "Процесс {{name}}", + "newKnowledge.processingTaskState.canceled": "Прерванный", + "newKnowledge.processingTaskState.dispatch_pending": "Подготовка", + "newKnowledge.processingTaskState.failed": "Не удалось", + "newKnowledge.processingTaskState.queued": "В очереди", + "newKnowledge.processingTaskState.retry_wait": "Ожидание повторной попытки", + "newKnowledge.processingTaskState.running": "Обработка · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Завершено", + "newKnowledge.processingTaskState.superseded": "Заменено", "newKnowledge.providerConnected": "{{provider}} подключен", "newKnowledge.providerLabel": "Выберите провайдера", "newKnowledge.providerLoadFailed": "Поставщикам не удалось загрузить", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Попросите редактора рабочего пространства создать первую базу знаний.", "newKnowledge.refreshConnectionStatus": "Обновить статус", "newKnowledge.refreshingConnection": "Обновление…", + "newKnowledge.reindexDocuments": "Переиндексация", "newKnowledge.removeSource": "Удалить источник", "newKnowledge.retryCrawl": "Повторить сканирование", "newKnowledge.retryProviderLoad": "Повторить попытку", "newKnowledge.retrySyncPolicy": "Повторить загрузку политики синхронизации", + "newKnowledge.retryTask": "Повторить попытку", "newKnowledge.rootUrl": "Корневой URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Этот раздел станет доступен на следующем этапе настройки.", + "newKnowledge.searchDocuments": "Поиск документов", "newKnowledge.searchSources": "Поиск источников", "newKnowledge.selectAll": "Выбрать все", + "newKnowledge.selectAllDocuments": "Выбрать все документы", "newKnowledge.selectAllSources": "Выбрать все источники", "newKnowledge.selectFilesAndFolders": "Выберите файлы и папки для синхронизации", "newKnowledge.skippedFailed": "Пропущено (ошибка)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Только вручную", "newKnowledge.syncPolicyProvider": "Использовать расписание поставщика", "newKnowledge.tags": "Теги", + "newKnowledge.taskActionFailed": "Не удалось обновить эту задачу. Обновите и повторите попытку.", + "newKnowledge.taskAttentionClear": "Никакие фоновые задачи не требуют внимания.", + "newKnowledge.taskAttentionCount": "Фоновые задачи {{count}} требуют внимания.", + "newKnowledge.taskAttentionErrorCount": "{{count}} фоновые задачи требуют внимания, включая невыполненные задачи.", + "newKnowledge.taskFailedNotification": "Не удалось обработать документ — повторите попытку в разделе «Задачи».", + "newKnowledge.taskHistoryIncomplete": "Доступна дополнительная история задач; число задач, требующих внимания, пока неполное.", + "newKnowledge.tasks": "Задачи", + "newKnowledge.tasksErrorDescription": "Не удалось загрузить фоновые задачи. Попробуйте еще раз.", + "newKnowledge.tasksWithAttention": "Задачи, {{count}} требуют внимания", "newKnowledge.unavailableDescription": "KnowledgeFS сейчас недоступен для этого рабочего пространства.", "newKnowledge.unavailableTitle": "Новая база знаний недоступна", "newKnowledge.updated": "Обновлено {{date}}", + "newKnowledge.updatedColumn": "Обновлено", "newKnowledge.uploadCharactersUnavailable": "Символы: —", + "newKnowledge.uploadDocuments": "Загрузить документы", "newKnowledge.uploadFiles": "Загрузить файлы", "newKnowledge.uploadFilesDescription": "Добавьте файлы с компьютера — они будут проиндексированы сразу после создания.", "newKnowledge.uploadingFiles": "Загрузка…", diff --git a/web/i18n/sl-SI/dataset.json b/web/i18n/sl-SI/dataset.json index 2df760fc004..1c64063ac4f 100644 --- a/web/i18n/sl-SI/dataset.json +++ b/web/i18n/sl-SI/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Model za ponovno razvrščanje je potreben za mešanico notranjega in zunanjega znanja.", "multimodal": "Multimodalen", "newKnowledge.actionsColumn": "Dejanja", + "newKnowledge.addDocument": "Dodaj dokument", "newKnowledge.addSource": "Dodaj vir", "newKnowledge.addSourceDescription": "Povežite vir s to bazo znanja — samodejno se sinhronizira in ostane sveža.", "newKnowledge.addSourceFailed": "Tega vira ni bilo mogoče dodati. Preverite nastavitve in poskusite znova.", "newKnowledge.addSourceRequiresSelection": "Pred dodajanjem vira izberite vsaj eno preiskano stran.", "newKnowledge.adjustAndRecrawl": "Prilagodi in ponovno preišči", + "newKnowledge.allDocumentStatuses": "Vsi statusi", "newKnowledge.allSources": "Vsi viri", "newKnowledge.appsUnavailable": "— aplikacij", "newKnowledge.authKind.api-key": "API ključ", "newKnowledge.authKind.endpoint": "Končna točka", "newKnowledge.authenticationMethod": "Metoda avtentikacije", "newKnowledge.backToList": "Nazaj k bazam znanja", + "newKnowledge.backgroundTasks": "Naloge v ozadju", + "newKnowledge.backgroundTasksDescription": "Dodajanje dokumentov, sinhronizacija virov in ponovno indeksiranje opravil.", "newKnowledge.booleanFalse": "Št", "newKnowledge.booleanTrue": "Da", + "newKnowledge.bulkDocumentActions": "Dejanja množičnih dokumentov", "newKnowledge.cancelAddSource": "Prekliči", "newKnowledge.cardType": "Prostor znanja", + "newKnowledge.clearDocumentSelection": "Počisti izbiro dokumenta", "newKnowledge.configureProvider": "Konfigurirajte {{provider}}", "newKnowledge.connectNotion": "Poveži Notion", "newKnowledge.connectProvider": "Povežite {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Ustvarjalci", "newKnowledge.customIntervalHours": "Interval po meri (ure)", "newKnowledge.customIntervalInvalid": "Vnesite celo število od 1 do 720 ur.", + "newKnowledge.deleteDocuments": "Izbriši ...", "newKnowledge.description": "Opis", "newKnowledge.descriptionHelp": "Jasen opis pomaga aplikacijam, da se odločijo, kdaj naj iščejo to znanje.", "newKnowledge.descriptionPlaceholder": "npr. Politika vračil, pogosta vprašanja o pošiljanju in koraki za eskalacijo — uporabite za vprašanja podpore strankam", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Nastavitev vira in predogled preiskovanja bosta odstranjena.", "newKnowledge.discardSourceDraftFailed": "Osnutka vira ni bilo mogoče zavreči. Poskusite znova.", "newKnowledge.discardSourceDraftTitle": "Želite zavreči ta osnutek vira?", + "newKnowledge.documentActions": "Dejanja za {{name}}", + "newKnowledge.documentActionsUnavailable": "Dejanja dokumentov še niso na voljo za to bazo znanja.", + "newKnowledge.documentColumn": "Dokument", + "newKnowledge.documentFilterLabel": "Filtrirajte dokumente po statusu", + "newKnowledge.documentPermissionRestricted": "Dovoljenja vašega delovnega prostora ne omogočajo dejanj dokumentov ali opravil.", + "newKnowledge.documentStatus.disabled": "Onemogočeno", + "newKnowledge.documentStatus.failed": "Ni uspelo", + "newKnowledge.documentStatus.processing": "Obdelava", + "newKnowledge.documentStatus.queued": "V čakalni vrsti", + "newKnowledge.documentStatus.ready": "Pripravljeno", + "newKnowledge.documentUploadExclusion.batchLimit": "omejitev velikosti paketa je presežena", + "newKnowledge.documentUploadExclusion.countLimit": "omejitev števila datotek je presežena", + "newKnowledge.documentUploadExclusion.fileSize": "datoteka je prevelika", + "newKnowledge.documentUploadExclusion.fileType": "nepodprta ali neveljavna vrsta datoteke", + "newKnowledge.documentUploadExclusion.more": "in še {{count}}", + "newKnowledge.documentUploadExclusion.processing": "obdelava ni uspela", + "newKnowledge.documentUploadExclusion.quota": "kvota delovnega prostora je presežena", + "newKnowledge.documentUploadExclusion.target": "cilj dokumenta ni več veljaven", + "newKnowledge.documentUploadFailed": "Teh dokumentov nismo mogli naložiti. poskusi ponovno", + "newKnowledge.documentUploadPartial": "Obdelava {{accepted}} dokumentov se je začela; {{excluded}} jih ni bilo mogoče dodati: {{details}}", + "newKnowledge.documentUploadRejected": "Noben dokument ni bil sprejet: {{details}}", + "newKnowledge.documentUploadStarted": "Začela se je obdelava dokumentov.", "newKnowledge.documents": "Dokumenti", + "newKnowledge.documentsDescription": "Sredstva znanja in njihove revizije — od kod prihaja posamezni dokument in ali je zadnja revizija indeksirana.", + "newKnowledge.documentsDropHint": "ali spustite PDF, DOCX, Markdown, HTML, XLSX ali TXT za nalaganje", + "newKnowledge.documentsEmptyDescription": "Tukaj naložite datoteke ali povežite vir na zavihku Viri – dokumenti so samodejno razčlenjeni, razdeljeni na kose in indeksirani.", + "newKnowledge.documentsEmptyTitle": "Dokumentov še ni", + "newKnowledge.documentsErrorDescription": "Dokumentov nismo mogli naložiti. poskusi ponovno", + "newKnowledge.documentsErrorTitle": "Dokumentov ni bilo mogoče naložiti", + "newKnowledge.documentsPermissionDescription": "Nimate dovoljenja za ogled dokumentov v tej bazi znanja.", + "newKnowledge.documentsPermissionTitle": "Dokumenti so omejeni", + "newKnowledge.documentsReindexFailed": "Ponovnega indeksiranja nismo mogli začeti. poskusi ponovno", + "newKnowledge.documentsReindexPartial": "{{queued}} dokumenti v čakalni vrsti; {{missing}} ne obstaja več.", + "newKnowledge.documentsReindexStarted": "Začelo se je ponovno indeksiranje.", + "newKnowledge.documentsSelected": "Izbrani dokumenti {{count}}", "newKnowledge.documentsUnavailable": "— dokumentov", + "newKnowledge.downloadDocuments": "Prenos", "newKnowledge.editSource": "Uredi", "newKnowledge.emptyDescription": "Dodajte dokumente in agenti jih lahko berejo neposredno — brez nastavljanja indeksiranja.", "newKnowledge.emptyTitle": "Pametnejši način ustvarjanja baz znanja", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Spoznajte novo bazo znanja ✨", "newKnowledge.illustrationHeadline": "Vsebino povežite enkrat — vsaka aplikacija bo odgovarjala z najnovejšim znanjem.", "newKnowledge.includeSubpages": "Vključi podstrani", + "newKnowledge.interruptTask": "Prekini", "newKnowledge.invalidRootUrl": "Vnesite veljaven URL http(s).", "newKnowledge.keepEditing": "Nadaljuj urejanje", + "newKnowledge.lastReadyRevisionHint": "Neuspešna obdelava nikoli ne prekine pridobivanja — zadnja pripravljena revizija ostane na voljo v indeksu vsebine.", "newKnowledge.lastSyncColumn": "Zadnja sinhronizacija", "newKnowledge.learnMore": "Več informacij", "newKnowledge.leavePartialSetupConfirm": "Zapusti nastavitev", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Staro", "newKnowledge.loadMore": "Naloži več", "newKnowledge.loadingSyncPolicy": "Nalaganje pravilnika sinhronizacije", + "newKnowledge.manualUpload": "Ročno nalaganje", "newKnowledge.maxPages": "Največje število strani", + "newKnowledge.metadata": "Meta podatki", "newKnowledge.moreProviders": "Več ponudnikov", "newKnowledge.name": "Ime znanja", "newKnowledge.namePlaceholder": "npr. Priročnik za podporo strankam", "newKnowledge.nameRequired": "Vnesite ime baze znanja.", "newKnowledge.new": "Novo", + "newKnowledge.noBackgroundTasks": "Ni še opravil v ozadju.", "newKnowledge.noDescription": "Brez opisa", + "newKnowledge.noMatchingDocuments": "Noben dokument ne ustreza temu iskanju.", "newKnowledge.noMatchingSources": "Noben vir ne ustreza temu iskanju.", "newKnowledge.noPagesFound": "Na {{host}} ni bilo najdenih strani", "newKnowledge.noPagesFoundDescription": "Pajek je dosegel spletno mesto, vendar ni našel strani za indeksiranje. Poskusite drug URL ali razširite možnosti preiskovanja.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "Preiskanih strani na {{host}}: {{count}}", "newKnowledge.pagesCrawled_other": "Preiskanih strani na {{host}}: {{count}}", "newKnowledge.pagesSelected": "Izbrano: {{count}}", + "newKnowledge.partialDocumentResults": "Morda se ujema več dokumentov. Naložite jih več za nadaljevanje iskanja.", "newKnowledge.permission": "Dovoljenje", "newKnowledge.permissionAllMembers": "Delovni prostor · vsi člani si lahko ogledujejo in urejajo", + "newKnowledge.permissionLoadFailed": "Dovoljenj za urejanje ni bilo mogoče preveriti. Poskusite znova.", "newKnowledge.permissionOnlyMe": "Samo jaz", "newKnowledge.permissionRestricted": "Vaša dovoljenja za delovni prostor ne dovoljujejo spreminjanja te nastavitve.", "newKnowledge.permissionUpdateFailed": "Zbirka znanja je bila ustvarjena, vendar njenega dovoljenja ni bilo mogoče potrditi. Poskusite znova, da dokončate nastavitev.", "newKnowledge.preview": "Predogled", "newKnowledge.previewUnavailable": "Predogled bo na voljo, ko zaledje dokonča obdelavo te datoteke.", + "newKnowledge.processDocument": "Proces {{name}}", + "newKnowledge.processingTaskState.canceled": "Prekinjeno", + "newKnowledge.processingTaskState.dispatch_pending": "Priprava", + "newKnowledge.processingTaskState.failed": "Ni uspelo", + "newKnowledge.processingTaskState.queued": "V čakalni vrsti", + "newKnowledge.processingTaskState.retry_wait": "Čakam na ponovni poskus", + "newKnowledge.processingTaskState.running": "Obdelava · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Dokončano", + "newKnowledge.processingTaskState.superseded": "Nadomeščeno", "newKnowledge.providerConnected": "{{provider}} je povezan", "newKnowledge.providerLabel": "Izberi ponudnika", "newKnowledge.providerLoadFailed": "Ponudnikov ni bilo mogoče naložiti", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Prosite urednika delovnega prostora, naj ustvari prvo bazo znanja.", "newKnowledge.refreshConnectionStatus": "Osveži stanje", "newKnowledge.refreshingConnection": "Osvežitev …", + "newKnowledge.reindexDocuments": "Ponovno indeksiraj", "newKnowledge.removeSource": "Odstrani vir", "newKnowledge.retryCrawl": "Ponovi preiskovanje", "newKnowledge.retryProviderLoad": "Poskusi znova", "newKnowledge.retrySyncPolicy": "Znova poskusi naložiti pravilnik sinhronizacije", + "newKnowledge.retryTask": "Poskusite znova", "newKnowledge.rootUrl": "Korenski URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Ta razdelek bo na voljo v naslednjem koraku nastavitve.", + "newKnowledge.searchDocuments": "Iskanje dokumentov", "newKnowledge.searchSources": "Iskanje virov", "newKnowledge.selectAll": "Izberi vse", + "newKnowledge.selectAllDocuments": "Izberite vse dokumente", "newKnowledge.selectAllSources": "Izberi vse vire", "newKnowledge.selectFilesAndFolders": "Izberite datoteke in mape za sinhronizacijo", "newKnowledge.skippedFailed": "Preskočeno (neuspešno)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Samo ročno", "newKnowledge.syncPolicyProvider": "Uporabi urnik ponudnika", "newKnowledge.tags": "Oznake", + "newKnowledge.taskActionFailed": "Tega opravila nismo mogli posodobiti. Osvežite in poskusite znova.", + "newKnowledge.taskAttentionClear": "Nobena opravila v ozadju ne potrebujejo pozornosti.", + "newKnowledge.taskAttentionCount": "Opravila v ozadju {{count}} zahtevajo pozornost.", + "newKnowledge.taskAttentionErrorCount": "Opravila v ozadju {{count}} zahtevajo pozornost, vključno z neuspešnimi opravili.", + "newKnowledge.taskFailedNotification": "Obdelava dokumenta ni uspela — poskusite znova v opravilih", + "newKnowledge.taskHistoryIncomplete": "Na voljo je več zgodovine opravil; število opravil, ki zahtevajo pozornost, ni popolno.", + "newKnowledge.tasks": "Naloge", + "newKnowledge.tasksErrorDescription": "Opravil v ozadju nismo mogli naložiti. poskusi ponovno", + "newKnowledge.tasksWithAttention": "Naloge, {{count}} potrebujejo pozornost", "newKnowledge.unavailableDescription": "KnowledgeFS trenutno ni na voljo za ta delovni prostor.", "newKnowledge.unavailableTitle": "Novo znanje ni na voljo", "newKnowledge.updated": "Posodobljeno {{date}}", + "newKnowledge.updatedColumn": "Posodobljeno", "newKnowledge.uploadCharactersUnavailable": "Znaki: —", + "newKnowledge.uploadDocuments": "Nalaganje dokumentov", "newKnowledge.uploadFiles": "Naloži datoteke", "newKnowledge.uploadFilesDescription": "Dodajte datoteke iz računalnika — indeksirajo se takoj ob ustvarjanju.", "newKnowledge.uploadingFiles": "Nalaganje…", diff --git a/web/i18n/th-TH/dataset.json b/web/i18n/th-TH/dataset.json index 9d0b197eaf6..94015c3215c 100644 --- a/web/i18n/th-TH/dataset.json +++ b/web/i18n/th-TH/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "โมเดล Rerank จําเป็นสําหรับการผสมผสานระหว่างความรู้ภายในและภายนอก", "multimodal": "หลายรูปแบบ", "newKnowledge.actionsColumn": "การดำเนินการ", + "newKnowledge.addDocument": "เพิ่มเอกสาร", "newKnowledge.addSource": "เพิ่มแหล่งที่มา", "newKnowledge.addSourceDescription": "เชื่อมต่อแหล่งข้อมูลกับฐานความรู้นี้ โดยจะซิงค์โดยอัตโนมัติและมีความสดใหม่อยู่เสมอ", "newKnowledge.addSourceFailed": "ไม่สามารถเพิ่มแหล่งข้อมูลนี้ได้ โปรดตรวจสอบการตั้งค่าแล้วลองอีกครั้ง", "newKnowledge.addSourceRequiresSelection": "เลือกหน้าที่รวบรวมข้อมูลแล้วอย่างน้อยหนึ่งหน้าก่อนเพิ่มแหล่งข้อมูล", "newKnowledge.adjustAndRecrawl": "ปรับและรวบรวมข้อมูลอีกครั้ง", + "newKnowledge.allDocumentStatuses": "สถานะทั้งหมด", "newKnowledge.allSources": "แหล่งที่มาทั้งหมด", "newKnowledge.appsUnavailable": "— แอป", "newKnowledge.authKind.api-key": "คีย์ API", "newKnowledge.authKind.endpoint": "จุดสิ้นสุด", "newKnowledge.authenticationMethod": "วิธีการตรวจสอบความถูกต้อง", "newKnowledge.backToList": "กลับสู่ฐานความรู้", + "newKnowledge.backgroundTasks": "งานเบื้องหลัง", + "newKnowledge.backgroundTasksDescription": "การเพิ่มเอกสาร การซิงค์แหล่งที่มา และงานจัดทำดัชนีใหม่", "newKnowledge.booleanFalse": "ไม่", "newKnowledge.booleanTrue": "ใช่", + "newKnowledge.bulkDocumentActions": "การดำเนินการกับเอกสารจำนวนมาก", "newKnowledge.cancelAddSource": "ยกเลิก", "newKnowledge.cardType": "พื้นที่ความรู้", + "newKnowledge.clearDocumentSelection": "ล้างการเลือกเอกสาร", "newKnowledge.configureProvider": "กำหนดค่า {{provider}}", "newKnowledge.connectNotion": "เชื่อมต่อ Notion", "newKnowledge.connectProvider": "เชื่อมต่อ {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "ผู้สร้าง", "newKnowledge.customIntervalHours": "ช่วงเวลาแบบกำหนดเอง (ชั่วโมง)", "newKnowledge.customIntervalInvalid": "ป้อนจำนวนเต็มตั้งแต่ 1 ถึง 720 ชั่วโมง", + "newKnowledge.deleteDocuments": "ลบ...", "newKnowledge.description": "คำอธิบาย", "newKnowledge.descriptionHelp": "คำอธิบายที่ชัดเจนช่วยให้แอปตัดสินใจว่าจะค้นหาความรู้นี้เมื่อใด", "newKnowledge.descriptionPlaceholder": "เช่น นโยบายการคืนเงิน คำถามที่พบบ่อยเกี่ยวกับการจัดส่ง และขั้นตอนการยกระดับ — ใช้สำหรับคำถามด้านการสนับสนุนลูกค้า", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "การกำหนดค่าแหล่งข้อมูลและตัวอย่างการรวบรวมข้อมูลจะถูกลบ", "newKnowledge.discardSourceDraftFailed": "ไม่สามารถละทิ้งฉบับร่างแหล่งข้อมูลได้ ลองอีกครั้ง", "newKnowledge.discardSourceDraftTitle": "ละทิ้งฉบับร่างแหล่งข้อมูลนี้หรือไม่", + "newKnowledge.documentActions": "การดำเนินการสำหรับ {{name}}", + "newKnowledge.documentActionsUnavailable": "การดำเนินการกับเอกสารยังไม่พร้อมใช้งานสำหรับฐานความรู้นี้", + "newKnowledge.documentColumn": "เอกสาร", + "newKnowledge.documentFilterLabel": "กรองเอกสารตามสถานะ", + "newKnowledge.documentPermissionRestricted": "สิทธิ์ของพื้นที่ทำงานไม่อนุญาตให้ดำเนินการกับเอกสารหรืองาน", + "newKnowledge.documentStatus.disabled": "ปิดการใช้งาน", + "newKnowledge.documentStatus.failed": "ล้มเหลว", + "newKnowledge.documentStatus.processing": "กำลังประมวลผล", + "newKnowledge.documentStatus.queued": "เข้าคิว", + "newKnowledge.documentStatus.ready": "พร้อม", + "newKnowledge.documentUploadExclusion.batchLimit": "เกินขีดจำกัดขนาดแบตช์", + "newKnowledge.documentUploadExclusion.countLimit": "เกินขีดจำกัดจำนวนไฟล์", + "newKnowledge.documentUploadExclusion.fileSize": "ไฟล์มีขนาดใหญ่เกินไป", + "newKnowledge.documentUploadExclusion.fileType": "ประเภทไฟล์ไม่รองรับหรือไม่ถูกต้อง", + "newKnowledge.documentUploadExclusion.more": "และอีก {{count}} รายการ", + "newKnowledge.documentUploadExclusion.processing": "การประมวลผลล้มเหลว", + "newKnowledge.documentUploadExclusion.quota": "เกินโควตาพื้นที่ทำงาน", + "newKnowledge.documentUploadExclusion.target": "เป้าหมายเอกสารไม่ถูกต้องอีกต่อไป", + "newKnowledge.documentUploadFailed": "เราไม่สามารถอัปโหลดเอกสารเหล่านี้ได้ ลองอีกครั้ง", + "newKnowledge.documentUploadPartial": "เริ่มประมวลผลเอกสาร {{accepted}} รายการแล้ว; เพิ่มไม่ได้ {{excluded}} รายการ: {{details}}", + "newKnowledge.documentUploadRejected": "ไม่มีเอกสารที่ได้รับการยอมรับ: {{details}}", + "newKnowledge.documentUploadStarted": "การประมวลผลเอกสารเริ่มต้นขึ้น", "newKnowledge.documents": "เอกสาร", + "newKnowledge.documentsDescription": "สินทรัพย์ความรู้และการแก้ไข — เอกสารแต่ละฉบับมาจากไหนและมีการจัดทำดัชนีการแก้ไขล่าสุดหรือไม่", + "newKnowledge.documentsDropHint": "หรือวาง PDF, DOCX, Markdown, HTML, XLSX หรือ TXT เพื่ออัปโหลด", + "newKnowledge.documentsEmptyDescription": "อัปโหลดไฟล์ที่นี่ หรือเชื่อมต่อแหล่งข้อมูลบนแท็บแหล่งที่มา เอกสารจะถูกแยกวิเคราะห์ แบ่งกลุ่ม และจัดทำดัชนีโดยอัตโนมัติ", + "newKnowledge.documentsEmptyTitle": "ยังไม่มีเอกสาร", + "newKnowledge.documentsErrorDescription": "เราไม่สามารถโหลดเอกสารได้ ลองอีกครั้ง", + "newKnowledge.documentsErrorTitle": "ไม่สามารถโหลดเอกสารได้", + "newKnowledge.documentsPermissionDescription": "คุณไม่มีสิทธิ์ดูเอกสารในฐานความรู้นี้", + "newKnowledge.documentsPermissionTitle": "เอกสารมีข้อจำกัด", + "newKnowledge.documentsReindexFailed": "เราไม่สามารถเริ่มการจัดทำดัชนีใหม่ได้ ลองอีกครั้ง", + "newKnowledge.documentsReindexPartial": "เอกสาร {{queued}} อยู่ในคิว; {{missing}} ไม่มีอยู่แล้ว", + "newKnowledge.documentsReindexStarted": "เริ่มการจัดทำดัชนีใหม่แล้ว", + "newKnowledge.documentsSelected": "เลือกเอกสาร {{count}} แล้ว", "newKnowledge.documentsUnavailable": "— เอกสาร", + "newKnowledge.downloadDocuments": "ดาวน์โหลด", "newKnowledge.editSource": "แก้ไข", "newKnowledge.emptyDescription": "เพิ่มเอกสารของคุณ แล้วเอเจนต์จะอ่านได้โดยตรง โดยไม่ต้องตั้งค่าการทำดัชนี", "newKnowledge.emptyTitle": "วิธีที่ชาญฉลาดกว่าในการสร้างฐานความรู้", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "พบกับฐานความรู้แบบใหม่ ✨", "newKnowledge.illustrationHeadline": "เชื่อมต่อเนื้อหาครั้งเดียว ทุกแอปจะตอบด้วยความรู้ล่าสุด", "newKnowledge.includeSubpages": "รวมหน้าย่อย", + "newKnowledge.interruptTask": "ขัดจังหวะ", "newKnowledge.invalidRootUrl": "ป้อน URL http(s) ที่ถูกต้อง", "newKnowledge.keepEditing": "แก้ไขต่อ", + "newKnowledge.lastReadyRevisionHint": "การประมวลผลที่ล้มเหลวจะไม่ขัดจังหวะการดึงข้อมูล — การแก้ไขที่พร้อมใช้ครั้งล่าสุดจะยังคงอยู่ในดัชนีเนื้อหา", "newKnowledge.lastSyncColumn": "ซิงค์ครั้งล่าสุด", "newKnowledge.learnMore": "เรียนรู้เพิ่มเติม", "newKnowledge.leavePartialSetupConfirm": "ออกจากการตั้งค่า", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "แบบเดิม", "newKnowledge.loadMore": "โหลดเพิ่มเติม", "newKnowledge.loadingSyncPolicy": "กำลังโหลดนโยบายการซิงค์", + "newKnowledge.manualUpload": "อัพโหลดด้วยตนเอง", "newKnowledge.maxPages": "จำนวนหน้าสูงสุด", + "newKnowledge.metadata": "ข้อมูลเมตา", "newKnowledge.moreProviders": "ผู้ให้บริการเพิ่มเติม", "newKnowledge.name": "ชื่อความรู้", "newKnowledge.namePlaceholder": "เช่น คู่มือการสนับสนุนลูกค้า", "newKnowledge.nameRequired": "ป้อนชื่อฐานความรู้", "newKnowledge.new": "แบบใหม่", + "newKnowledge.noBackgroundTasks": "ยังไม่มีงานพื้นหลัง", "newKnowledge.noDescription": "ไม่มีคำอธิบาย", + "newKnowledge.noMatchingDocuments": "ไม่มีเอกสารที่ตรงกับการค้นหานี้", "newKnowledge.noMatchingSources": "ไม่มีแหล่งที่มาที่ตรงกับการค้นหานี้", "newKnowledge.noPagesFound": "ไม่พบหน้าที่ {{host}}", "newKnowledge.noPagesFoundDescription": "โปรแกรมรวบรวมข้อมูลเข้าถึงเว็บไซต์แล้วแต่ไม่พบหน้าที่สร้างดัชนีได้ ลองใช้ URL อื่นหรือขยายตัวเลือกการรวบรวมข้อมูล", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "รวบรวมข้อมูล {{count}} หน้าที่ {{host}}", "newKnowledge.pagesCrawled_other": "รวบรวมข้อมูล {{count}} หน้าที่ {{host}}", "newKnowledge.pagesSelected": "เลือกแล้ว {{count}} รายการ", + "newKnowledge.partialDocumentResults": "อาจมีเอกสารที่ตรงกันเพิ่มเติม โหลดเพิ่มเพื่อค้นหาต่อ", "newKnowledge.permission": "การอนุญาต", "newKnowledge.permissionAllMembers": "พื้นที่ทำงาน · สมาชิกทุกคนสามารถดูและแก้ไขได้", + "newKnowledge.permissionLoadFailed": "ไม่สามารถตรวจสอบสิทธิ์การแก้ไขของคุณได้ โปรดลองอีกครั้ง", "newKnowledge.permissionOnlyMe": "มีเพียงฉันเท่านั้น", "newKnowledge.permissionRestricted": "สิทธิ์พื้นที่ทำงานของคุณไม่อนุญาตให้เปลี่ยนการตั้งค่านี้", "newKnowledge.permissionUpdateFailed": "สร้างฐานความรู้แล้ว แต่ไม่สามารถยืนยันสิทธิ์ได้ โปรดลองอีกครั้งเพื่อดำเนินการตั้งค่าให้เสร็จ", "newKnowledge.preview": "ตัวอย่าง", "newKnowledge.previewUnavailable": "ตัวอย่างจะพร้อมใช้งานหลังจากแบ็กเอนด์ประมวลผลไฟล์นี้เสร็จสิ้น", + "newKnowledge.processDocument": "ประมวลผล {{name}}", + "newKnowledge.processingTaskState.canceled": "ขัดจังหวะ", + "newKnowledge.processingTaskState.dispatch_pending": "กำลังเตรียมตัว", + "newKnowledge.processingTaskState.failed": "ล้มเหลว", + "newKnowledge.processingTaskState.queued": "เข้าคิว", + "newKnowledge.processingTaskState.retry_wait": "กำลังรอที่จะลองอีกครั้ง", + "newKnowledge.processingTaskState.running": "กำลังประมวลผล · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "เสร็จสิ้น", + "newKnowledge.processingTaskState.superseded": "เข้ามาแทนที่", "newKnowledge.providerConnected": "เชื่อมต่อ {{provider}} แล้ว", "newKnowledge.providerLabel": "เลือกผู้ให้บริการ", "newKnowledge.providerLoadFailed": "ผู้ให้บริการไม่สามารถโหลดได้", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "ขอให้ผู้แก้ไขพื้นที่ทำงานสร้างฐานความรู้แรก", "newKnowledge.refreshConnectionStatus": "รีเฟรชสถานะ", "newKnowledge.refreshingConnection": "สดชื่น...", + "newKnowledge.reindexDocuments": "จัดทำดัชนีใหม่", "newKnowledge.removeSource": "ลบแหล่งสัญญาณ", "newKnowledge.retryCrawl": "ลองรวบรวมข้อมูลอีกครั้ง", "newKnowledge.retryProviderLoad": "ลองอีกครั้ง", "newKnowledge.retrySyncPolicy": "ลองโหลดนโยบายการซิงค์อีกครั้ง", + "newKnowledge.retryTask": "ลองอีกครั้ง", "newKnowledge.rootUrl": "URL หลัก", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "ส่วนนี้จะพร้อมใช้งานในขั้นตอนการตั้งค่าถัดไป", + "newKnowledge.searchDocuments": "ค้นหาเอกสาร", "newKnowledge.searchSources": "แหล่งค้นหา", "newKnowledge.selectAll": "เลือกทั้งหมด", + "newKnowledge.selectAllDocuments": "เลือกเอกสารทั้งหมด", "newKnowledge.selectAllSources": "เลือกแหล่งที่มาทั้งหมด", "newKnowledge.selectFilesAndFolders": "เลือกไฟล์และโฟลเดอร์ที่จะซิงค์", "newKnowledge.skippedFailed": "ข้าม (ล้มเหลว)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "ด้วยตนเองเท่านั้น", "newKnowledge.syncPolicyProvider": "ใช้กำหนดการของผู้ให้บริการ", "newKnowledge.tags": "แท็ก", + "newKnowledge.taskActionFailed": "เราไม่สามารถอัปเดตงานนี้ได้ รีเฟรชแล้วลองอีกครั้ง", + "newKnowledge.taskAttentionClear": "ไม่มีงานพื้นหลังที่ต้องให้ความสนใจ", + "newKnowledge.taskAttentionCount": "งานพื้นหลัง {{count}} จำเป็นต้องได้รับการดูแล", + "newKnowledge.taskAttentionErrorCount": "งานเบื้องหลัง {{count}} ต้องการความสนใจ รวมถึงงานที่ล้มเหลวด้วย", + "newKnowledge.taskFailedNotification": "การประมวลผลเอกสารล้มเหลว — ลองอีกครั้งจากงาน", + "newKnowledge.taskHistoryIncomplete": "มีประวัติงานเพิ่มเติม จำนวนงานที่ต้องตรวจสอบยังไม่ครบถ้วน", + "newKnowledge.tasks": "งาน", + "newKnowledge.tasksErrorDescription": "เราไม่สามารถโหลดงานพื้นหลังได้ ลองอีกครั้ง", + "newKnowledge.tasksWithAttention": "งาน {{count}} ต้องการความสนใจ", "newKnowledge.unavailableDescription": "ขณะนี้ KnowledgeFS ไม่พร้อมใช้งานสำหรับพื้นที่ทำงานนี้", "newKnowledge.unavailableTitle": "ความรู้แบบใหม่ไม่พร้อมใช้งาน", "newKnowledge.updated": "อัปเดตเมื่อ {{date}}", + "newKnowledge.updatedColumn": "อัปเดตแล้ว", "newKnowledge.uploadCharactersUnavailable": "อักขระ: —", + "newKnowledge.uploadDocuments": "อัพโหลดเอกสาร", "newKnowledge.uploadFiles": "อัปโหลดไฟล์", "newKnowledge.uploadFilesDescription": "เพิ่มไฟล์จากคอมพิวเตอร์ ไฟล์จะถูกทำดัชนีทันทีที่สร้าง", "newKnowledge.uploadingFiles": "กำลังอัปโหลด…", diff --git a/web/i18n/tr-TR/dataset.json b/web/i18n/tr-TR/dataset.json index 8aab4dd2c43..cbd6fa18f4e 100644 --- a/web/i18n/tr-TR/dataset.json +++ b/web/i18n/tr-TR/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Yeniden Sıralama modeli, iç ve dış bilgilerin karışımı için gereklidir.", "multimodal": "Çok Modlu", "newKnowledge.actionsColumn": "Eylemler", + "newKnowledge.addDocument": "Belge ekle", "newKnowledge.addSource": "Kaynak ekle", "newKnowledge.addSourceDescription": "Bir kaynağı bu bilgi tabanına bağlayın; otomatik olarak senkronize edilir ve güncel kalır.", "newKnowledge.addSourceFailed": "Bu kaynak eklenemedi. Ayarları gözden geçirip tekrar deneyin.", "newKnowledge.addSourceRequiresSelection": "Kaynağı eklemeden önce taranmış en az bir sayfa seçin.", "newKnowledge.adjustAndRecrawl": "Ayarla ve yeniden tara", + "newKnowledge.allDocumentStatuses": "Tüm durum", "newKnowledge.allSources": "Tüm kaynaklar", "newKnowledge.appsUnavailable": "— uygulama", "newKnowledge.authKind.api-key": "API anahtarı", "newKnowledge.authKind.endpoint": "Uç nokta", "newKnowledge.authenticationMethod": "Kimlik doğrulama yöntemi", "newKnowledge.backToList": "Bilgi tabanlarına geri dön", + "newKnowledge.backgroundTasks": "Arka plan görevleri", + "newKnowledge.backgroundTasksDescription": "Belge ekleme, kaynak senkronizasyonu ve işleri yeniden indeksleme.", "newKnowledge.booleanFalse": "Hayır", "newKnowledge.booleanTrue": "Evet", + "newKnowledge.bulkDocumentActions": "Toplu belge işlemleri", "newKnowledge.cancelAddSource": "İptal et", "newKnowledge.cardType": "Bilgi alanı", + "newKnowledge.clearDocumentSelection": "Belge seçimini temizle", "newKnowledge.configureProvider": "{{provider}}'ı Yapılandır", "newKnowledge.connectNotion": "Notion’a bağlan", "newKnowledge.connectProvider": "{{provider}}'a bağlanın", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Oluşturanlar", "newKnowledge.customIntervalHours": "Özel aralık (saat)", "newKnowledge.customIntervalInvalid": "1 ile 720 saat arasında bir tam sayı girin.", + "newKnowledge.deleteDocuments": "Sil…", "newKnowledge.description": "Açıklama", "newKnowledge.descriptionHelp": "Açık bir açıklama, uygulamaların bu Bilgiyi ne zaman arayacaklarına karar vermesine yardımcı olur.", "newKnowledge.descriptionPlaceholder": "örneğin Geri ödeme politikası, gönderimle ilgili SSS'ler ve üst kademeye yükseltme adımları — müşteri desteği soruları için kullanın", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Kaynak yapılandırması ve tarama önizlemesi kaldırılacak.", "newKnowledge.discardSourceDraftFailed": "Kaynak taslağı silinemedi. Yeniden deneyin.", "newKnowledge.discardSourceDraftTitle": "Bu kaynak taslağı silinsin mi?", + "newKnowledge.documentActions": "{{name}} için eylemler", + "newKnowledge.documentActionsUnavailable": "Belge eylemleri henüz bu bilgi tabanında kullanılamıyor.", + "newKnowledge.documentColumn": "Belge", + "newKnowledge.documentFilterLabel": "Belgeleri duruma göre filtreleyin", + "newKnowledge.documentPermissionRestricted": "Çalışma alanı izinleriniz belge veya görev işlemlerine izin vermiyor.", + "newKnowledge.documentStatus.disabled": "Devre dışı", + "newKnowledge.documentStatus.failed": "Başarısız", + "newKnowledge.documentStatus.processing": "İşleme", + "newKnowledge.documentStatus.queued": "Sıraya alındı", + "newKnowledge.documentStatus.ready": "Hazır", + "newKnowledge.documentUploadExclusion.batchLimit": "toplu iş boyutu sınırı aşıldı", + "newKnowledge.documentUploadExclusion.countLimit": "dosya sayısı sınırı aşıldı", + "newKnowledge.documentUploadExclusion.fileSize": "dosya çok büyük", + "newKnowledge.documentUploadExclusion.fileType": "desteklenmeyen veya geçersiz dosya türü", + "newKnowledge.documentUploadExclusion.more": "ve {{count}} tane daha", + "newKnowledge.documentUploadExclusion.processing": "işleme başarısız oldu", + "newKnowledge.documentUploadExclusion.quota": "çalışma alanı kotası aşıldı", + "newKnowledge.documentUploadExclusion.target": "belge hedefi artık geçerli değil", + "newKnowledge.documentUploadFailed": "Bu belgeleri yükleyemedik. Tekrar deneyin.", + "newKnowledge.documentUploadPartial": "{{accepted}} belgenin işlenmesi başladı; {{excluded}} belge eklenemedi: {{details}}", + "newKnowledge.documentUploadRejected": "Hiçbir belge kabul edilmedi: {{details}}", + "newKnowledge.documentUploadStarted": "Evrak işlemleri başlatıldı.", "newKnowledge.documents": "Belgeler", + "newKnowledge.documentsDescription": "Bilgi varlıkları ve revizyonları — her belgenin nereden geldiği ve en son revizyonun indekslenip indekslenmediği.", + "newKnowledge.documentsDropHint": "veya yüklemek için PDF, DOCX, Markdown, HTML, XLSX veya TXT'yi bırakın", + "newKnowledge.documentsEmptyDescription": "Dosyaları buraya yükleyin veya Kaynaklar sekmesinde bir kaynağa bağlanın; belgeler otomatik olarak ayrıştırılır, parçalanır ve dizine eklenir.", + "newKnowledge.documentsEmptyTitle": "Henüz belge yok", + "newKnowledge.documentsErrorDescription": "Belgeleri yükleyemedik. Tekrar deneyin.", + "newKnowledge.documentsErrorTitle": "Dokümanlar yüklenemedi", + "newKnowledge.documentsPermissionDescription": "Bu bilgi tabanındaki belgeleri görüntüleme izniniz yok.", + "newKnowledge.documentsPermissionTitle": "Belgeler kısıtlı", + "newKnowledge.documentsReindexFailed": "Yeniden indekslemeye başlayamadık. Tekrar deneyin.", + "newKnowledge.documentsReindexPartial": "{{queued}} belgeleri sıraya alındı; {{missing}} artık mevcut değil.", + "newKnowledge.documentsReindexStarted": "Yeniden indeksleme başladı.", + "newKnowledge.documentsSelected": "{{count}} belgeleri seçildi", "newKnowledge.documentsUnavailable": "— belge", + "newKnowledge.downloadDocuments": "İndir", "newKnowledge.editSource": "Düzenle", "newKnowledge.emptyDescription": "Belgelerinizi ekleyin; ajanlar indeksleme yapılandırmadan belgeleri doğrudan okuyabilsin.", "newKnowledge.emptyTitle": "Bilgi tabanları oluşturmanın daha akıllı yolu", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Yeni Bilgi Tabanı ile tanışın ✨", "newKnowledge.illustrationHeadline": "İçeriğinizi bir kez bağlayın — her uygulama en güncel bilgilerle yanıt versin.", "newKnowledge.includeSubpages": "Alt sayfaları dahil et", + "newKnowledge.interruptTask": "Kesinti", "newKnowledge.invalidRootUrl": "Geçerli bir http(s) URL’si girin.", "newKnowledge.keepEditing": "Düzenlemeye devam et", + "newKnowledge.lastReadyRevisionHint": "Başarısız işleme hiçbir zaman alımı kesintiye uğratmaz; son hazır revizyon içerik dizininde mevcut kalır.", "newKnowledge.lastSyncColumn": "Son senkronizasyon", "newKnowledge.learnMore": "Daha fazla bilgi", "newKnowledge.leavePartialSetupConfirm": "Kurulumdan çık", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Eski", "newKnowledge.loadMore": "Daha fazla yükle", "newKnowledge.loadingSyncPolicy": "Eşitleme politikası yükleniyor", + "newKnowledge.manualUpload": "Manuel yükleme", "newKnowledge.maxPages": "En fazla sayfa", + "newKnowledge.metadata": "Meta Veriler", "newKnowledge.moreProviders": "Daha fazla sağlayıcı", "newKnowledge.name": "Bilgi adı", "newKnowledge.namePlaceholder": "örneğin Müşteri Desteği El Kitabı", "newKnowledge.nameRequired": "Bir bilgi tabanı adı girin.", "newKnowledge.new": "Yeni", + "newKnowledge.noBackgroundTasks": "Henüz arka plan görevi yok.", "newKnowledge.noDescription": "Açıklama yok", + "newKnowledge.noMatchingDocuments": "Bu aramayla eşleşen belge yok.", "newKnowledge.noMatchingSources": "Bu aramayla eşleşen kaynak yok.", "newKnowledge.noPagesFound": "{{host}} adresinde sayfa bulunamadı", "newKnowledge.noPagesFoundDescription": "Tarayıcı siteye ulaştı ancak dizine eklenebilir sayfa bulamadı. Farklı bir URL deneyin veya tarama seçeneklerini genişletin.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "{{host}} üzerinde {{count}} sayfa tarandı", "newKnowledge.pagesCrawled_other": "{{host}} üzerinde {{count}} sayfa tarandı", "newKnowledge.pagesSelected": "{{count}} seçildi", + "newKnowledge.partialDocumentResults": "Eşleşen başka belgeler olabilir. Aramaya devam etmek için daha fazla yükleyin.", "newKnowledge.permission": "İzin", "newKnowledge.permissionAllMembers": "Çalışma alanı · tüm üyeler görüntüleyebilir ve düzenleyebilir", + "newKnowledge.permissionLoadFailed": "Düzenleme izinleriniz doğrulanamadı. Tekrar deneyin.", "newKnowledge.permissionOnlyMe": "Sadece ben", "newKnowledge.permissionRestricted": "Çalışma alanı izinleriniz bu ayarın değiştirilmesine izin vermiyor.", "newKnowledge.permissionUpdateFailed": "Bilgi tabanı oluşturuldu ancak izni doğrulanamadı. Kurulumu tamamlamak için yeniden deneyin.", "newKnowledge.preview": "Önizleme", "newKnowledge.previewUnavailable": "Arka uç bu dosyayı işlemeyi tamamladığında önizleme kullanılabilir.", + "newKnowledge.processDocument": "Süreç {{name}}", + "newKnowledge.processingTaskState.canceled": "Kesintiye uğradı", + "newKnowledge.processingTaskState.dispatch_pending": "Hazırlanıyor", + "newKnowledge.processingTaskState.failed": "Başarısız", + "newKnowledge.processingTaskState.queued": "Sıraya alındı", + "newKnowledge.processingTaskState.retry_wait": "Yeniden denenmeyi bekliyor", + "newKnowledge.processingTaskState.running": "İşleniyor · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Tamamlandı", + "newKnowledge.processingTaskState.superseded": "Değiştirildi", "newKnowledge.providerConnected": "{{provider}} bağlandı", "newKnowledge.providerLabel": "Bir sağlayıcı seçin", "newKnowledge.providerLoadFailed": "Sağlayıcılar yüklenemedi", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "İlk bilgi tabanını oluşturması için bir çalışma alanı editörüne başvurun.", "newKnowledge.refreshConnectionStatus": "Durumu yenile", "newKnowledge.refreshingConnection": "Yenileniyor…", + "newKnowledge.reindexDocuments": "Yeniden indeksle", "newKnowledge.removeSource": "Kaynağı kaldır", "newKnowledge.retryCrawl": "Taramayı yeniden dene", "newKnowledge.retryProviderLoad": "Tekrar dene", "newKnowledge.retrySyncPolicy": "Eşitleme politikasını yeniden dene", + "newKnowledge.retryTask": "Yeniden dene", "newKnowledge.rootUrl": "Kök URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Bu bölüm bir sonraki kurulum adımında kullanılabilir hale gelecektir.", + "newKnowledge.searchDocuments": "Belgeleri arayın", "newKnowledge.searchSources": "Kaynakları ara", "newKnowledge.selectAll": "Tümünü seç", + "newKnowledge.selectAllDocuments": "Tüm belgeleri seç", "newKnowledge.selectAllSources": "Tüm kaynakları seç", "newKnowledge.selectFilesAndFolders": "Senkronize edilecek dosya ve klasörleri seçin", "newKnowledge.skippedFailed": "Atlandı (başarısız)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Yalnızca manuel", "newKnowledge.syncPolicyProvider": "Sağlayıcı zamanlamasını kullan", "newKnowledge.tags": "Etiketler", + "newKnowledge.taskActionFailed": "Bu görevi güncelleyemedik. Yenileyin ve tekrar deneyin.", + "newKnowledge.taskAttentionClear": "Hiçbir arka plan görevinin dikkat edilmesi gerekmez.", + "newKnowledge.taskAttentionCount": "{{count}} arka plan görevlerine dikkat edilmesi gerekiyor.", + "newKnowledge.taskAttentionErrorCount": "{{count}} Başarısız görevler de dahil olmak üzere arka plan görevlerine dikkat edilmesi gerekir.", + "newKnowledge.taskFailedNotification": "Belge işleme başarısız oldu — Görevler'den yeniden deneyin", + "newKnowledge.taskHistoryIncomplete": "Daha fazla görev geçmişi mevcut; ilgilenilmesi gereken görev sayısı henüz tamamlanmadı.", + "newKnowledge.tasks": "Görevler", + "newKnowledge.tasksErrorDescription": "Arka plan görevlerini yükleyemedik. Tekrar deneyin.", + "newKnowledge.tasksWithAttention": "Görevler, {{count}} tanesiyle ilgilenilmesi gerekiyor", "newKnowledge.unavailableDescription": "KnowledgeFS şu anda bu çalışma alanında kullanılamıyor.", "newKnowledge.unavailableTitle": "Yeni Bilgi kullanılamıyor", "newKnowledge.updated": "{{date}} tarihinde güncellendi", + "newKnowledge.updatedColumn": "Güncellendi", "newKnowledge.uploadCharactersUnavailable": "Karakterler: —", + "newKnowledge.uploadDocuments": "Belgeleri yükleyin", "newKnowledge.uploadFiles": "Dosya yükle", "newKnowledge.uploadFilesDescription": "Bilgisayarınızdan dosya ekleyin; oluşturduğunuz anda indekslenirler.", "newKnowledge.uploadingFiles": "Yükleniyor…", diff --git a/web/i18n/uk-UA/dataset.json b/web/i18n/uk-UA/dataset.json index 0af9b2f1618..938f7030959 100644 --- a/web/i18n/uk-UA/dataset.json +++ b/web/i18n/uk-UA/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Модель Rerank необхідна для поєднання внутрішніх і зовнішніх знань.", "multimodal": "Мультимодальний", "newKnowledge.actionsColumn": "Дії", + "newKnowledge.addDocument": "Додати документ", "newKnowledge.addSource": "Додати джерело", "newKnowledge.addSourceDescription": "Підключіть джерело до цієї бази знань — вона автоматично синхронізується та залишається актуальною.", "newKnowledge.addSourceFailed": "Не вдалося додати це джерело. Перевірте налаштування та повторіть спробу.", "newKnowledge.addSourceRequiresSelection": "Перед додаванням джерела виберіть принаймні одну проскановану сторінку.", "newKnowledge.adjustAndRecrawl": "Налаштувати й просканувати знову", + "newKnowledge.allDocumentStatuses": "Всі статуси", "newKnowledge.allSources": "Усі джерела", "newKnowledge.appsUnavailable": "— застосунків", "newKnowledge.authKind.api-key": "API ключ", "newKnowledge.authKind.endpoint": "Кінцева точка", "newKnowledge.authenticationMethod": "Метод автентифікації", "newKnowledge.backToList": "Назад до баз знань", + "newKnowledge.backgroundTasks": "Фонові завдання", + "newKnowledge.backgroundTasksDescription": "Додавання документів, синхронізація джерела та повторне індексування завдань.", "newKnowledge.booleanFalse": "Ні", "newKnowledge.booleanTrue": "Так", + "newKnowledge.bulkDocumentActions": "Масові дії з документами", "newKnowledge.cancelAddSource": "Скасувати", "newKnowledge.cardType": "Простір знань", + "newKnowledge.clearDocumentSelection": "Очистити вибір документів", "newKnowledge.configureProvider": "Налаштувати {{provider}}", "newKnowledge.connectNotion": "Підключити Notion", "newKnowledge.connectProvider": "Підключіть {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Автори", "newKnowledge.customIntervalHours": "Власний інтервал (години)", "newKnowledge.customIntervalInvalid": "Введіть ціле число від 1 до 720 годин.", + "newKnowledge.deleteDocuments": "Видалити…", "newKnowledge.description": "опис", "newKnowledge.descriptionHelp": "Чіткий опис допомагає програмам вирішити, коли шукати ці знання.", "newKnowledge.descriptionPlaceholder": "напр. Політика відшкодування, поширені запитання щодо доставки та етапи ескалації — використовуйте для запитань служби підтримки клієнтів", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Конфігурацію джерела й попередній перегляд сканування буде видалено.", "newKnowledge.discardSourceDraftFailed": "Не вдалося відхилити чернетку джерела. Повторіть спробу.", "newKnowledge.discardSourceDraftTitle": "Відхилити цю чернетку джерела?", + "newKnowledge.documentActions": "Дії для {{name}}", + "newKnowledge.documentActionsUnavailable": "Дії з документами поки що недоступні для цієї бази знань.", + "newKnowledge.documentColumn": "документ", + "newKnowledge.documentFilterLabel": "Фільтр документів за статусом", + "newKnowledge.documentPermissionRestricted": "Дозволи вашого робочого простору не дозволяють виконувати дії з документами або завданнями.", + "newKnowledge.documentStatus.disabled": "Вимкнено", + "newKnowledge.documentStatus.failed": "Не вдалося", + "newKnowledge.documentStatus.processing": "Обробка", + "newKnowledge.documentStatus.queued": "У черзі", + "newKnowledge.documentStatus.ready": "Готовий", + "newKnowledge.documentUploadExclusion.batchLimit": "перевищено обмеження розміру пакета", + "newKnowledge.documentUploadExclusion.countLimit": "перевищено обмеження кількості файлів", + "newKnowledge.documentUploadExclusion.fileSize": "файл завеликий", + "newKnowledge.documentUploadExclusion.fileType": "непідтримуваний або недійсний тип файлу", + "newKnowledge.documentUploadExclusion.more": "і ще {{count}}", + "newKnowledge.documentUploadExclusion.processing": "помилка обробки", + "newKnowledge.documentUploadExclusion.quota": "перевищено квоту робочої області", + "newKnowledge.documentUploadExclusion.target": "ціль документа більше недійсна", + "newKnowledge.documentUploadFailed": "Нам не вдалося завантажити ці документи. Спробуйте знову.", + "newKnowledge.documentUploadPartial": "Розпочато обробку {{accepted}} документів; не вдалося додати {{excluded}}: {{details}}", + "newKnowledge.documentUploadRejected": "Жодного документа не прийнято: {{details}}", + "newKnowledge.documentUploadStarted": "Розпочато обробку документів.", "newKnowledge.documents": "Документи", + "newKnowledge.documentsDescription": "Бази знань та їх редакції — звідки походить кожен документ і чи проіндексовано останню редакцію.", + "newKnowledge.documentsDropHint": "або відпустіть PDF, DOCX, Markdown, HTML, XLSX або TXT для завантаження", + "newKnowledge.documentsEmptyDescription": "Завантажте файли сюди або підключіть джерело на вкладці «Джерела» — документи аналізуються, розбиваються на частини та індексуються автоматично.", + "newKnowledge.documentsEmptyTitle": "Документів ще немає", + "newKnowledge.documentsErrorDescription": "Не вдалося завантажити документи. Спробуйте знову.", + "newKnowledge.documentsErrorTitle": "Не вдалося завантажити документи", + "newKnowledge.documentsPermissionDescription": "Ви не маєте дозволу переглядати документи в цій базі знань.", + "newKnowledge.documentsPermissionTitle": "Документи обмежені", + "newKnowledge.documentsReindexFailed": "Не вдалося розпочати повторне індексування. Спробуйте знову.", + "newKnowledge.documentsReindexPartial": "{{queued}} документи в черзі; {{missing}} більше не існує.", + "newKnowledge.documentsReindexStarted": "Розпочато переіндексацію.", + "newKnowledge.documentsSelected": "Вибрані документи {{count}}", "newKnowledge.documentsUnavailable": "— документів", + "newKnowledge.downloadDocuments": "Завантажити", "newKnowledge.editSource": "Редагувати", "newKnowledge.emptyDescription": "Додайте документи, і агенти зможуть читати їх безпосередньо — без налаштування індексації.", "newKnowledge.emptyTitle": "Розумніший спосіб створювати бази знань", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Познайомтеся з новою базою знань ✨", "newKnowledge.illustrationHeadline": "Підключіть вміст один раз — і кожна програма відповідатиме на основі найсвіжіших знань.", "newKnowledge.includeSubpages": "Включати підсторінки", + "newKnowledge.interruptTask": "Переривати", "newKnowledge.invalidRootUrl": "Введіть дійсну URL-адресу http(s).", "newKnowledge.keepEditing": "Продовжити редагування", + "newKnowledge.lastReadyRevisionHint": "Невдала обробка ніколи не перериває пошук — остання готова версія залишається доступною в індексі вмісту.", "newKnowledge.lastSyncColumn": "Остання синхронізація", "newKnowledge.learnMore": "Докладніше", "newKnowledge.leavePartialSetupConfirm": "Вийти з налаштування", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Попередня версія", "newKnowledge.loadMore": "Завантажити ще", "newKnowledge.loadingSyncPolicy": "Завантаження політики синхронізації", + "newKnowledge.manualUpload": "Ручне завантаження", "newKnowledge.maxPages": "Максимум сторінок", + "newKnowledge.metadata": "Метадані", "newKnowledge.moreProviders": "Інші провайдери", "newKnowledge.name": "Назва знання", "newKnowledge.namePlaceholder": "напр. Посібник із підтримки клієнтів", "newKnowledge.nameRequired": "Введіть назву бази знань.", "newKnowledge.new": "Нова версія", + "newKnowledge.noBackgroundTasks": "Фонових завдань ще немає.", "newKnowledge.noDescription": "Немає опису", + "newKnowledge.noMatchingDocuments": "Немає документів, що відповідають цьому запиту.", "newKnowledge.noMatchingSources": "Жодне джерело не відповідає цьому запиту.", "newKnowledge.noPagesFound": "На {{host}} не знайдено сторінок", "newKnowledge.noPagesFoundDescription": "Робот отримав доступ до сайту, але не знайшов сторінок для індексування. Спробуйте іншу URL-адресу або розширте параметри сканування.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "Проскановано сторінок на {{host}}: {{count}}", "newKnowledge.pagesCrawled_other": "Проскановано сторінок на {{host}}: {{count}}", "newKnowledge.pagesSelected": "Вибрано: {{count}}", + "newKnowledge.partialDocumentResults": "Може бути більше відповідних документів. Завантажте ще, щоб продовжити пошук.", "newKnowledge.permission": "Дозвіл", "newKnowledge.permissionAllMembers": "Робоча область · усі учасники можуть переглядати та редагувати", + "newKnowledge.permissionLoadFailed": "Не вдалося перевірити дозволи на редагування. Спробуйте ще раз.", "newKnowledge.permissionOnlyMe": "Тільки я", "newKnowledge.permissionRestricted": "Дозволи вашої робочої області не дозволяють змінити це налаштування.", "newKnowledge.permissionUpdateFailed": "Базу знань створено, але не вдалося підтвердити її дозвіл. Повторіть спробу, щоб завершити налаштування.", "newKnowledge.preview": "Попередній перегляд", "newKnowledge.previewUnavailable": "Попередній перегляд стане доступним після завершення обробки цього файлу сервером.", + "newKnowledge.processDocument": "Процес {{name}}", + "newKnowledge.processingTaskState.canceled": "Перерваний", + "newKnowledge.processingTaskState.dispatch_pending": "Готується", + "newKnowledge.processingTaskState.failed": "Не вдалося", + "newKnowledge.processingTaskState.queued": "У черзі", + "newKnowledge.processingTaskState.retry_wait": "Очікування повторної спроби", + "newKnowledge.processingTaskState.running": "Обробка · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Виконано", + "newKnowledge.processingTaskState.superseded": "Замінено", "newKnowledge.providerConnected": "{{provider}} підключено", "newKnowledge.providerLabel": "Оберіть провайдера", "newKnowledge.providerLoadFailed": "Не вдалося завантажити постачальників", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Попросіть редактора робочого простору створити першу базу знань.", "newKnowledge.refreshConnectionStatus": "Оновити статус", "newKnowledge.refreshingConnection": "Оновлення…", + "newKnowledge.reindexDocuments": "Переіндексувати", "newKnowledge.removeSource": "Видалити джерело", "newKnowledge.retryCrawl": "Повторити сканування", "newKnowledge.retryProviderLoad": "Спробуйте ще раз", "newKnowledge.retrySyncPolicy": "Повторити завантаження політики синхронізації", + "newKnowledge.retryTask": "Повторіть спробу", "newKnowledge.rootUrl": "Коренева URL-адреса", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Цей розділ стане доступним на наступному кроці налаштування.", + "newKnowledge.searchDocuments": "Пошук документів", "newKnowledge.searchSources": "Пошук джерел", "newKnowledge.selectAll": "Вибрати все", + "newKnowledge.selectAllDocuments": "Виберіть усі документи", "newKnowledge.selectAllSources": "Вибрати всі джерела", "newKnowledge.selectFilesAndFolders": "Виберіть файли й папки для синхронізації", "newKnowledge.skippedFailed": "Пропущено (помилка)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Лише вручну", "newKnowledge.syncPolicyProvider": "Використовувати розклад постачальника", "newKnowledge.tags": "Теги", + "newKnowledge.taskActionFailed": "Не вдалося оновити це завдання. Оновіть і повторіть спробу.", + "newKnowledge.taskAttentionClear": "Жодні фонові завдання не потребують уваги.", + "newKnowledge.taskAttentionCount": "Фонові завдання {{count}} потребують уваги.", + "newKnowledge.taskAttentionErrorCount": "Фонові завдання {{count}} потребують уваги, включаючи невдалі завдання.", + "newKnowledge.taskFailedNotification": "Не вдалося обробити документ — повторіть спробу в розділі «Завдання»", + "newKnowledge.taskHistoryIncomplete": "Доступна додаткова історія завдань; кількість завдань, що потребують уваги, поки неповна.", + "newKnowledge.tasks": "завдання", + "newKnowledge.tasksErrorDescription": "Не вдалося завантажити фонові завдання. Спробуйте знову.", + "newKnowledge.tasksWithAttention": "Завдання, {{count}} потребують уваги", "newKnowledge.unavailableDescription": "KnowledgeFS зараз недоступний для цього робочого простору.", "newKnowledge.unavailableTitle": "Нова база знань недоступна", "newKnowledge.updated": "Оновлено {{date}}", + "newKnowledge.updatedColumn": "Оновлено", "newKnowledge.uploadCharactersUnavailable": "Символи: —", + "newKnowledge.uploadDocuments": "Завантажити документи", "newKnowledge.uploadFiles": "Завантажити файли", "newKnowledge.uploadFilesDescription": "Додайте файли з комп’ютера — вони індексуються одразу після створення.", "newKnowledge.uploadingFiles": "Завантаження…", diff --git a/web/i18n/vi-VN/dataset.json b/web/i18n/vi-VN/dataset.json index 81637c9e4fd..f8f849ce91a 100644 --- a/web/i18n/vi-VN/dataset.json +++ b/web/i18n/vi-VN/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Mô hình Rerank là cần thiết cho sự kết hợp giữa kiến thức bên trong và bên ngoài.", "multimodal": "Đa phương thức", "newKnowledge.actionsColumn": "Hành động", + "newKnowledge.addDocument": "Thêm tài liệu", "newKnowledge.addSource": "Thêm nguồn", "newKnowledge.addSourceDescription": "Kết nối nguồn với cơ sở kiến thức này — nguồn này tự động đồng bộ hóa và luôn cập nhật.", "newKnowledge.addSourceFailed": "Không thể thêm nguồn này. Hãy kiểm tra cài đặt và thử lại.", "newKnowledge.addSourceRequiresSelection": "Chọn ít nhất một trang đã thu thập trước khi thêm nguồn.", "newKnowledge.adjustAndRecrawl": "Điều chỉnh và thu thập lại", + "newKnowledge.allDocumentStatuses": "Tất cả trạng thái", "newKnowledge.allSources": "Tất cả các nguồn", "newKnowledge.appsUnavailable": "— ứng dụng", "newKnowledge.authKind.api-key": "Khóa API", "newKnowledge.authKind.endpoint": "Điểm cuối", "newKnowledge.authenticationMethod": "Phương thức xác thực", "newKnowledge.backToList": "Quay lại cơ sở kiến thức", + "newKnowledge.backgroundTasks": "Tác vụ nền", + "newKnowledge.backgroundTasksDescription": "Tài liệu thêm, đồng bộ hóa nguồn và lập chỉ mục lại công việc.", "newKnowledge.booleanFalse": "Không", "newKnowledge.booleanTrue": "Có", + "newKnowledge.bulkDocumentActions": "Hành động tài liệu hàng loạt", "newKnowledge.cancelAddSource": "Hủy", "newKnowledge.cardType": "Không gian kiến thức", + "newKnowledge.clearDocumentSelection": "Xóa lựa chọn tài liệu", "newKnowledge.configureProvider": "Định cấu hình {{provider}}", "newKnowledge.connectNotion": "Kết nối Notion", "newKnowledge.connectProvider": "Kết nối {{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "Người tạo", "newKnowledge.customIntervalHours": "Khoảng thời gian tùy chỉnh (giờ)", "newKnowledge.customIntervalInvalid": "Nhập số nguyên từ 1 đến 720 giờ.", + "newKnowledge.deleteDocuments": "Xóa…", "newKnowledge.description": "Mô tả", "newKnowledge.descriptionHelp": "Mô tả rõ ràng giúp ứng dụng quyết định thời điểm tìm kiếm Kiến thức này.", "newKnowledge.descriptionPlaceholder": "ví dụ. Chính sách hoàn tiền, Câu hỏi thường gặp về vận chuyển và các bước báo cáo — sử dụng cho các câu hỏi hỗ trợ khách hàng", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "Cấu hình nguồn và bản xem trước thu thập sẽ bị xóa.", "newKnowledge.discardSourceDraftFailed": "Không thể hủy bản nháp nguồn. Hãy thử lại.", "newKnowledge.discardSourceDraftTitle": "Hủy bản nháp nguồn này?", + "newKnowledge.documentActions": "Các hành động dành cho {{name}}", + "newKnowledge.documentActionsUnavailable": "Hành động tài liệu chưa có sẵn cho cơ sở kiến thức này.", + "newKnowledge.documentColumn": "tài liệu", + "newKnowledge.documentFilterLabel": "Lọc tài liệu theo trạng thái", + "newKnowledge.documentPermissionRestricted": "Quyền trong không gian làm việc của bạn không cho phép thao tác với tài liệu hoặc tác vụ.", + "newKnowledge.documentStatus.disabled": "Đã tắt", + "newKnowledge.documentStatus.failed": "thất bại", + "newKnowledge.documentStatus.processing": "Đang xử lý", + "newKnowledge.documentStatus.queued": "Đã xếp hàng", + "newKnowledge.documentStatus.ready": "Sẵn sàng", + "newKnowledge.documentUploadExclusion.batchLimit": "đã vượt quá giới hạn kích thước lô", + "newKnowledge.documentUploadExclusion.countLimit": "đã vượt quá giới hạn số lượng tệp", + "newKnowledge.documentUploadExclusion.fileSize": "tệp quá lớn", + "newKnowledge.documentUploadExclusion.fileType": "loại tệp không được hỗ trợ hoặc không hợp lệ", + "newKnowledge.documentUploadExclusion.more": "và {{count}} mục khác", + "newKnowledge.documentUploadExclusion.processing": "xử lý thất bại", + "newKnowledge.documentUploadExclusion.quota": "đã vượt quá hạn mức không gian làm việc", + "newKnowledge.documentUploadExclusion.target": "đích tài liệu không còn hợp lệ", + "newKnowledge.documentUploadFailed": "Chúng tôi không thể tải lên những tài liệu này. Hãy thử lại.", + "newKnowledge.documentUploadPartial": "Đã bắt đầu xử lý {{accepted}} tài liệu; không thể thêm {{excluded}} tài liệu: {{details}}", + "newKnowledge.documentUploadRejected": "Không có tài liệu nào được chấp nhận: {{details}}", + "newKnowledge.documentUploadStarted": "Quá trình xử lý tài liệu bắt đầu.", "newKnowledge.documents": "Tài liệu", + "newKnowledge.documentsDescription": "Tài sản kiến ​​thức và các bản sửa đổi của chúng - mỗi tài liệu đến từ đâu và liệu bản sửa đổi mới nhất có được lập chỉ mục hay không.", + "newKnowledge.documentsDropHint": "hoặc thả PDF, DOCX, Markdown, HTML, XLSX hoặc TXT để tải lên", + "newKnowledge.documentsEmptyDescription": "Tải tệp lên đây hoặc kết nối nguồn trên tab Nguồn — tài liệu được phân tích cú pháp, phân đoạn và lập chỉ mục tự động.", + "newKnowledge.documentsEmptyTitle": "Chưa có tài liệu nào", + "newKnowledge.documentsErrorDescription": "Chúng tôi không thể tải tài liệu. Hãy thử lại.", + "newKnowledge.documentsErrorTitle": "Không thể tải tài liệu", + "newKnowledge.documentsPermissionDescription": "Bạn không có quyền xem tài liệu trong cơ sở kiến thức này.", + "newKnowledge.documentsPermissionTitle": "Tài liệu bị hạn chế", + "newKnowledge.documentsReindexFailed": "Chúng tôi không thể bắt đầu lập chỉ mục lại. Hãy thử lại.", + "newKnowledge.documentsReindexPartial": "Tài liệu {{queued}} được xếp hàng đợi; {{missing}} không còn tồn tại.", + "newKnowledge.documentsReindexStarted": "Lập chỉ mục lại bắt đầu.", + "newKnowledge.documentsSelected": "Đã chọn tài liệu {{count}}", "newKnowledge.documentsUnavailable": "— tài liệu", + "newKnowledge.downloadDocuments": "Tải xuống", "newKnowledge.editSource": "Chỉnh sửa", "newKnowledge.emptyDescription": "Thêm tài liệu để tác nhân có thể đọc trực tiếp — không cần cấu hình lập chỉ mục.", "newKnowledge.emptyTitle": "Cách thông minh hơn để xây dựng cơ sở kiến thức", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "Khám phá Cơ sở kiến thức mới ✨", "newKnowledge.illustrationHeadline": "Kết nối nội dung một lần — mọi ứng dụng sẽ trả lời bằng kiến thức mới nhất.", "newKnowledge.includeSubpages": "Bao gồm các trang con", + "newKnowledge.interruptTask": "Ngắt", "newKnowledge.invalidRootUrl": "Nhập URL http(s) hợp lệ.", "newKnowledge.keepEditing": "Tiếp tục chỉnh sửa", + "newKnowledge.lastReadyRevisionHint": "Quá trình xử lý không thành công không bao giờ làm gián đoạn quá trình truy xuất — bản sửa đổi sẵn sàng cuối cùng vẫn có sẵn trong chỉ mục nội dung.", "newKnowledge.lastSyncColumn": "Đồng bộ hóa lần cuối", "newKnowledge.learnMore": "Tìm hiểu thêm", "newKnowledge.leavePartialSetupConfirm": "Thoát thiết lập", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "Cũ", "newKnowledge.loadMore": "Tải thêm", "newKnowledge.loadingSyncPolicy": "Đang tải chính sách đồng bộ", + "newKnowledge.manualUpload": "Tải lên thủ công", "newKnowledge.maxPages": "Số trang tối đa", + "newKnowledge.metadata": "Siêu dữ liệu", "newKnowledge.moreProviders": "Nhà cung cấp khác", "newKnowledge.name": "Tên kiến thức", "newKnowledge.namePlaceholder": "ví dụ: Sổ tay hỗ trợ khách hàng", "newKnowledge.nameRequired": "Nhập tên cơ sở kiến thức.", "newKnowledge.new": "Mới", + "newKnowledge.noBackgroundTasks": "Chưa có tác vụ nền nào.", "newKnowledge.noDescription": "Không có mô tả", + "newKnowledge.noMatchingDocuments": "Không có tài liệu phù hợp với tìm kiếm này.", "newKnowledge.noMatchingSources": "Không có nguồn nào phù hợp với tìm kiếm này.", "newKnowledge.noPagesFound": "Không tìm thấy trang nào tại {{host}}", "newKnowledge.noPagesFoundDescription": "Trình thu thập đã truy cập trang web nhưng không tìm thấy trang nào có thể lập chỉ mục. Hãy thử URL khác hoặc mở rộng tùy chọn thu thập.", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "Đã thu thập {{count}} trang tại {{host}}", "newKnowledge.pagesCrawled_other": "Đã thu thập {{count}} trang tại {{host}}", "newKnowledge.pagesSelected": "Đã chọn {{count}}", + "newKnowledge.partialDocumentResults": "Có thể còn tài liệu phù hợp khác. Hãy tải thêm để tiếp tục tìm kiếm.", "newKnowledge.permission": "Quyền", "newKnowledge.permissionAllMembers": "Không gian làm việc · tất cả thành viên đều có thể xem và chỉnh sửa", + "newKnowledge.permissionLoadFailed": "Không thể xác minh quyền chỉnh sửa của bạn. Hãy thử lại.", "newKnowledge.permissionOnlyMe": "Chỉ có tôi", "newKnowledge.permissionRestricted": "Quyền không gian làm việc của bạn không cho phép thay đổi cài đặt này.", "newKnowledge.permissionUpdateFailed": "Cơ sở kiến thức đã được tạo nhưng không thể xác nhận quyền. Hãy thử lại để hoàn tất thiết lập.", "newKnowledge.preview": "Xem trước", "newKnowledge.previewUnavailable": "Bản xem trước sẽ khả dụng sau khi backend xử lý xong tệp này.", + "newKnowledge.processDocument": "Quá trình {{name}}", + "newKnowledge.processingTaskState.canceled": "Bị gián đoạn", + "newKnowledge.processingTaskState.dispatch_pending": "chuẩn bị", + "newKnowledge.processingTaskState.failed": "thất bại", + "newKnowledge.processingTaskState.queued": "Đã xếp hàng", + "newKnowledge.processingTaskState.retry_wait": "Đang chờ thử lại", + "newKnowledge.processingTaskState.running": "Đang xử lý · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "Đã hoàn thành", + "newKnowledge.processingTaskState.superseded": "Đã thay thế", "newKnowledge.providerConnected": "Đã kết nối {{provider}}", "newKnowledge.providerLabel": "Chọn nhà cung cấp", "newKnowledge.providerLoadFailed": "Nhà cung cấp không thể tải", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "Yêu cầu biên tập viên không gian làm việc tạo cơ sở kiến thức đầu tiên.", "newKnowledge.refreshConnectionStatus": "Trạng thái làm mới", "newKnowledge.refreshingConnection": "Đang làm mới…", + "newKnowledge.reindexDocuments": "Lập chỉ mục lại", "newKnowledge.removeSource": "Xóa nguồn", "newKnowledge.retryCrawl": "Thử thu thập lại", "newKnowledge.retryProviderLoad": "Thử lại", "newKnowledge.retrySyncPolicy": "Thử lại chính sách đồng bộ", + "newKnowledge.retryTask": "Thử lại", "newKnowledge.rootUrl": "URL gốc", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Phần này sẽ có sẵn trong bước thiết lập tiếp theo.", + "newKnowledge.searchDocuments": "Tìm kiếm tài liệu", "newKnowledge.searchSources": "Nguồn tìm kiếm", "newKnowledge.selectAll": "Chọn tất cả", + "newKnowledge.selectAllDocuments": "Chọn tất cả tài liệu", "newKnowledge.selectAllSources": "Chọn tất cả nguồn", "newKnowledge.selectFilesAndFolders": "Chọn tệp và thư mục để đồng bộ", "newKnowledge.skippedFailed": "Đã bỏ qua (thất bại)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "Chỉ thủ công", "newKnowledge.syncPolicyProvider": "Dùng lịch của nhà cung cấp", "newKnowledge.tags": "Thẻ", + "newKnowledge.taskActionFailed": "Chúng tôi không thể cập nhật nhiệm vụ này. Hãy làm mới và thử lại.", + "newKnowledge.taskAttentionClear": "Không có tác vụ nền nào cần chú ý.", + "newKnowledge.taskAttentionCount": "Các tác vụ chạy nền {{count}} cần được chú ý.", + "newKnowledge.taskAttentionErrorCount": "Các tác vụ chạy nền {{count}} cần được chú ý, kể cả những tác vụ bị lỗi.", + "newKnowledge.taskFailedNotification": "Xử lý tài liệu không thành công — thử lại trong Tác vụ", + "newKnowledge.taskHistoryIncomplete": "Còn lịch sử tác vụ khác; số tác vụ cần chú ý chưa đầy đủ.", + "newKnowledge.tasks": "Nhiệm vụ", + "newKnowledge.tasksErrorDescription": "Chúng tôi không thể tải tác vụ nền. Hãy thử lại.", + "newKnowledge.tasksWithAttention": "Nhiệm vụ, {{count}} cần chú ý", "newKnowledge.unavailableDescription": "KnowledgeFS hiện không khả dụng cho không gian làm việc này.", "newKnowledge.unavailableTitle": "Kiến thức mới không khả dụng", "newKnowledge.updated": "Cập nhật {{date}}", + "newKnowledge.updatedColumn": "Đã cập nhật", "newKnowledge.uploadCharactersUnavailable": "Ký tự: —", + "newKnowledge.uploadDocuments": "Tải tài liệu lên", "newKnowledge.uploadFiles": "Tải tệp lên", "newKnowledge.uploadFilesDescription": "Thêm tệp từ máy tính — tệp sẽ được lập chỉ mục ngay khi tạo.", "newKnowledge.uploadingFiles": "Đang tải lên…", diff --git a/web/i18n/zh-Hans/dataset.json b/web/i18n/zh-Hans/dataset.json index 4eec8a5ee87..dc03d3bda14 100644 --- a/web/i18n/zh-Hans/dataset.json +++ b/web/i18n/zh-Hans/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "混合使用内部和外部知识时需要配置 Rerank 模型。", "multimodal": "多模态", "newKnowledge.actionsColumn": "行动", + "newKnowledge.addDocument": "添加文档", "newKnowledge.addSource": "添加来源", "newKnowledge.addSourceDescription": "将来源连接到该知识库 - 它会自动同步并保持最新。", "newKnowledge.addSourceFailed": "无法添加此来源。请检查设置后重试。", "newKnowledge.addSourceRequiresSelection": "添加来源前,请至少选择一个已抓取的页面。", "newKnowledge.adjustAndRecrawl": "调整并重新抓取", + "newKnowledge.allDocumentStatuses": "全部状态", "newKnowledge.allSources": "所有来源", "newKnowledge.appsUnavailable": "— 个应用", "newKnowledge.authKind.api-key": "API 密钥", "newKnowledge.authKind.endpoint": "端点", "newKnowledge.authenticationMethod": "认证方式", "newKnowledge.backToList": "返回知识库列表", + "newKnowledge.backgroundTasks": "后台任务", + "newKnowledge.backgroundTasksDescription": "文档添加、数据源同步和重新索引任务。", "newKnowledge.booleanFalse": "否", "newKnowledge.booleanTrue": "是的", + "newKnowledge.bulkDocumentActions": "批量文档操作", "newKnowledge.cancelAddSource": "取消", "newKnowledge.cardType": "知识空间", + "newKnowledge.clearDocumentSelection": "清除文档选择", "newKnowledge.configureProvider": "配置{{provider}}", "newKnowledge.connectNotion": "连接 Notion", "newKnowledge.connectProvider": "连接{{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "创建者", "newKnowledge.customIntervalHours": "自定义间隔(小时)", "newKnowledge.customIntervalInvalid": "请输入 1 到 720 之间的整数小时数。", + "newKnowledge.deleteDocuments": "删除…", "newKnowledge.description": "描述", "newKnowledge.descriptionHelp": "清晰的描述有助于应用判断何时搜索此知识库。", "newKnowledge.descriptionPlaceholder": "例如:退款政策、配送常见问题和升级处理步骤——用于客户支持问题", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "来源配置和抓取预览将被移除。", "newKnowledge.discardSourceDraftFailed": "无法放弃来源草稿。请重试。", "newKnowledge.discardSourceDraftTitle": "放弃此来源草稿?", + "newKnowledge.documentActions": "{{name}} 的操作", + "newKnowledge.documentActionsUnavailable": "此知识库暂不支持文档操作。", + "newKnowledge.documentColumn": "文档", + "newKnowledge.documentFilterLabel": "按状态筛选文档", + "newKnowledge.documentPermissionRestricted": "你的工作区权限不允许执行文档或任务操作。", + "newKnowledge.documentStatus.disabled": "已禁用", + "newKnowledge.documentStatus.failed": "失败", + "newKnowledge.documentStatus.processing": "处理中", + "newKnowledge.documentStatus.queued": "排队中", + "newKnowledge.documentStatus.ready": "就绪", + "newKnowledge.documentUploadExclusion.batchLimit": "超出批次大小限制", + "newKnowledge.documentUploadExclusion.countLimit": "超出文件数量限制", + "newKnowledge.documentUploadExclusion.fileSize": "文件过大", + "newKnowledge.documentUploadExclusion.fileType": "不支持或无效的文件类型", + "newKnowledge.documentUploadExclusion.more": "以及另外 {{count}} 个", + "newKnowledge.documentUploadExclusion.processing": "处理失败", + "newKnowledge.documentUploadExclusion.quota": "超出工作区配额", + "newKnowledge.documentUploadExclusion.target": "文档目标已失效", + "newKnowledge.documentUploadFailed": "无法上传这些文档,请重试。", + "newKnowledge.documentUploadPartial": "已开始处理 {{accepted}} 个文档;{{excluded}} 个无法添加:{{details}}", + "newKnowledge.documentUploadRejected": "未接受任何文档:{{details}}", + "newKnowledge.documentUploadStarted": "文档处理已开始。", "newKnowledge.documents": "文档", + "newKnowledge.documentsDescription": "知识资产及其修订,显示每个文档的来源以及最新修订是否已建立索引。", + "newKnowledge.documentsDropHint": "或拖放 PDF、DOCX、Markdown、HTML、XLSX 或 TXT 文件以上传", + "newKnowledge.documentsEmptyDescription": "在此上传文件,或在“数据源”标签页连接数据源。文档会自动完成解析、分块和索引。", + "newKnowledge.documentsEmptyTitle": "暂无文档", + "newKnowledge.documentsErrorDescription": "无法加载文档,请重试。", + "newKnowledge.documentsErrorTitle": "文档加载失败", + "newKnowledge.documentsPermissionDescription": "你没有权限查看此知识库中的文档。", + "newKnowledge.documentsPermissionTitle": "文档访问受限", + "newKnowledge.documentsReindexFailed": "无法开始重新索引,请重试。", + "newKnowledge.documentsReindexPartial": "已将 {{queued}} 个文档加入队列;{{missing}} 个文档已不存在。", + "newKnowledge.documentsReindexStarted": "已开始重新索引。", + "newKnowledge.documentsSelected": "已选择 {{count}} 个文档", "newKnowledge.documentsUnavailable": "— 篇文档", + "newKnowledge.downloadDocuments": "下载", "newKnowledge.editSource": "编辑", "newKnowledge.emptyDescription": "添加文档后,智能体即可直接读取,无需配置索引。", "newKnowledge.emptyTitle": "更智能的知识库构建方式", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "认识全新的知识库 ✨", "newKnowledge.illustrationHeadline": "只需连接一次内容,每个应用都能基于最新知识回答。", "newKnowledge.includeSubpages": "包含子页面", + "newKnowledge.interruptTask": "中断", "newKnowledge.invalidRootUrl": "请输入有效的 http(s) URL。", "newKnowledge.keepEditing": "继续编辑", + "newKnowledge.lastReadyRevisionHint": "处理失败不会中断检索,内容索引中会继续保留上一个就绪的修订。", "newKnowledge.lastSyncColumn": "上次同步", "newKnowledge.learnMore": "了解更多", "newKnowledge.leavePartialSetupConfirm": "离开设置", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "旧版", "newKnowledge.loadMore": "加载更多", "newKnowledge.loadingSyncPolicy": "正在加载同步策略", + "newKnowledge.manualUpload": "手动上传", "newKnowledge.maxPages": "最大页面数", + "newKnowledge.metadata": "元数据", "newKnowledge.moreProviders": "更多提供商", "newKnowledge.name": "知识库名称", "newKnowledge.namePlaceholder": "例如:客户支持手册", "newKnowledge.nameRequired": "请输入知识库名称。", "newKnowledge.new": "新版", + "newKnowledge.noBackgroundTasks": "暂无后台任务。", "newKnowledge.noDescription": "暂无描述", + "newKnowledge.noMatchingDocuments": "没有符合此搜索条件的文档。", "newKnowledge.noMatchingSources": "没有与此搜索匹配的来源。", "newKnowledge.noPagesFound": "在 {{host}} 未找到页面", "newKnowledge.noPagesFoundDescription": "抓取工具已访问网站,但未找到可建立索引的页面。请尝试其他 URL,或扩大抓取范围。", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "已抓取 {{host}} 的 {{count}} 个页面", "newKnowledge.pagesCrawled_other": "已抓取 {{host}} 的 {{count}} 个页面", "newKnowledge.pagesSelected": "已选择 {{count}} 项", + "newKnowledge.partialDocumentResults": "可能还有匹配的文档。加载更多以继续搜索。", "newKnowledge.permission": "权限", "newKnowledge.permissionAllMembers": "工作区 · 所有成员均可查看和编辑", + "newKnowledge.permissionLoadFailed": "无法验证你的编辑权限,请重试。", "newKnowledge.permissionOnlyMe": "仅自己", "newKnowledge.permissionRestricted": "你的工作区权限不允许更改此设置。", "newKnowledge.permissionUpdateFailed": "知识库已创建,但无法确认其权限状态。请重试以完成设置。", "newKnowledge.preview": "预览", "newKnowledge.previewUnavailable": "后端完成此文件的处理后即可预览。", + "newKnowledge.processDocument": "处理 {{name}}", + "newKnowledge.processingTaskState.canceled": "已中断", + "newKnowledge.processingTaskState.dispatch_pending": "准备中", + "newKnowledge.processingTaskState.failed": "失败", + "newKnowledge.processingTaskState.queued": "排队中", + "newKnowledge.processingTaskState.retry_wait": "等待重试", + "newKnowledge.processingTaskState.running": "处理中 · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "已完成", + "newKnowledge.processingTaskState.superseded": "已被取代", "newKnowledge.providerConnected": "{{provider}}已连接", "newKnowledge.providerLabel": "提供商", "newKnowledge.providerLoadFailed": "提供商无法加载", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "请联系工作区编辑者创建第一个知识库。", "newKnowledge.refreshConnectionStatus": "刷新状态", "newKnowledge.refreshingConnection": "正在刷新……", + "newKnowledge.reindexDocuments": "重新索引", "newKnowledge.removeSource": "删除来源", "newKnowledge.retryCrawl": "重试抓取", "newKnowledge.retryProviderLoad": "重试", "newKnowledge.retrySyncPolicy": "重试同步策略", + "newKnowledge.retryTask": "重试", "newKnowledge.rootUrl": "根 URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "此部分将在下一设置步骤中开放。", + "newKnowledge.searchDocuments": "搜索文档", "newKnowledge.searchSources": "搜索来源", "newKnowledge.selectAll": "全选", + "newKnowledge.selectAllDocuments": "选择全部文档", "newKnowledge.selectAllSources": "选择所有来源", "newKnowledge.selectFilesAndFolders": "选择要同步的文件和文件夹", "newKnowledge.skippedFailed": "已跳过(失败)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "仅手动", "newKnowledge.syncPolicyProvider": "使用提供商计划", "newKnowledge.tags": "标签", + "newKnowledge.taskActionFailed": "无法更新此任务,请刷新后重试。", + "newKnowledge.taskAttentionClear": "没有需要关注的后台任务。", + "newKnowledge.taskAttentionCount": "{{count}} 个后台任务需要关注。", + "newKnowledge.taskAttentionErrorCount": "{{count}} 个后台任务需要关注,其中包含失败的任务。", + "newKnowledge.taskFailedNotification": "文档处理失败,请在“任务”中重试", + "newKnowledge.taskHistoryIncomplete": "还有更多任务记录,需关注的任务数量尚未完整。", + "newKnowledge.tasks": "任务", + "newKnowledge.tasksErrorDescription": "无法加载后台任务,请重试。", + "newKnowledge.tasksWithAttention": "任务,{{count}} 个需要关注", "newKnowledge.unavailableDescription": "KnowledgeFS 当前在此工作区不可用。", "newKnowledge.unavailableTitle": "新版知识库不可用", "newKnowledge.updated": "更新于 {{date}}", + "newKnowledge.updatedColumn": "更新时间", "newKnowledge.uploadCharactersUnavailable": "字符数:—", + "newKnowledge.uploadDocuments": "上传文档", "newKnowledge.uploadFiles": "上传文件", "newKnowledge.uploadFilesDescription": "从电脑添加文件,创建后会立即建立索引。", "newKnowledge.uploadingFiles": "正在上传…", diff --git a/web/i18n/zh-Hant/dataset.json b/web/i18n/zh-Hant/dataset.json index c70ca2fb615..7c4a2490a9e 100644 --- a/web/i18n/zh-Hant/dataset.json +++ b/web/i18n/zh-Hant/dataset.json @@ -132,21 +132,27 @@ "mixtureInternalAndExternalTip": "Rerank 模型是內部和外部知識的混合所必需的。", "multimodal": "多模態", "newKnowledge.actionsColumn": "行動", + "newKnowledge.addDocument": "新增文件", "newKnowledge.addSource": "新增來源", "newKnowledge.addSourceDescription": "將來源連接到該知識庫 - 它會自動同步並保持最新。", "newKnowledge.addSourceFailed": "無法新增此來源。請檢查設定後再試一次。", "newKnowledge.addSourceRequiresSelection": "新增來源前,請至少選取一個已抓取的頁面。", "newKnowledge.adjustAndRecrawl": "調整並重新抓取", + "newKnowledge.allDocumentStatuses": "全部狀態", "newKnowledge.allSources": "所有來源", "newKnowledge.appsUnavailable": "— 個應用程式", "newKnowledge.authKind.api-key": "API 金鑰", "newKnowledge.authKind.endpoint": "端點", "newKnowledge.authenticationMethod": "認證方式", "newKnowledge.backToList": "返回知識庫列表", + "newKnowledge.backgroundTasks": "背景任務", + "newKnowledge.backgroundTasksDescription": "文件新增、資料來源同步和重新索引任務。", "newKnowledge.booleanFalse": "否", "newKnowledge.booleanTrue": "是的", + "newKnowledge.bulkDocumentActions": "批次文件操作", "newKnowledge.cancelAddSource": "取消", "newKnowledge.cardType": "知識空間", + "newKnowledge.clearDocumentSelection": "清除文件選取", "newKnowledge.configureProvider": "配置{{provider}}", "newKnowledge.connectNotion": "連接 Notion", "newKnowledge.connectProvider": "連接{{provider}}", @@ -181,6 +187,7 @@ "newKnowledge.creators": "建立者", "newKnowledge.customIntervalHours": "自訂間隔(小時)", "newKnowledge.customIntervalInvalid": "請輸入 1 到 720 之間的整數小時數。", + "newKnowledge.deleteDocuments": "刪除…", "newKnowledge.description": "描述", "newKnowledge.descriptionHelp": "清楚的描述有助於應用程式判斷何時搜尋此知識庫。", "newKnowledge.descriptionPlaceholder": "例如:退款政策、配送常見問題和升級處理步驟——用於客戶支援問題", @@ -196,8 +203,43 @@ "newKnowledge.discardSourceDraftDescription": "來源設定和抓取預覽將被移除。", "newKnowledge.discardSourceDraftFailed": "無法捨棄來源草稿。請重試。", "newKnowledge.discardSourceDraftTitle": "捨棄此來源草稿?", + "newKnowledge.documentActions": "{{name}} 的操作", + "newKnowledge.documentActionsUnavailable": "此知識庫暫不支援文件操作。", + "newKnowledge.documentColumn": "文件", + "newKnowledge.documentFilterLabel": "依狀態篩選文件", + "newKnowledge.documentPermissionRestricted": "你的工作區權限不允許執行文件或任務操作。", + "newKnowledge.documentStatus.disabled": "已停用", + "newKnowledge.documentStatus.failed": "失敗", + "newKnowledge.documentStatus.processing": "處理中", + "newKnowledge.documentStatus.queued": "排隊中", + "newKnowledge.documentStatus.ready": "就緒", + "newKnowledge.documentUploadExclusion.batchLimit": "超出批次大小限制", + "newKnowledge.documentUploadExclusion.countLimit": "超出檔案數量限制", + "newKnowledge.documentUploadExclusion.fileSize": "檔案過大", + "newKnowledge.documentUploadExclusion.fileType": "不支援或無效的檔案類型", + "newKnowledge.documentUploadExclusion.more": "以及另外 {{count}} 個", + "newKnowledge.documentUploadExclusion.processing": "處理失敗", + "newKnowledge.documentUploadExclusion.quota": "超出工作區配額", + "newKnowledge.documentUploadExclusion.target": "文件目標已失效", + "newKnowledge.documentUploadFailed": "無法上傳這些文件,請再試一次。", + "newKnowledge.documentUploadPartial": "已開始處理 {{accepted}} 個文件;{{excluded}} 個無法新增:{{details}}", + "newKnowledge.documentUploadRejected": "未接受任何文件:{{details}}", + "newKnowledge.documentUploadStarted": "文件處理已開始。", "newKnowledge.documents": "文件", + "newKnowledge.documentsDescription": "知識資產及其修訂,顯示每份文件的來源以及最新修訂是否已建立索引。", + "newKnowledge.documentsDropHint": "或拖放 PDF、DOCX、Markdown、HTML、XLSX 或 TXT 檔案以上傳", + "newKnowledge.documentsEmptyDescription": "在此上傳檔案,或在「資料來源」分頁連接資料來源。文件會自動完成解析、分塊和索引。", + "newKnowledge.documentsEmptyTitle": "暫無文件", + "newKnowledge.documentsErrorDescription": "無法載入文件,請再試一次。", + "newKnowledge.documentsErrorTitle": "文件載入失敗", + "newKnowledge.documentsPermissionDescription": "你沒有權限查看此知識庫中的文件。", + "newKnowledge.documentsPermissionTitle": "文件存取受限", + "newKnowledge.documentsReindexFailed": "無法開始重新索引,請再試一次。", + "newKnowledge.documentsReindexPartial": "已將 {{queued}} 個文件加入佇列;{{missing}} 個文件已不存在。", + "newKnowledge.documentsReindexStarted": "已開始重新索引。", + "newKnowledge.documentsSelected": "已選取 {{count}} 個文件", "newKnowledge.documentsUnavailable": "— 份文件", + "newKnowledge.downloadDocuments": "下載", "newKnowledge.editSource": "編輯", "newKnowledge.emptyDescription": "加入文件後,智慧代理即可直接讀取,無需設定索引。", "newKnowledge.emptyTitle": "更聰明的知識庫建立方式", @@ -210,8 +252,10 @@ "newKnowledge.guideTitle": "認識全新的知識庫 ✨", "newKnowledge.illustrationHeadline": "只需連接一次內容,每個應用程式都能以最新知識回答。", "newKnowledge.includeSubpages": "包含子頁面", + "newKnowledge.interruptTask": "中斷", "newKnowledge.invalidRootUrl": "請輸入有效的 http(s) URL。", "newKnowledge.keepEditing": "繼續編輯", + "newKnowledge.lastReadyRevisionHint": "處理失敗不會中斷檢索,內容索引中會繼續保留上一個就緒的修訂。", "newKnowledge.lastSyncColumn": "上次同步", "newKnowledge.learnMore": "瞭解更多", "newKnowledge.leavePartialSetupConfirm": "離開設定", @@ -220,13 +264,17 @@ "newKnowledge.legacy": "舊版", "newKnowledge.loadMore": "載入更多", "newKnowledge.loadingSyncPolicy": "正在載入同步政策", + "newKnowledge.manualUpload": "手動上傳", "newKnowledge.maxPages": "頁面數上限", + "newKnowledge.metadata": "中繼資料", "newKnowledge.moreProviders": "更多提供者", "newKnowledge.name": "知識庫名稱", "newKnowledge.namePlaceholder": "例如:客戶支援手冊", "newKnowledge.nameRequired": "請輸入知識庫名稱。", "newKnowledge.new": "新版", + "newKnowledge.noBackgroundTasks": "暫無背景任務。", "newKnowledge.noDescription": "暫無描述", + "newKnowledge.noMatchingDocuments": "沒有符合此搜尋條件的文件。", "newKnowledge.noMatchingSources": "沒有與此搜尋相符的來源。", "newKnowledge.noPagesFound": "在 {{host}} 找不到頁面", "newKnowledge.noPagesFoundDescription": "抓取工具已連線至網站,但找不到可建立索引的頁面。請嘗試其他 URL,或擴大抓取範圍。", @@ -242,13 +290,24 @@ "newKnowledge.pagesCrawled_one": "已抓取 {{host}} 的 {{count}} 個頁面", "newKnowledge.pagesCrawled_other": "已抓取 {{host}} 的 {{count}} 個頁面", "newKnowledge.pagesSelected": "已選取 {{count}} 項", + "newKnowledge.partialDocumentResults": "可能還有符合的文件。載入更多以繼續搜尋。", "newKnowledge.permission": "權限", "newKnowledge.permissionAllMembers": "工作區 · 所有成員皆可檢視和編輯", + "newKnowledge.permissionLoadFailed": "無法驗證你的編輯權限,請再試一次。", "newKnowledge.permissionOnlyMe": "僅自己", "newKnowledge.permissionRestricted": "你的工作區權限不允許變更此設定。", "newKnowledge.permissionUpdateFailed": "知識庫已建立,但無法確認其權限狀態。請重試以完成設定。", "newKnowledge.preview": "預覽", "newKnowledge.previewUnavailable": "後端完成此檔案的處理後即可預覽。", + "newKnowledge.processDocument": "處理 {{name}}", + "newKnowledge.processingTaskState.canceled": "已中斷", + "newKnowledge.processingTaskState.dispatch_pending": "準備中", + "newKnowledge.processingTaskState.failed": "失敗", + "newKnowledge.processingTaskState.queued": "排隊中", + "newKnowledge.processingTaskState.retry_wait": "等待重試", + "newKnowledge.processingTaskState.running": "處理中 · {{progress}}%", + "newKnowledge.processingTaskState.succeeded": "已完成", + "newKnowledge.processingTaskState.superseded": "已被取代", "newKnowledge.providerConnected": "{{provider}}已連接", "newKnowledge.providerLabel": "供應商", "newKnowledge.providerLoadFailed": "提供者無法載入", @@ -259,15 +318,19 @@ "newKnowledge.readOnlyEmpty": "請聯絡工作區編輯者建立第一個知識庫。", "newKnowledge.refreshConnectionStatus": "刷新狀態", "newKnowledge.refreshingConnection": "正在重新整理…", + "newKnowledge.reindexDocuments": "重新索引", "newKnowledge.removeSource": "刪除來源", "newKnowledge.retryCrawl": "重試抓取", "newKnowledge.retryProviderLoad": "重試", "newKnowledge.retrySyncPolicy": "重試同步政策", + "newKnowledge.retryTask": "重試", "newKnowledge.rootUrl": "根 URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "此區段將在下一個設定步驟中開放。", + "newKnowledge.searchDocuments": "搜尋文件", "newKnowledge.searchSources": "搜尋來源", "newKnowledge.selectAll": "全選", + "newKnowledge.selectAllDocuments": "選取全部文件", "newKnowledge.selectAllSources": "選擇所有來源", "newKnowledge.selectFilesAndFolders": "選擇要同步的檔案與資料夾", "newKnowledge.skippedFailed": "已略過(失敗)", @@ -311,10 +374,21 @@ "newKnowledge.syncPolicyManual": "僅限手動", "newKnowledge.syncPolicyProvider": "使用供應商排程", "newKnowledge.tags": "標籤", + "newKnowledge.taskActionFailed": "無法更新此任務,請重新整理後再試一次。", + "newKnowledge.taskAttentionClear": "沒有需要注意的背景任務。", + "newKnowledge.taskAttentionCount": "{{count}} 個背景任務需要注意。", + "newKnowledge.taskAttentionErrorCount": "{{count}} 個背景任務需要注意,其中包含失敗的任務。", + "newKnowledge.taskFailedNotification": "文件處理失敗,請在「任務」中重試", + "newKnowledge.taskHistoryIncomplete": "還有更多任務記錄,需關注的任務數量尚未完整。", + "newKnowledge.tasks": "任務", + "newKnowledge.tasksErrorDescription": "無法載入背景任務,請再試一次。", + "newKnowledge.tasksWithAttention": "任務,{{count}} 個需要注意", "newKnowledge.unavailableDescription": "KnowledgeFS 目前在此工作區不可用。", "newKnowledge.unavailableTitle": "新版知識庫不可用", "newKnowledge.updated": "更新於 {{date}}", + "newKnowledge.updatedColumn": "更新時間", "newKnowledge.uploadCharactersUnavailable": "字元數:—", + "newKnowledge.uploadDocuments": "上傳文件", "newKnowledge.uploadFiles": "上傳檔案", "newKnowledge.uploadFilesDescription": "從電腦加入檔案,建立後會立即建立索引。", "newKnowledge.uploadingFiles": "正在上傳…",