diff --git a/api/services/knowledge_fs_operations.py b/api/services/knowledge_fs_operations.py index b8ee5fbd517..0a140b2be2e 100644 --- a/api/services/knowledge_fs_operations.py +++ b/api/services/knowledge_fs_operations.py @@ -99,7 +99,7 @@ def _external_source_operation( *, request_headers: tuple[str, ...] = ("x-trace-id",), ) -> KnowledgeFSOperation: - """Declare a source-connection operation restricted to dataset editors.""" + """Declare an external-source operation restricted to dataset editors.""" return _console_operation( operation_id, method, @@ -347,12 +347,10 @@ KNOWLEDGE_FS_CONSOLE_OPERATIONS: Final[tuple[KnowledgeFSOperation, ...]] = ( rbac_permission=RBACPermission.DATASET_READONLY, legacy_role="reader", ), - _console_operation( - operation_id="putKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy", - method="PUT", - path="knowledge-spaces/{id}/sources/{sourceId}/sync-policy", - rbac_permission=RBACPermission.DATASET_EDIT, - legacy_role="dataset_editor", + _external_source_operation( + "putKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy", + "PUT", + "knowledge-spaces/{id}/sources/{sourceId}/sync-policy", ), _dataset_read_operation("getKnowledgeSpacesByIdDocuments", "knowledge-spaces/{id}/documents"), _dataset_edit_operation("postKnowledgeSpacesByIdDocuments", "POST", "knowledge-spaces/{id}/documents"), diff --git a/api/tests/unit_tests/services/test_knowledge_fs_proxy.py b/api/tests/unit_tests/services/test_knowledge_fs_proxy.py index 1033c76f13d..3ba26fb9733 100644 --- a/api/tests/unit_tests/services/test_knowledge_fs_proxy.py +++ b/api/tests/unit_tests/services/test_knowledge_fs_proxy.py @@ -158,7 +158,10 @@ _OPERATION_AUTHORIZATION_POLICIES = { "dataset_editor", ), "getKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy": (RBACPermission.DATASET_READONLY, "reader"), - "putKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy": (RBACPermission.DATASET_EDIT, "dataset_editor"), + "putKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy": ( + RBACPermission.DATASET_EXTERNAL_CONNECT, + "dataset_editor", + ), "getKnowledgeSpacesByIdLogicalDocuments": (RBACPermission.DATASET_READONLY, "reader"), "getKnowledgeSpacesByIdLogicalDocumentsByDocumentId": (RBACPermission.DATASET_READONLY, "reader"), "getKnowledgeSpacesByIdDocumentsByDocumentIdRevisions": (RBACPermission.DATASET_READONLY, "reader"), diff --git a/packages/iconify-collections/assets/public/common/dropbox.svg b/packages/iconify-collections/assets/public/common/dropbox.svg new file mode 100644 index 00000000000..b2f0eae0359 --- /dev/null +++ b/packages/iconify-collections/assets/public/common/dropbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/public/common/firecrawl.svg b/packages/iconify-collections/assets/public/common/firecrawl.svg new file mode 100644 index 00000000000..8f4356b7c30 --- /dev/null +++ b/packages/iconify-collections/assets/public/common/firecrawl.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/public/common/google-drive.svg b/packages/iconify-collections/assets/public/common/google-drive.svg new file mode 100644 index 00000000000..812910a7116 --- /dev/null +++ b/packages/iconify-collections/assets/public/common/google-drive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/custom-public/icons.json b/packages/iconify-collections/custom-public/icons.json index ff9d33c6744..ef48d368602 100644 --- a/packages/iconify-collections/custom-public/icons.json +++ b/packages/iconify-collections/custom-public/icons.json @@ -1,6 +1,6 @@ { "prefix": "custom-public", - "lastModified": 1784810412, + "lastModified": 1784870906, "icons": { "agent-building-blocks": { "body": "" @@ -83,9 +83,19 @@ "width": 50, "height": 26 }, + "common-dropbox": { + "body": "", + "width": 24, + "height": 24 + }, "common-enter-key": { "body": "" }, + "common-firecrawl": { + "body": "", + "width": 28, + "height": 40 + }, "common-gdpr": { "body": "", "width": 23, @@ -96,6 +106,11 @@ "width": 18, "height": 18 }, + "common-google-drive": { + "body": "", + "width": 24, + "height": 24 + }, "common-highlight": { "body": "", "width": 46, diff --git a/packages/iconify-collections/custom-public/info.json b/packages/iconify-collections/custom-public/info.json index c25fc1912aa..d283dc036f5 100644 --- a/packages/iconify-collections/custom-public/info.json +++ b/packages/iconify-collections/custom-public/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-public", "name": "Dify Custom Public", - "total": 147, + "total": 150, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", diff --git a/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/sources/new/page.tsx b/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/sources/new/page.tsx index a3e3fd4e011..02c943f2e1e 100644 --- a/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/sources/new/page.tsx +++ b/web/app/(commonLayout)/datasets/new/[knowledgeSpaceId]/sources/new/page.tsx @@ -1,7 +1,24 @@ import { AddSourcePage } from '@/features/new-rag/add-source-page' +import { singleSearchParam } from '@/features/new-rag/routes' -export default async function Page({ params }: { params: Promise<{ knowledgeSpaceId: string }> }) { +export default async function Page({ + params, + searchParams, +}: { + params: Promise<{ knowledgeSpaceId: string }> + searchParams: Promise<{ + draft?: string | string[] + type?: string | string[] + }> +}) { const { knowledgeSpaceId } = await params + const { draft, type } = await searchParams - return + return ( + + ) } diff --git a/web/features/new-rag/__tests__/add-source-page.spec.tsx b/web/features/new-rag/__tests__/add-source-page.spec.tsx index 668595c9f71..6e2b545752a 100644 --- a/web/features/new-rag/__tests__/add-source-page.spec.tsx +++ b/web/features/new-rag/__tests__/add-source-page.spec.tsx @@ -2,10 +2,19 @@ import type { GetKnowledgeSpacesByIdSourceConnectionsResponse, GetSourceProvidersResponse, } from '@dify/contracts/knowledge-fs/types.gen' -import { act, screen, waitFor, within } from '@testing-library/react' +import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { StrictMode } from 'react' import { render } from '@/test/console/render' import { AddSourcePage } from '../add-source-page' +import { newKnowledgeSourceDraftStorageKey } from '../routes' + +const routerMock = vi.hoisted(() => ({ + push: vi.fn(), + replace: vi.fn(), +})) + +vi.mock('@/next/navigation', () => ({ useRouter: () => routerMock })) const toastInfoMock = vi.hoisted(() => vi.fn()) @@ -18,6 +27,7 @@ type ConnectionsInfiniteData = { } type ConnectionsInfiniteOptions = { + enabled?: boolean getNextPageParam: ( lastPage: GetKnowledgeSpacesByIdSourceConnectionsResponse, ) => string | undefined @@ -45,37 +55,41 @@ const queryState = vi.hoisted(() => ({ })) const clientMock = vi.hoisted(() => ({ - cancelRun: vi.fn(), createConnection: vi.fn(), - createSource: vi.fn(), - deleteConnection: vi.fn(), - deleteSource: vi.fn(), - getPages: vi.fn(), - getRun: vi.fn(), - getSource: vi.fn(), - listSources: vi.fn(), refreshConnection: vi.fn(), - retryRun: vi.fn(), - startPreview: vi.fn(), })) -const routerMock = vi.hoisted(() => ({ replace: vi.fn() })) - const queryClientMock = vi.hoisted(() => ({ invalidateQueries: vi.fn(), })) -const providerQueryOptionsMock = vi.hoisted(() => vi.fn(() => ({ queryKey: ['source-providers'] }))) -const connectionInfiniteOptionsMock = vi.hoisted(() => - vi.fn((_options: ConnectionsInfiniteOptions) => ({ queryKey: ['source-connections'] })), +const providerQueryOptionsMock = vi.hoisted(() => + vi.fn((options: { enabled?: boolean }) => ({ + enabled: options.enabled, + queryKey: ['source-providers'], + })), ) +const connectionInfiniteOptionsMock = vi.hoisted(() => + vi.fn((options: ConnectionsInfiniteOptions) => ({ + enabled: options.enabled, + queryKey: ['source-connections'], + })), +) +const providerHookOptionsMock = vi.hoisted(() => vi.fn()) +const connectionHookOptionsMock = vi.hoisted(() => vi.fn()) vi.mock('@tanstack/react-query', async (importOriginal) => { const original = await importOriginal() return { ...original, - useInfiniteQuery: () => queryState.connections, - useQuery: () => queryState.providers, + useInfiniteQuery: (options: unknown) => { + connectionHookOptionsMock(options) + return queryState.connections + }, + useQuery: (options: unknown) => { + providerHookOptionsMock(options) + return queryState.providers + }, useQueryClient: () => queryClientMock, } }) @@ -83,18 +97,8 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { vi.mock('@/service/client', () => ({ consoleClient: { knowledgeFs: { - deleteKnowledgeSpacesByIdSourceConnectionsByConnectionId: clientMock.deleteConnection, - deleteKnowledgeSpacesByIdSourcesBySourceId: clientMock.deleteSource, - getKnowledgeSpacesByIdSources: clientMock.listSources, - getKnowledgeSpacesByIdSourcesBySourceId: clientMock.getSource, - getKnowledgeSpacesByIdSourceWorkflowsByRunId: clientMock.getRun, - getKnowledgeSpacesByIdSourceWorkflowsByRunIdPages: clientMock.getPages, postKnowledgeSpacesByIdSourceConnections: clientMock.createConnection, postKnowledgeSpacesByIdSourceConnectionsByConnectionIdRefresh: clientMock.refreshConnection, - postKnowledgeSpacesByIdSources: clientMock.createSource, - postKnowledgeSpacesByIdSourcesBySourceIdCrawlPreview: clientMock.startPreview, - postKnowledgeSpacesByIdSourceWorkflowsByRunIdCancel: clientMock.cancelRun, - postKnowledgeSpacesByIdSourceWorkflowsByRunIdRetry: clientMock.retryRun, }, }, consoleQuery: { @@ -110,10 +114,6 @@ vi.mock('@/service/client', () => ({ }, })) -vi.mock('@/next/navigation', () => ({ - useRouter: () => routerMock, -})) - const firecrawlProvider: GetSourceProvidersResponse['items'][number] = { authKinds: ['api-key', 'endpoint'], available: true, @@ -148,14 +148,6 @@ const firecrawlProvider: GetSourceProvidersResponse['items'][number] = { secret: true, type: 'string', }, - { - description: 'Generic bearer token', - format: 'password', - name: 'token', - required: false, - secret: true, - type: 'string', - }, { description: 'Self-hosted endpoint', format: 'uri', @@ -165,32 +157,13 @@ const firecrawlProvider: GetSourceProvidersResponse['items'][number] = { type: 'string', }, ], - displayName: 'Firecrawl', + displayName: 'Plugin daemon website crawl', id: 'plugin-daemon-website', } -const jinaProvider: GetSourceProvidersResponse['items'][number] = { - authKinds: ['api-key'], - available: true, - capabilities: ['website-crawl'], - configuration: [ - { - description: 'Jina API key', - format: 'password', - name: 'apiKey', - required: true, - secret: true, - type: 'string', - }, - ], - displayName: 'Jina Reader', - id: 'jina-reader', -} - const connection = ( status: 'provisioning' | 'active' | 'expired' | 'error' | 'revoked', version = 2, - overrides: Partial = {}, ) => ({ authKind: 'api-key' as const, configuration: { @@ -207,54 +180,17 @@ const connection = ( status, updatedAt: '2026-07-20T10:00:00Z', version, - ...overrides, }) -const provisionalSource = { - connectionId: 'connection-1', - createdAt: '2026-07-20T10:00:00Z', - id: 'source-1', - knowledgeSpaceId: 'space-1', - metadata: { preview: true }, - name: 'Dify docs', - status: 'disabled' as const, - type: 'web' as const, - updatedAt: '2026-07-20T10:00:00Z', - uri: 'https://docs.dify.ai/', - version: 1, -} - -const previewRun = { - checkpoint: 'queued', - createdAt: '2026-07-20T10:00:00Z', - executionAttempts: 1, - id: 'run-1', - knowledgeSpaceId: 'space-1', - kind: 'crawl-preview', - maxExecutionAttempts: 3, - progressCompleted: 0, - progressFailed: 0, - progressSkipped: 0, - sourceId: 'source-1', - state: 'running', - updatedAt: '2026-07-20T10:00:00Z', -} - -function deferred() { - let resolve!: (value: T) => void - const promise = new Promise((nextResolve) => { - resolve = nextResolve - }) - return { promise, resolve } -} - describe('AddSourcePage', () => { beforeEach(() => { vi.clearAllMocks() - for (const mock of Object.values(clientMock)) mock.mockReset() + globalThis.sessionStorage.clear() + clientMock.createConnection.mockReset() + clientMock.refreshConnection.mockReset() queryState.connections.refetch.mockReset() queryState.providers.refetch.mockReset() - queryState.providers.data = { items: [firecrawlProvider, jinaProvider] } + queryState.providers.data = { items: [firecrawlProvider] } queryState.providers.error = null queryState.providers.isPending = false queryState.connections.data = { pages: [{ items: [] }] } @@ -263,13 +199,10 @@ describe('AddSourcePage', () => { queryState.connections.isFetchNextPageError = false queryState.connections.isFetchingNextPage = false queryState.connections.isPending = false - clientMock.createSource.mockResolvedValue(provisionalSource) - clientMock.deleteSource.mockResolvedValue({ id: 'deletion-1' }) - clientMock.getPages.mockResolvedValue({ items: [] }) - clientMock.getRun.mockResolvedValue({ ...previewRun, state: 'preview_ready' }) - clientMock.getSource.mockResolvedValue(provisionalSource) - clientMock.listSources.mockResolvedValue({ items: [] }) - clientMock.startPreview.mockResolvedValue(previewRun) + }) + + afterEach(() => { + globalThis.sessionStorage.clear() }) it('loads the provider catalog and every scoped connection cursor page', () => { @@ -279,6 +212,7 @@ describe('AddSourcePage', () => { expect(providerQueryOptionsMock).toHaveBeenCalledWith({ context: { silent: true }, + enabled: true, input: {}, retry: false, }) @@ -303,6 +237,32 @@ describe('AddSourcePage', () => { await waitFor(() => expect(queryState.connections.fetchNextPage).toHaveBeenCalledOnce()) }) + it('stops website connection pagination after switching source type', async () => { + const user = userEvent.setup() + + render() + queryState.connections.hasNextPage = true + + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) + + expect(queryState.connections.fetchNextPage).not.toHaveBeenCalled() + }) + + it.each(['onlineDocuments', 'onlineDrive'])( + 'does not request website provider data for the %s state', + (initialSourceType) => { + queryState.providers.isPending = true + queryState.connections.isPending = true + + render() + + expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) + expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) + expect(screen.queryByRole('status', { name: 'common.loading' })).not.toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.providerUnavailable')).toBeInTheDocument() + }, + ) + it('stops automatic connection pagination after a cursor error', () => { queryState.connections.error = new Error('next page failed') queryState.connections.hasNextPage = true @@ -324,7 +284,244 @@ describe('AddSourcePage', () => { expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toBeEnabled() expect( screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' }), + ).toBeDisabled() + }) + + it('offers replacement setup when the saved provider connection is revoked', () => { + queryState.connections.data = { pages: [{ items: [connection('revoked')] }] } + + render() + + expect( + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).toBeEnabled() + expect(screen.queryByRole('button', { name: 'common.operation.retry' })).not.toBeInTheDocument() + }) + + it('restores a website draft handed off by the creation flow', async () => { + const user = userEvent.setup() + queryState.connections.data = { pages: [{ items: [connection('active')] }] } + + render( + , + ) + + expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toHaveValue( + 'https://docs.dify.ai', + ) + expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( + 'Dify docs', + ) + await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.crawlOptions/ })) + expect( + screen.getByRole('checkbox', { name: 'dataset.newKnowledge.includeSubpages' }), + ).not.toBeChecked() + expect(screen.getByRole('spinbutton', { name: 'dataset.newKnowledge.maxPages' })).toHaveValue( + 25, + ) + }) + + it('keeps the exact website provider selected and lets the user switch to Firecrawl', async () => { + const user = userEvent.setup() + render( + , + ) + + expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeChecked() + expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) + expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: false }) + expect(screen.getByText('dataset.newKnowledge.providerUnavailable')).toBeInTheDocument() + await user.click(screen.getByRole('radio', { name: 'Firecrawl' })) + expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() + expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true }) + expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true }) + }) + + it('clears a website dependency alert when the provider changes', async () => { + const user = userEvent.setup() + render( + , + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + expect(screen.getByRole('alert')).toBeInTheDocument() + + await user.click(screen.getByRole('radio', { name: 'Jina Reader' })) + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }) + + it('restores online document configuration and reaches an honest backend boundary', async () => { + const user = userEvent.setup() + render( + , + ) + + expect(screen.getByRole('radio', { name: 'Google Docs' })).toBeChecked() + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toHaveValue( + 'Shared product docs', + ) + expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toHaveValue( + 'daily', + ) + const addSource = screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' }) + expect(addSource).toBeEnabled() + await user.click(addSource) + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.sourceSetupBackendDependency', + ) + }) + + it('restores a website draft from session storage', async () => { + queryState.connections.data = { pages: [{ items: [connection('active')] }] } + const storageKey = newKnowledgeSourceDraftStorageKey('draft-1') + globalThis.sessionStorage.setItem( + storageKey, + JSON.stringify({ + includeSubpages: false, + maxPages: 25, + provider: 'Firecrawl', + rootUrl: 'https://docs.dify.ai', + sourceName: 'Dify docs', + sourceType: 'websiteCrawl', + syncPolicy: 'provider', + }), + ) + + render() + + expect( + await screen.findByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), + ).toHaveValue('https://docs.dify.ai') + expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( + 'Dify docs', + ) + expect(globalThis.sessionStorage.getItem(storageKey)).not.toBeNull() + }) + + it('retains the draft through Strict Mode, delayed loading, and a real remount', async () => { + queryState.connections.data = { pages: [{ items: [connection('active')] }] } + queryState.connections.isPending = true + const storageKey = newKnowledgeSourceDraftStorageKey('strict-draft') + globalThis.sessionStorage.setItem( + storageKey, + JSON.stringify({ + includeSubpages: true, + maxPages: 100, + provider: 'Firecrawl', + rootUrl: 'https://docs.dify.ai/strict', + sourceName: 'Strict docs', + sourceType: 'websiteCrawl', + syncPolicy: 'provider', + }), + ) + + const view = render( + + + , + ) + await waitFor(() => expect(globalThis.sessionStorage.getItem(storageKey)).not.toBeNull()) + + queryState.connections.isPending = false + view.rerender( + + + , + ) + + expect( + await screen.findByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), + ).toHaveValue('https://docs.dify.ai/strict') + expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( + 'Strict docs', + ) + + view.unmount() + queryState.connections.isPending = false + render( + + + , + ) + expect( + await screen.findByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), + ).toHaveValue('https://docs.dify.ai/strict') + }) + + it('clears the stored draft when source setup is canceled', async () => { + const user = userEvent.setup() + const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => undefined) + const storageKey = newKnowledgeSourceDraftStorageKey('cancel-draft') + globalThis.sessionStorage.setItem( + storageKey, + JSON.stringify({ + includeSubpages: true, + maxPages: 100, + provider: 'Firecrawl', + rootUrl: 'https://docs.dify.ai', + sourceName: 'Dify docs', + sourceType: 'websiteCrawl', + syncPolicy: 'provider', + }), + ) + + render() + await user.click( + await screen.findByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' }), + ) + const confirmation = await screen.findByRole('alertdialog', { + name: 'dataset.newKnowledge.discardSourceDraftTitle', + }) + expect(globalThis.sessionStorage.getItem(storageKey)).not.toBeNull() + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardDraftConfirm' }), + ) + + expect(globalThis.sessionStorage.getItem(storageKey)).toBeNull() + expect(historyBack).toHaveBeenCalledOnce() + act(() => window.dispatchEvent(new PopStateEvent('popstate'))) + expect(routerMock.replace).toHaveBeenCalledWith('/datasets/new/space-1/sources') + expect(confirmation).not.toBeInTheDocument() }) it('creates the exact Firecrawl provider connection without leaking credentials', async () => { @@ -333,12 +530,11 @@ describe('AddSourcePage', () => { render() await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'secret-value') - expect(screen.getByLabelText('Token')).toBeInTheDocument() - expect(screen.getByLabelText('Endpoint')).toBeInTheDocument() - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.connectProvider/ })) + await user.type(screen.getByLabelText('Endpoint'), 'https://crawl.example.com') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) await waitFor(() => expect(clientMock.createConnection).toHaveBeenCalledWith({ @@ -346,6 +542,7 @@ describe('AddSourcePage', () => { authKind: 'api-key', configuration: { datasource: 'crawl', + endpoint: 'https://crawl.example.com', pluginId: 'langgenius/firecrawl_datasource', provider: 'firecrawl', }, @@ -356,6 +553,8 @@ describe('AddSourcePage', () => { params: { id: 'space-1' }, }), ) + await screen.findByRole('status', { name: 'appApi.loading' }) + act(() => window.dispatchEvent(new PopStateEvent('popstate'))) expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ queryKey: ['source-connections'], }) @@ -363,82 +562,108 @@ describe('AddSourcePage', () => { expect(screen.queryByDisplayValue('secret-value')).not.toBeInTheDocument() }) - it('renders the provider catalog returned by the API without invented entries', () => { + it('releases the parent history guard before the crawl preview owns navigation', async () => { + const user = userEvent.setup() + const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => undefined) + clientMock.createConnection.mockResolvedValue(connection('active')) + + render() + await user.click( + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), + ) + await user.type(screen.getByLabelText(/Api Key/), 'secret-value') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) + + await waitFor(() => expect(historyBack).toHaveBeenCalledOnce()) + expect(screen.queryByText(/dataset\.newKnowledge\.providerConnected/)).not.toBeInTheDocument() + + act(() => window.dispatchEvent(new PopStateEvent('popstate'))) + await screen.findByText(/dataset\.newKnowledge\.providerConnected/) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + + expect(historyBack).toHaveBeenCalledOnce() + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources') + }) + + it('keeps the website setup interactive until it reaches the backend boundary', async () => { + const user = userEvent.setup() + render( + , + ) + + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' })).toHaveValue( + 'https://docs.dify.ai', + ) + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toHaveValue( + 'Dify docs', + ) + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' })).toBeEnabled() + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() + const crawlAndPreview = screen.getByRole('button', { + name: 'dataset.newKnowledge.crawlAndPreview', + }) + expect(crawlAndPreview).toBeEnabled() + await user.click(crawlAndPreview) + expect(screen.getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.sourceSetupBackendDependency', + ) + }) + + it('keeps pending website edits when the provider connection becomes active', async () => { + const user = userEvent.setup() + const initialSourceDraft = { + includeSubpages: true, + maxPages: 100, + provider: 'Firecrawl' as const, + rootUrl: 'https://docs.dify.ai', + sourceName: 'Dify docs', + sourceType: 'websiteCrawl' as const, + syncPolicy: 'provider' as const, + } + const view = render( + , + ) + const rootUrl = screen.getByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' }) + const sourceName = screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }) + await user.clear(rootUrl) + await user.type(rootUrl, 'https://docs.dify.ai/edited') + await user.clear(sourceName) + await user.type(sourceName, 'Edited docs') + + queryState.connections.data = { pages: [{ items: [connection('active')] }] } + view.rerender( + , + ) + await screen.findByRole('status', { name: 'appApi.loading' }) + act(() => window.dispatchEvent(new PopStateEvent('popstate'))) + + expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toHaveValue( + 'https://docs.dify.ai/edited', + ) + expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue( + 'Edited docs', + ) + }) + + it('does not select a lookalike provider by fuzzy display name', () => { queryState.providers.data = { - items: [{ ...jinaProvider, displayName: 'Workspace Reader' }], + items: [{ ...firecrawlProvider, displayName: 'Firecrawl impostor', id: 'impostor' }], } render() - expect(screen.getByRole('radio', { name: 'Workspace Reader' })).toBeChecked() - expect(screen.queryByRole('radio', { name: 'Firecrawl' })).not.toBeInTheDocument() - expect(screen.queryByRole('radio', { name: 'WaterCrawl' })).not.toBeInTheDocument() - }) - - it('keeps every installed provider selectable when the preferred provider is returned later', () => { - queryState.providers.data = { - items: [ - ...Array.from({ length: 4 }, (_, index) => ({ - ...jinaProvider, - displayName: `Provider ${index + 1}`, - id: `provider-${index + 1}`, - })), - firecrawlProvider, - ], - } - - render() - - expect(screen.getByRole('radio', { name: 'Provider 4' })).toBeInTheDocument() - expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() - }) - - it('switches providers and creates a connection from the selected catalog schema', async () => { - const user = userEvent.setup() - clientMock.createConnection.mockResolvedValue( - connection('active', 1, { - id: 'jina-connection', - name: 'Jina Reader', - providerId: 'jina-reader', - }), - ) - - render() - await user.click(screen.getByRole('radio', { name: 'Jina Reader' })) - await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ) - await user.type(screen.getByLabelText(/Api Key/), 'jina-secret') - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.connectProvider/ })) - - await waitFor(() => - expect(clientMock.createConnection).toHaveBeenCalledWith({ - body: { - authKind: 'api-key', - configuration: {}, - credentials: { apiKey: 'jina-secret' }, - name: 'Jina Reader', - providerId: 'jina-reader', - }, - params: { id: 'space-1' }, - }), - ) - }) - - it('clears the configuration form when the selected provider changes', async () => { - const user = userEvent.setup() - - render() - await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ) - await user.type(screen.getByLabelText(/Api Key/), 'firecrawl-secret') - await user.click(screen.getByRole('radio', { name: 'Jina Reader' })) - - expect( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ).toBeInTheDocument() - expect(screen.queryByDisplayValue('firecrawl-secret')).not.toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.firecrawlUnavailable')).toBeInTheDocument() }) it('clears sensitive input but retains non-sensitive input after a connection error', async () => { @@ -448,17 +673,15 @@ describe('AddSourcePage', () => { render() await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'do-not-retain') - await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.endpoint' })) await user.type(screen.getByLabelText('Endpoint'), 'https://crawl.example.com') - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.connectProvider/ })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) - expect(await screen.findByText(/dataset\.newKnowledge\.connectionFailed/)).toBeInTheDocument() - expect(screen.getByLabelText('Endpoint')).toHaveValue('https://crawl.example.com') - await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.api-key' })) + expect(await screen.findByText('dataset.newKnowledge.connectionFailed')).toBeInTheDocument() expect(screen.getByLabelText(/Api Key/)).toHaveValue('') + expect(screen.getByLabelText('Endpoint')).toHaveValue('https://crawl.example.com') }) it('reconciles a response-lost create before showing an error', async () => { @@ -470,32 +693,16 @@ describe('AddSourcePage', () => { render() await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'secret-value') - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.connectProvider/ })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) + await waitFor(() => expect(clientMock.createConnection).toHaveBeenCalledOnce()) + await screen.findByRole('status', { name: 'appApi.loading' }) + act(() => window.dispatchEvent(new PopStateEvent('popstate'))) expect(await screen.findByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() - expect(screen.queryByText(/dataset\.newKnowledge\.connectionFailed/)).not.toBeInTheDocument() - }) - - it('retries response-lost create reconciliation while the connection replica catches up', async () => { - const user = userEvent.setup() - clientMock.createConnection.mockRejectedValue(new Error('response lost')) - queryState.connections.refetch - .mockResolvedValueOnce({ data: { pages: [{ items: [] }] } }) - .mockResolvedValueOnce({ data: { pages: [{ items: [connection('active')] }] } }) - - render() - await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ) - await user.type(screen.getByLabelText(/Api Key/), 'secret-value') - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.connectProvider/ })) - - expect(await screen.findByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() - expect(queryState.connections.refetch).toHaveBeenCalledTimes(2) - expect(screen.queryByText(/dataset\.newKnowledge\.connectionFailed/)).not.toBeInTheDocument() + expect(screen.queryByText('dataset.newKnowledge.connectionFailed')).not.toBeInTheDocument() }) it('clears an API key when authentication modes are changed and changed back', async () => { @@ -504,7 +711,7 @@ describe('AddSourcePage', () => { render() await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'must-not-return') await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.endpoint' })) @@ -515,34 +722,19 @@ describe('AddSourcePage', () => { it('supports an endpoint descriptor without sending a hidden secret field', async () => { const user = userEvent.setup() - clientMock.createConnection.mockResolvedValue({ ...connection('active'), authKind: 'endpoint' }) - queryState.providers.data = { - items: [ - { - ...firecrawlProvider, - configuration: [ - ...firecrawlProvider.configuration, - { - description: 'Deployment region', - name: 'region', - required: true, - secret: false, - type: 'string', - }, - ], - }, - ], - } + clientMock.createConnection.mockResolvedValue({ + ...connection('active'), + authKind: 'endpoint', + }) render() await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) await user.type(screen.getByLabelText(/Api Key/), 'must-not-be-sent') await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.authKind.endpoint' })) await user.type(screen.getByLabelText('Endpoint'), 'https://crawl.example.com') - await user.type(screen.getByLabelText(/Region/), 'us-east-1') - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.connectProvider/ })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectProvider' })) await waitFor(() => expect(clientMock.createConnection).toHaveBeenCalledWith({ @@ -553,7 +745,6 @@ describe('AddSourcePage', () => { endpoint: 'https://crawl.example.com', pluginId: 'langgenius/firecrawl_datasource', provider: 'firecrawl', - region: 'us-east-1', }, credentials: {}, name: 'Firecrawl', @@ -583,7 +774,7 @@ describe('AddSourcePage', () => { render() await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.getByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ) expect(screen.getByLabelText(/Api Key/)).toHaveAccessibleDescription('Firecrawl API key') @@ -608,39 +799,6 @@ describe('AddSourcePage', () => { expect(screen.getByText(/dataset\.newKnowledge\.providerConnected/)).toBeInTheDocument() }) - it('revokes an invalid connection before offering a clean reconfiguration form', async () => { - const user = userEvent.setup() - queryState.connections.data = { pages: [{ items: [connection('expired')] }] } - clientMock.deleteConnection.mockResolvedValue(connection('revoked', 3)) - - render() - await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ) - - await waitFor(() => - expect(clientMock.deleteConnection).toHaveBeenCalledWith({ - params: { connectionId: 'connection-1', id: 'space-1' }, - query: { expectedVersion: 2 }, - }), - ) - await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ) - expect(screen.getByLabelText(/Api Key/)).toHaveValue('') - }) - - it('treats a revoked connection as available for clean configuration', () => { - queryState.connections.data = { pages: [{ items: [connection('revoked')] }] } - - render() - - expect( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ).toBeInTheDocument() - expect(screen.queryByRole('button', { name: 'common.operation.retry' })).not.toBeInTheDocument() - }) - it('reconciles a refresh version race and retries with the server version', async () => { const user = userEvent.setup() queryState.connections.data = { pages: [{ items: [connection('error')] }] } @@ -708,7 +866,7 @@ describe('AddSourcePage', () => { const view = render() await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) expect( - await screen.findByText(/dataset\.newKnowledge\.connectionProvisioning/), + await screen.findByText('dataset.newKnowledge.connectionProvisioning'), ).toBeInTheDocument() queryState.connections.data = { @@ -781,7 +939,7 @@ describe('AddSourcePage', () => { 'dataset.newKnowledge.connectionRefreshFailed', ) expect( - screen.queryByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.queryByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).not.toBeInTheDocument() }) @@ -795,26 +953,45 @@ describe('AddSourcePage', () => { }) expect(onlineDocuments).toBeEnabled() expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })).toBeEnabled() + expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeEnabled() expect( - screen.getByRole('group', { name: 'dataset.newKnowledge.providerLabel' }), + screen.getByRole('group', { name: 'datasetCreation.stepOne.website.chooseProvider' }), ).toBeInTheDocument() expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() - expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeEnabled() - expect(screen.queryByRole('radio', { name: 'WaterCrawl' })).not.toBeInTheDocument() - expect(screen.queryByRole('radio', { name: 'FakeCrawler' })).not.toBeInTheDocument() - expect( - screen.getByRole('link', { name: 'dataset.newKnowledge.moreProviders' }), - ).toHaveAttribute('href', '/marketplace?category=datasource') await user.click(onlineDocuments) expect(onlineDocuments).toBeChecked() expect( - screen.queryByRole('group', { name: 'datasetCreation.stepOne.website.chooseProvider' }), - ).not.toBeInTheDocument() + screen.getByRole('group', { name: 'datasetCreation.stepOne.website.chooseProvider' }), + ).toBeVisible() + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() + expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toBeEnabled() + expect(screen.getByRole('status')).toHaveTextContent('dataset.newKnowledge.providerUnavailable') + await user.type( + screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }), + 'Product docs', + ) + + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })) + expect(screen.getByRole('radio', { name: 'Google Drive' })).toBeChecked() + await user.click(onlineDocuments) + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toHaveValue( + 'Product docs', + ) + }) + + it('restores the selected source type from the create flow', () => { + render() + + expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })).toBeChecked() + expect( + screen.getByRole('region', { name: 'dataset.newKnowledge.selectFilesAndFolders' }), + ).toBeVisible() + expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled() expect(screen.getByRole('status')).toHaveTextContent('dataset.newKnowledge.providerUnavailable') }) - it('keeps the final Add source action disabled until a crawl result is ready', async () => { + it('disables the final Add source action while its backend dependency is missing', async () => { const user = userEvent.setup() render() @@ -824,265 +1001,6 @@ describe('AddSourcePage', () => { expect(toastInfoMock).not.toHaveBeenCalled() }) - it('asks before discarding an edited source draft', async () => { - const user = userEvent.setup() - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) - - const dialog = await screen.findByRole('alertdialog', { - name: 'dataset.newKnowledge.discardSourceDraftTitle', - }) - expect(dialog).toHaveTextContent('dataset.newKnowledge.discardSourceDraftDescription') - await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' })) - expect(routerMock.replace).not.toHaveBeenCalled() - }) - - it('asks before discarding provider connection credentials', async () => { - const user = userEvent.setup() - - render() - await user.click( - screen.getByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), - ) - await user.type(screen.getByLabelText(/Api Key/), 'unsaved-secret') - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) - - expect( - await screen.findByRole('alertdialog', { - name: 'dataset.newKnowledge.discardSourceDraftTitle', - }), - ).toBeInTheDocument() - expect(routerMock.replace).not.toHaveBeenCalled() - }) - - it('guards client-side marketplace navigation while the source draft is dirty', async () => { - const user = userEvent.setup() - const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => undefined) - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.click(screen.getByRole('link', { name: /dataset\.newKnowledge\.moreProviders/ })) - - const dialog = await screen.findByRole('alertdialog', { - name: 'dataset.newKnowledge.discardSourceDraftTitle', - }) - expect(routerMock.replace).not.toHaveBeenCalled() - await user.click( - within(dialog).getByRole('button', { name: 'dataset.newKnowledge.discardDraftConfirm' }), - ) - await waitFor(() => expect(historyBack).toHaveBeenCalledOnce()) - - act(() => window.dispatchEvent(new PopStateEvent('popstate'))) - expect(routerMock.replace).toHaveBeenCalledWith('/marketplace?category=datasource') - }) - - it('guards client-side navigation from links outside the add source form', async () => { - const user = userEvent.setup() - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - - render( - <> - Documents - - , - ) - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.click(screen.getByRole('link', { name: 'Documents' })) - - expect( - await screen.findByRole('alertdialog', { - name: 'dataset.newKnowledge.discardSourceDraftTitle', - }), - ).toBeInTheDocument() - expect(routerMock.replace).not.toHaveBeenCalled() - }) - - it('waits for source creation before deleting the provisional source on discard', async () => { - const user = userEvent.setup() - const sourceRequest = deferred() - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - clientMock.createSource.mockReturnValue(sourceRequest.promise) - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ }), - 'Dify docs', - ) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledOnce()) - - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) - const dialog = await screen.findByRole('alertdialog', { - name: 'dataset.newKnowledge.discardSourceDraftTitle', - }) - await user.click( - within(dialog).getByRole('button', { name: 'dataset.newKnowledge.discardDraftConfirm' }), - ) - expect(clientMock.deleteSource).not.toHaveBeenCalled() - - await act(async () => sourceRequest.resolve(provisionalSource)) - - await waitFor(() => expect(clientMock.deleteSource).toHaveBeenCalledOnce()) - }) - - it('starts with a fresh preview draft after switching active providers', async () => { - const user = userEvent.setup() - const jinaConnection = connection('active', 2, { - configuration: { provider: 'jina' }, - id: 'jina-connection', - name: 'Jina Reader', - providerId: 'jina-reader', - }) - const jinaSource = { - ...provisionalSource, - connectionId: jinaConnection.id, - id: 'source-2', - } - queryState.connections.data = { - pages: [{ items: [connection('active'), jinaConnection] }], - } - clientMock.createSource - .mockResolvedValueOnce(provisionalSource) - .mockResolvedValueOnce(jinaSource) - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ }), - 'Dify docs', - ) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - await screen.findByText(/^dataset\.newKnowledge\.noPagesFound:/) - - await user.click(screen.getByRole('radio', { name: 'Jina Reader' })) - - expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toHaveValue('') - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://jina.ai', - ) - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ }), - 'Jina docs', - ) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - - await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledTimes(2)) - expect(clientMock.createSource.mock.calls[1]?.[0].body.connectionId).toBe('jina-connection') - }) - - it('reconciles a response-lost provisional source deletion before leaving', async () => { - const user = userEvent.setup() - const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => undefined) - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - clientMock.deleteSource.mockRejectedValue(new Error('response lost')) - clientMock.getSource.mockRejectedValue( - Object.assign(new Error('source not found'), { status: 404 }), - ) - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ }), - 'Dify docs', - ) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledOnce()) - - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) - const dialog = await screen.findByRole('alertdialog', { - name: 'dataset.newKnowledge.discardSourceDraftTitle', - }) - await user.click( - within(dialog).getByRole('button', { name: 'dataset.newKnowledge.discardDraftConfirm' }), - ) - - await waitFor(() => - expect(clientMock.deleteSource).toHaveBeenCalledWith({ - body: { expectedRevision: 1 }, - headers: { 'idempotency-key': expect.any(String) }, - params: { id: 'space-1', sourceId: 'source-1' }, - query: { documents: 'cascade' }, - }), - ) - expect(clientMock.getSource).toHaveBeenCalledWith({ - params: { id: 'space-1', sourceId: 'source-1' }, - }) - expect(historyBack).toHaveBeenCalledOnce() - act(() => window.dispatchEvent(new PopStateEvent('popstate'))) - expect(routerMock.replace).toHaveBeenCalledWith('/datasets/new/space-1/sources') - }) - - it('protects an edited source draft from browser unload', async () => { - const user = userEvent.setup() - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - const event = new Event('beforeunload', { cancelable: true }) - - act(() => window.dispatchEvent(event)) - - expect(event.defaultPrevented).toBe(true) - }) - - it('deletes a known provisional source with keepalive when the page unloads', async () => { - const user = userEvent.setup() - queryState.connections.data = { pages: [{ items: [connection('active')] }] } - - render() - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ }), - 'https://docs.dify.ai', - ) - await user.type( - screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ }), - 'Dify docs', - ) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledOnce()) - - act(() => window.dispatchEvent(new Event('pagehide'))) - - await waitFor(() => - expect(clientMock.deleteSource).toHaveBeenCalledWith( - { - body: { expectedRevision: 1 }, - headers: { 'idempotency-key': expect.any(String) }, - params: { id: 'space-1', sourceId: 'source-1' }, - query: { documents: 'cascade' }, - }, - { context: { keepalive: true, silent: true } }, - ), - ) - }) - it('shows catalog unavailability instead of offering a fake connection', () => { queryState.providers.data = { items: [{ ...firecrawlProvider, available: false, unavailableReason: 'Disabled by admin' }], @@ -1092,7 +1010,7 @@ describe('AddSourcePage', () => { expect(screen.getByText('Disabled by admin')).toBeInTheDocument() expect( - screen.queryByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.queryByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).not.toBeInTheDocument() }) @@ -1111,7 +1029,7 @@ describe('AddSourcePage', () => { expect(screen.getByText('dataset.newKnowledge.providerUnavailable')).toBeInTheDocument() expect( - screen.queryByRole('button', { name: /dataset\.newKnowledge\.configureProvider/ }), + screen.queryByRole('button', { name: /^dataset\.newKnowledge\.configureProvider/ }), ).not.toBeInTheDocument() }) }) diff --git a/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx b/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx new file mode 100644 index 00000000000..a878aa38f7a --- /dev/null +++ b/web/features/new-rag/__tests__/crawl-selection-form.spec.tsx @@ -0,0 +1,723 @@ +import type { + GetKnowledgeSpacesByIdSourcesBySourceIdSyncPolicyResponse, + GetKnowledgeSpacesByIdSourceWorkflowsByRunIdPagesResponse, + Source, + SourceWorkflowRun, +} from '@dify/contracts/knowledge-fs/types.gen' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { act, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { createInstance } from 'i18next' +import { getInitOptions } from '@/i18n-config/settings' +import datasetTranslations from '@/i18n/en-US/dataset.json' +import { render } from '@/test/console/render' +import { CrawlSelectionForm } from '../crawl-selection-form' + +const clientMock = vi.hoisted(() => ({ + getPolicy: vi.fn(), + getWorkflow: vi.fn(), + selectPages: vi.fn(), + updatePolicy: vi.fn(), +})) + +const routerMock = vi.hoisted(() => ({ push: vi.fn() })) +const queryClientMock = vi.hoisted(() => ({ invalidateQueries: vi.fn() })) +const policyQueryOptionsMock = vi.hoisted(() => + vi.fn(({ input }) => ({ + queryFn: () => clientMock.getPolicy(input), + queryKey: ['sync-policy', input.params.sourceId], + retry: false, + })), +) + +vi.mock('@/next/navigation', () => ({ useRouter: () => routerMock })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const original = await importOriginal() + return { ...original, useQueryClient: () => queryClientMock } +}) + +vi.mock('@/service/client', () => ({ + consoleClient: { + knowledgeFs: { + getKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy: clientMock.getPolicy, + getKnowledgeSpacesByIdSourceWorkflowsByRunId: clientMock.getWorkflow, + }, + }, + consoleQuery: { + knowledgeFs: { + getKnowledgeSpacesByIdSources: { + key: vi.fn(() => ['knowledge-sources']), + }, + getKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy: { + queryOptions: policyQueryOptionsMock, + }, + postKnowledgeSpacesByIdSourceWorkflowsByRunIdSelection: { + mutationOptions: vi.fn(() => ({ + mutationFn: (input: unknown) => clientMock.selectPages(input), + })), + }, + putKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy: { + mutationOptions: vi.fn(() => ({ + mutationFn: (input: unknown) => clientMock.updatePolicy(input), + })), + }, + }, + }, +})) + +type PreviewPage = GetKnowledgeSpacesByIdSourceWorkflowsByRunIdPagesResponse['items'][number] + +const source: Source = { + connectionId: 'connection-1', + createdAt: '2026-07-20T10:00:00Z', + id: 'source-1', + knowledgeSpaceId: 'space-1', + metadata: {}, + name: 'Dify docs', + status: 'disabled', + type: 'web', + updatedAt: '2026-07-20T10:00:00Z', + uri: 'https://docs.dify.ai/', + version: 3, +} + +const run: SourceWorkflowRun = { + checkpoint: 'crawl', + createdAt: '2026-07-20T10:00:00Z', + executionAttempts: 1, + id: 'run-1', + knowledgeSpaceId: 'space-1', + kind: 'crawl-preview', + maxExecutionAttempts: 3, + progressCompleted: 3, + progressFailed: 1, + progressSkipped: 0, + progressTotal: 4, + sourceId: source.id, + state: 'succeeded', + updatedAt: '2026-07-20T10:01:00Z', +} + +const pages: PreviewPage[] = [ + { + pageId: 'page-1', + sourceUrl: 'https://docs.dify.ai/getting-started', + title: 'Getting started', + }, + { + pageId: 'page-2', + sourceUrl: 'https://docs.dify.ai/guides', + title: 'Guides', + }, + { + pageId: 'off-domain', + sourceUrl: 'https://github.com/langgenius/dify/edit/main/docs.md', + title: 'Edit this page', + }, +] + +const policy = ( + overrides: Partial = {}, +): GetKnowledgeSpacesByIdSourcesBySourceIdSyncPolicyResponse => ({ + createdAt: '2026-07-20T10:00:00Z', + enabled: true, + expectedSourceVersion: 3, + id: 'policy-1', + knowledgeSpaceId: 'space-1', + mode: 'interval', + revision: 2, + sourceId: source.id, + updatedAt: '2026-07-20T10:00:00Z', + ...overrides, +}) + +function deferred() { + let resolve!: (value: T) => void + const promise = new Promise((nextResolve) => { + resolve = nextResolve + }) + return { promise, resolve } +} + +function renderSelectionForm( + onCancel = vi.fn(), + workflowUncertain = false, + discardRequested = () => false, + previewPages = pages, + initialSyncMode?: 'custom' | 'interval' | 'manual' | 'provider', +) { + const queryClient = new QueryClient({ + defaultOptions: { mutations: { retry: false }, queries: { retry: false } }, + }) + const onSubmissionUncertainChange = vi.fn() + const onRecrawl = vi.fn() + const onWorkflowPending = vi.fn() + const onWorkflowRun = vi.fn() + const view = render( + + + , + ) + return { + ...view, + onRecrawl, + onSubmissionUncertainChange, + onWorkflowPending, + onWorkflowRun, + } +} + +describe('CrawlSelectionForm', () => { + beforeEach(() => { + vi.clearAllMocks() + clientMock.getPolicy.mockResolvedValue(policy()) + clientMock.getWorkflow.mockResolvedValue({ ...run, checkpoint: 'complete', state: 'completed' }) + clientMock.updatePolicy.mockResolvedValue(policy({ mode: 'manual', revision: 3 })) + clientMock.selectPages.mockResolvedValue({ ...run, checkpoint: 'import', state: 'queued' }) + }) + + it('uses singular and plural copy for the crawl summary', async () => { + const instance = createInstance() + await instance.init({ + ...getInitOptions(), + defaultNS: 'dataset', + lng: 'en-US', + ns: ['dataset'], + resources: { 'en-US': { dataset: datasetTranslations } }, + }) + const tDataset = instance.getFixedT('en-US', 'dataset') + expect(tDataset(($) => $['newKnowledge.pagesCrawled'], { count: 1, host: 'example.com' })).toBe( + '1 page crawled at example.com', + ) + expect(tDataset(($) => $['newKnowledge.pagesCrawled'], { count: 2, host: 'example.com' })).toBe( + '2 pages crawled at example.com', + ) + }) + + it('loads a missing initial sync policy without a global error notification', async () => { + renderSelectionForm() + await screen.findByRole('checkbox', { name: 'Getting started' }) + + expect(policyQueryOptionsMock).toHaveBeenCalledWith( + expect.objectContaining({ context: { silent: true } }), + ) + }) + + it('preserves the sync policy selected in the creation flow', async () => { + renderSelectionForm(vi.fn(), false, () => false, pages, 'manual') + + expect( + await screen.findByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + ).toHaveValue('manual') + }) + + it('selects only valid same-domain pages and exposes an indeterminate select-all state', async () => { + const user = userEvent.setup() + renderSelectionForm() + + await screen.findByRole('checkbox', { name: 'Getting started' }) + const addSource = screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' }) + expect(addSource).toBeDisabled() + const offDomainPage = screen.getByRole('checkbox', { name: 'Edit this page' }) + expect(offDomainPage).toHaveAttribute('aria-disabled', 'true') + expect(offDomainPage).toHaveAccessibleDescription( + 'https://github.com/langgenius/dify/edit/main/docs.md dataset.newKnowledge.skippedOffDomain', + ) + expect(screen.getByText('dataset.newKnowledge.skippedOffDomain')).toBeInTheDocument() + expect(screen.getByText(/dataset\.newKnowledge\.skippedFailed/)).toBeInTheDocument() + + await user.click(screen.getByRole('checkbox', { name: 'Getting started' })) + expect(addSource).toBeEnabled() + expect( + screen.getByRole('checkbox', { name: 'dataset.newKnowledge.selectAll' }), + ).toHaveAttribute('aria-checked', 'mixed') + expect( + screen.getByRole('checkbox', { name: 'dataset.newKnowledge.selectAll' }), + ).toHaveAttribute('data-indeterminate') + expect(screen.getByText('dataset.newKnowledge.pagesSelected:{"count":1}')).toBeInTheDocument() + + await user.click(screen.getByRole('checkbox', { name: 'dataset.newKnowledge.selectAll' })) + expect(screen.getByRole('checkbox', { name: 'Getting started' })).toBeChecked() + expect(screen.getByRole('checkbox', { name: 'Guides' })).toBeChecked() + expect(screen.getByRole('checkbox', { name: 'Edit this page' })).not.toBeChecked() + }) + + it('toggles selection from the visible checkbox labels', async () => { + const user = userEvent.setup() + renderSelectionForm() + await screen.findByRole('checkbox', { name: 'Getting started' }) + + await user.click(screen.getByText('Getting started')) + expect(screen.getByRole('checkbox', { name: 'Getting started' })).toBeChecked() + await user.click(screen.getByText('dataset.newKnowledge.selectAll')) + + expect(screen.getByRole('checkbox', { name: 'Getting started' })).toBeChecked() + expect(screen.getByRole('checkbox', { name: 'Guides' })).toBeChecked() + }) + + it('locks stale selections while keeping workflow reconciliation available', async () => { + const onCancel = vi.fn() + const user = userEvent.setup() + const { onRecrawl } = renderSelectionForm(onCancel, true) + await screen.findByRole('checkbox', { name: 'Getting started' }) + + expect(screen.getByRole('checkbox', { name: 'Getting started' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })).toBeDisabled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reCrawl' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + + expect(onRecrawl).toHaveBeenCalledOnce() + expect(onCancel).toHaveBeenCalledOnce() + }) + + it('never submits more page IDs than the selection contract accepts', async () => { + const longPageList: PreviewPage[] = Array.from({ length: 201 }, (_, index) => ({ + pageId: `page-${index + 1}`, + sourceUrl: `https://docs.dify.ai/page-${index + 1}`, + title: `Page ${index + 1}`, + })) + const user = userEvent.setup() + renderSelectionForm(vi.fn(), false, () => false, longPageList) + await user.click( + await screen.findByRole('checkbox', { name: 'dataset.newKnowledge.selectAll' }), + ) + + const overflowPage = screen.getByRole('checkbox', { name: 'Page 201' }) + expect(overflowPage).toHaveAttribute('aria-disabled', 'true') + expect(overflowPage).toHaveAccessibleDescription( + 'https://docs.dify.ai/page-201 dataset.newKnowledge.maxPages: 200', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.selectPages).toHaveBeenCalledOnce()) + expect(clientMock.selectPages.mock.calls[0]?.[0].body.pageIds).toHaveLength(200) + }) + + it('validates a custom interval and submits policy plus selection exactly once', async () => { + const selectionRequest = deferred() + clientMock.selectPages.mockReturnValue(selectionRequest.promise) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'custom', + ) + const interval = screen.getByRole('spinbutton', { + name: 'dataset.newKnowledge.customIntervalHours', + }) + await user.clear(interval) + await user.type(interval, '0') + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })).toBeDisabled() + expect(interval).toHaveAccessibleDescription('dataset.newKnowledge.customIntervalInvalid') + await user.clear(interval) + await user.type(interval, '6') + + const addSource = screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' }) + await user.dblClick(addSource) + + await waitFor(() => expect(clientMock.updatePolicy).toHaveBeenCalledOnce()) + expect(clientMock.updatePolicy).toHaveBeenCalledWith({ + body: { + customIntervalSeconds: 21600, + enabled: true, + expectedRevision: 2, + expectedSourceVersion: 3, + mode: 'custom', + }, + params: { id: 'space-1', sourceId: 'source-1' }, + }) + expect(clientMock.selectPages).toHaveBeenCalledOnce() + expect(clientMock.selectPages).toHaveBeenCalledWith({ + body: { pageIds: ['page-1'] }, + headers: { 'Idempotency-Key': expect.any(String) }, + params: { id: 'space-1', runId: 'run-1' }, + }) + + selectionRequest.resolve({ ...run, checkpoint: 'import', state: 'queued' }) + await waitFor(() => expect(clientMock.getWorkflow).toHaveBeenCalledOnce()) + await waitFor(() => + expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ['knowledge-sources'], + }), + ) + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources') + }) + + it.each(['complete', 'success'])( + 'accepts the %s workflow state as a successful import', + async (state) => { + clientMock.selectPages.mockResolvedValue({ ...run, checkpoint: 'complete', state }) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + expect(clientMock.getWorkflow).not.toHaveBeenCalled() + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }, + ) + + it.each(['error', 'exhausted', 'superseded'])( + 'stops polling when the import reaches the %s workflow state', + async (state) => { + clientMock.selectPages.mockResolvedValue({ ...run, checkpoint: 'import', state }) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.addSourceFailed', + ) + expect(clientMock.getWorkflow).not.toHaveBeenCalled() + expect(routerMock.push).not.toHaveBeenCalled() + }, + ) + + it('tracks policy updates and stops before selection when discard is requested', async () => { + const policyRequest = deferred() + clientMock.updatePolicy.mockReturnValue(policyRequest.promise) + let discardRequested = false + const user = userEvent.setup() + const { onWorkflowPending } = renderSelectionForm(vi.fn(), false, () => discardRequested) + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'manual', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + expect(onWorkflowPending).toHaveBeenCalledOnce() + expect(clientMock.selectPages).not.toHaveBeenCalled() + discardRequested = true + await act(async () => + policyRequest.resolve(policy({ enabled: false, mode: 'manual', revision: 3 })), + ) + await expect(onWorkflowPending.mock.calls[0]?.[0]).resolves.toBeUndefined() + expect(clientMock.selectPages).not.toHaveBeenCalled() + }) + + it('releases a pending import for cancellation as soon as the current poll returns', async () => { + const workflowRequest = deferred() + clientMock.getWorkflow.mockReturnValue(workflowRequest.promise) + let discardRequested = false + const user = userEvent.setup() + const { onWorkflowPending } = renderSelectionForm(vi.fn(), false, () => discardRequested) + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.getWorkflow).toHaveBeenCalledOnce()) + expect(onWorkflowPending).toHaveBeenCalledOnce() + discardRequested = true + const importRun = { ...run, checkpoint: 'import', state: 'running' } + await act(async () => workflowRequest.resolve(importRun)) + + await expect(onWorkflowPending.mock.calls[0]?.[0]).resolves.toEqual(importRun) + expect(clientMock.getWorkflow).toHaveBeenCalledOnce() + expect(queryClientMock.invalidateQueries).not.toHaveBeenCalled() + expect(routerMock.push).not.toHaveBeenCalled() + }) + + it('keeps Cancel available while the import is pending', async () => { + const workflowRequest = deferred() + clientMock.getWorkflow.mockReturnValue(workflowRequest.promise) + const onCancel = vi.fn() + const user = userEvent.setup() + const { onWorkflowPending } = renderSelectionForm(onCancel) + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.getWorkflow).toHaveBeenCalledOnce()) + const cancel = screen.getByRole('button', { + name: 'dataset.newKnowledge.cancelAddSource', + }) + expect(cancel).toBeEnabled() + await user.click(cancel) + expect(onCancel).toHaveBeenCalledOnce() + + await act(async () => + workflowRequest.resolve({ ...run, checkpoint: 'complete', state: 'completed' }), + ) + await expect(onWorkflowPending.mock.calls[0]?.[0]).resolves.toEqual( + expect.objectContaining({ state: 'completed' }), + ) + }) + + it.each([ + { enabled: true, initialMode: 'interval', mode: 'provider' }, + { enabled: false, initialMode: 'interval', mode: 'manual' }, + { enabled: true, initialMode: 'manual', mode: 'interval' }, + ] as const)( + 'submits the $mode sync policy using the KnowledgeFS contract', + async ({ enabled, initialMode, mode }) => { + clientMock.getPolicy.mockResolvedValue( + policy({ enabled: initialMode !== 'manual', mode: initialMode }), + ) + clientMock.updatePolicy.mockResolvedValue(policy({ enabled, mode, revision: 3 })) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + mode, + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.updatePolicy).toHaveBeenCalledOnce()) + expect(clientMock.updatePolicy).toHaveBeenCalledWith({ + body: { + enabled, + expectedRevision: 2, + expectedSourceVersion: 3, + mode, + }, + params: { id: 'space-1', sourceId: 'source-1' }, + }) + expect(clientMock.selectPages).toHaveBeenCalledOnce() + }, + ) + + it('reconciles a lost sync-policy response before importing the selected pages', async () => { + clientMock.getPolicy + .mockReset() + .mockResolvedValueOnce(policy()) + .mockResolvedValueOnce(policy({ enabled: false, mode: 'manual', revision: 3 })) + clientMock.updatePolicy.mockRejectedValue(new Error('response lost')) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'manual', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.selectPages).toHaveBeenCalledOnce()) + expect(clientMock.getPolicy).toHaveBeenCalledTimes(2) + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }) + + it('allows discard when only the sync-policy outcome is uncertain', async () => { + clientMock.getPolicy + .mockReset() + .mockResolvedValueOnce(policy()) + .mockRejectedValueOnce(new Error('reconciliation unavailable')) + clientMock.updatePolicy.mockRejectedValue(new Error('response lost')) + const onCancel = vi.fn() + const user = userEvent.setup() + const { onSubmissionUncertainChange } = renderSelectionForm(onCancel) + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'manual', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.addSourceFailed', + ) + expect(onSubmissionUncertainChange).not.toHaveBeenCalledWith(true) + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })).toBeEnabled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + expect(onCancel).toHaveBeenCalledOnce() + }) + + it('uses the latest concurrency tokens after a conflicting sync-policy update', async () => { + clientMock.getPolicy + .mockReset() + .mockResolvedValueOnce(policy()) + .mockResolvedValueOnce( + policy({ + expectedSourceVersion: 4, + mode: 'provider', + revision: 3, + }), + ) + clientMock.updatePolicy + .mockRejectedValueOnce(Object.assign(new Error('conflict'), { status: 409 })) + .mockResolvedValueOnce(policy({ enabled: false, mode: 'manual', revision: 4 })) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'manual', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.addSourceFailed', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.updatePolicy).toHaveBeenCalledTimes(2)) + expect(clientMock.updatePolicy.mock.calls[1]?.[0]).toEqual({ + body: { + enabled: false, + expectedRevision: 3, + expectedSourceVersion: 4, + mode: 'manual', + }, + params: { id: 'space-1', sourceId: 'source-1' }, + }) + expect(clientMock.selectPages).toHaveBeenCalledOnce() + }) + + it('recovers when the initial sync policy cannot load', async () => { + clientMock.getPolicy + .mockReset() + .mockRejectedValueOnce(new Error('temporary failure')) + .mockResolvedValueOnce(policy()) + const user = userEvent.setup() + renderSelectionForm() + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.syncPolicyLoadFailed', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retrySyncPolicy' })) + + await screen.findByRole('checkbox', { name: 'Getting started' }) + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })).toBeDisabled() + }) + + it('creates the initial provider policy when a provisional source has no policy yet', async () => { + clientMock.getPolicy.mockRejectedValue( + Object.assign(new Error('policy not found'), { status: 404 }), + ) + clientMock.updatePolicy.mockResolvedValue(policy({ mode: 'provider', revision: 1 })) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.updatePolicy).toHaveBeenCalledOnce()) + expect(clientMock.updatePolicy).toHaveBeenCalledWith({ + body: { + enabled: true, + expectedRevision: 0, + expectedSourceVersion: 3, + mode: 'provider', + }, + params: { id: 'space-1', sourceId: 'source-1' }, + }) + expect(clientMock.selectPages).toHaveBeenCalledOnce() + }) + + it('keeps Cancel available when the sync policy cannot load', async () => { + clientMock.getPolicy.mockRejectedValue(new Error('temporary failure')) + const onCancel = vi.fn() + const user = userEvent.setup() + renderSelectionForm(onCancel) + + await screen.findByRole('alert') + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })).toBeDisabled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + + expect(onCancel).toHaveBeenCalledOnce() + }) + + it('keeps Cancel available while the sync policy is loading', async () => { + const policyRequest = deferred() + clientMock.getPolicy.mockReturnValue(policyRequest.promise) + const onCancel = vi.fn() + const user = userEvent.setup() + renderSelectionForm(onCancel) + + expect( + screen.getByRole('status', { name: 'dataset.newKnowledge.loadingSyncPolicy' }), + ).toBeVisible() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })).toBeDisabled() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + + expect(onCancel).toHaveBeenCalledOnce() + }) + + it('uses a new selection idempotency key after a confirmed import failure', async () => { + clientMock.getWorkflow.mockResolvedValueOnce({ ...run, checkpoint: 'import', state: 'failed' }) + const user = userEvent.setup() + renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.addSourceFailed', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.selectPages).toHaveBeenCalledTimes(2)) + expect(clientMock.selectPages.mock.calls[0]?.[0].headers['Idempotency-Key']).not.toBe( + clientMock.selectPages.mock.calls[1]?.[0].headers['Idempotency-Key'], + ) + }) + + it('reuses the selection idempotency key after a lost response without rewriting policy', async () => { + clientMock.getPolicy.mockResolvedValue(policy({ enabled: false, mode: 'manual' })) + clientMock.selectPages + .mockRejectedValueOnce(new Error('response lost')) + .mockResolvedValueOnce({ ...run, checkpoint: 'import', state: 'queued' }) + const user = userEvent.setup() + const { onSubmissionUncertainChange, onWorkflowPending, onWorkflowRun } = renderSelectionForm() + + await user.click(await screen.findByRole('checkbox', { name: 'Getting started' })) + await user.selectOptions( + screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }), + 'manual', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + expect(await screen.findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.addSourceFailed', + ) + expect(onWorkflowPending).toHaveBeenCalledOnce() + expect(onSubmissionUncertainChange).toHaveBeenLastCalledWith(true) + expect(screen.getByRole('checkbox', { name: 'Guides' })).toHaveAttribute( + 'aria-disabled', + 'true', + ) + expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toBeDisabled() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' }), + ).toBeEnabled() + await user.click(screen.getByText('Guides')) + expect(screen.getByRole('checkbox', { name: 'Guides' })).not.toBeChecked() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' })) + + await waitFor(() => expect(clientMock.selectPages).toHaveBeenCalledTimes(2)) + expect(onWorkflowRun).toHaveBeenCalledWith( + expect.objectContaining({ checkpoint: 'import', state: 'queued' }), + ) + expect(clientMock.updatePolicy).not.toHaveBeenCalled() + expect(clientMock.selectPages.mock.calls[0]?.[0].headers).toEqual( + clientMock.selectPages.mock.calls[1]?.[0].headers, + ) + }) +}) 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 0e2cd9b35ed..1397163b0d9 100644 --- a/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx +++ b/web/features/new-rag/__tests__/create-knowledge-page.spec.tsx @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CreateKnowledgePage } from '../create-knowledge-page' +import { newKnowledgeSourceDraftStorageKey } from '../routes' const serviceMock = vi.hoisted(() => ({ create: vi.fn(), @@ -103,6 +104,7 @@ async function choosePermission(user: ReturnType, option describe('CreateKnowledgePage', () => { beforeEach(() => { vi.clearAllMocks() + globalThis.sessionStorage.clear() serviceMock.create.mockResolvedValue(createdKnowledge) serviceMock.getPolicy.mockResolvedValue({ id: 'policy-1', @@ -126,6 +128,7 @@ describe('CreateKnowledgePage', () => { }) afterEach(() => { + globalThis.sessionStorage.clear() vi.restoreAllMocks() }) @@ -370,27 +373,82 @@ describe('CreateKnowledgePage', () => { expect(serviceMock.patchPolicy).toHaveBeenCalledOnce() }) - it('keeps source and upload disabled until their stacked flows land', () => { + it('integrates source setup while keeping the later upload flow disabled', async () => { + const user = userEvent.setup() navigationMock.startMode = 'source' renderPage() const startEmpty = screen.getByRole('radio', { name: 'dataset.newKnowledge.startEmpty' }) - expect(startEmpty).toBeChecked() + expect(startEmpty).not.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).toBeDisabled() - expect(connectSource).toHaveAccessibleDescription( - 'dataset.newKnowledge.connectSourceDescription dataset.cornerLabel.unavailable', + expect(connectSource).toBeEnabled() + expect(connectSource).toBeChecked() + expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })).toBeChecked() + expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled() + + await user.type( + screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder'), + 'https://docs.dify.ai', ) + await user.type( + 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', ) }) + it('hands the configured source draft to the add-source workflow after creation', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + renderPage() + await fillRequiredFields(user) + await user.type( + screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder'), + 'https://docs.dify.ai', + ) + await user.type( + screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'), + 'Dify docs', + ) + 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('renders the approved creation modal and exposes both dismiss actions', async () => { const user = userEvent.setup() renderPage() @@ -453,6 +511,27 @@ describe('CreateKnowledgePage', () => { expect(routerMock.replace).toHaveBeenCalledWith('/datasets?view=new') }) + it('asks before discarding a preserved source draft after switching source types', async () => { + const user = userEvent.setup() + navigationMock.startMode = 'source' + renderPage() + + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' })) + await user.type( + screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'), + 'Release notes', + ) + await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })) + await user.click(screen.getByRole('button', { name: 'common.operation.close' })) + + expect( + await screen.findByRole('alertdialog', { + name: 'dataset.newKnowledge.discardDraftTitle', + }), + ).toBeInTheDocument() + expect(routerMock.replace).not.toHaveBeenCalled() + }) + it('protects an unsaved draft from browser unload', async () => { const user = userEvent.setup() renderPage() diff --git a/web/features/new-rag/__tests__/request-id.spec.ts b/web/features/new-rag/__tests__/request-id.spec.ts new file mode 100644 index 00000000000..8d43da08c53 --- /dev/null +++ b/web/features/new-rag/__tests__/request-id.spec.ts @@ -0,0 +1,41 @@ +import { createRequestId } from '../request-id' + +const uuidV4Mock = vi.hoisted(() => vi.fn()) + +vi.mock('uuid', () => ({ + v4: uuidV4Mock, +})) + +describe('createRequestId', () => { + beforeEach(() => { + vi.clearAllMocks() + uuidV4Mock.mockReturnValue('fallback-request-id') + }) + + it('uses the native random UUID when it is available', () => { + const randomUUID = vi + .spyOn(globalThis.crypto, 'randomUUID') + .mockReturnValue('11111111-1111-4111-8111-111111111111') + + expect(createRequestId()).toBe('11111111-1111-4111-8111-111111111111') + expect(uuidV4Mock).not.toHaveBeenCalled() + + randomUUID.mockRestore() + }) + + it('falls back when randomUUID is unavailable', () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis.crypto, 'randomUUID') + Object.defineProperty(globalThis.crypto, 'randomUUID', { + configurable: true, + value: undefined, + }) + + try { + expect(createRequestId()).toBe('fallback-request-id') + expect(uuidV4Mock).toHaveBeenCalledOnce() + } finally { + if (descriptor) Object.defineProperty(globalThis.crypto, 'randomUUID', descriptor) + else Reflect.deleteProperty(globalThis.crypto, 'randomUUID') + } + }) +}) diff --git a/web/features/new-rag/__tests__/sources-page.spec.tsx b/web/features/new-rag/__tests__/sources-page.spec.tsx index ea67db85648..de137089814 100644 --- a/web/features/new-rag/__tests__/sources-page.spec.tsx +++ b/web/features/new-rag/__tests__/sources-page.spec.tsx @@ -154,14 +154,19 @@ describe('SourcesPage', () => { 'href', '/datasets/new/space-1/sources/new', ) - for (const brand of ['firecrawl', 'jina', 'notion', 'google-drive', 'confluence', 'more']) - expect(container.querySelector(`[data-brand="${brand}"]`)).toBeInTheDocument() - expect(container.querySelector('[data-brand="firecrawl"]')?.tagName).toBe('SPAN') - expect(container.querySelector('[data-brand="firecrawl"]')).toHaveClass('size-8') - expect(container.querySelector('[data-brand="jina"]')).toHaveClass('i-custom-public-llm-jina') - expect(container.querySelector('[data-brand="confluence"]')).toHaveClass( - 'i-custom-public-common-confluence', - ) + for (const [brand, iconClass] of [ + ['firecrawl', 'i-custom-public-common-firecrawl'], + ['jina', 'i-custom-public-llm-jina'], + ['notion', 'i-custom-public-common-notion'], + ['google-drive', 'i-custom-public-common-google-drive'], + ['confluence', 'i-custom-public-common-confluence'], + ['dropbox', 'i-custom-public-common-dropbox'], + ] as const) { + const icon = container.querySelector(`[data-brand="${brand}"]`) + expect(icon).toBeInTheDocument() + expect(icon?.tagName).toBe('SPAN') + expect(icon).toHaveClass(iconClass) + } }) it('renders real source statuses and filters by status and search text', async () => { @@ -231,6 +236,102 @@ describe('SourcesPage', () => { expect(within(rowsDescending[1]!).getByText('Alpha docs')).toBeInTheDocument() }) + it('places the newest source first until the user selects a name sort', () => { + sourcesQuery.data = { + pages: [ + { + items: [ + source({ id: 'older', name: 'Older source', createdAt: '2026-07-20T10:00:00Z' }), + source({ id: 'newer', name: 'Newest source', createdAt: '2026-07-20T10:01:00Z' }), + ], + }, + ], + } + + render() + + const rows = screen.getAllByRole('row').slice(1) + expect(within(rows[0]!).getByText('Newest source')).toBeInTheDocument() + expect(within(rows[1]!).getByText('Older source')).toBeInTheDocument() + }) + + it('keeps provisional crawl sources out of the source list', () => { + sourcesQuery.data = { + pages: [ + { + items: [ + source({ + id: 'preview', + metadata: { preview: true }, + name: 'Discarded preview', + status: 'disabled', + }), + source({ id: 'connected', name: 'Connected source' }), + source({ + id: 'submitted-preview', + metadata: { preview: true }, + name: 'Submitted preview', + status: 'syncing', + }), + ], + }, + ], + } + + render() + + expect(screen.queryByText('Discarded preview')).not.toBeInTheDocument() + expect(screen.getByText('Connected source')).toBeInTheDocument() + expect(screen.getByText('Submitted preview')).toBeInTheDocument() + }) + + it('continues past cursor pages containing only hidden preview drafts', () => { + sourcesQuery.data = { + pages: [ + { + items: [ + source({ + id: 'preview', + metadata: { preview: true }, + status: 'disabled', + }), + ], + nextCursor: 'next', + }, + ], + } + sourcesQuery.hasNextPage = true + + render() + + expect(sourcesQuery.fetchNextPage).toHaveBeenCalledOnce() + expect(screen.queryByText('dataset.newKnowledge.sourcesEmptyTitle')).not.toBeInTheDocument() + }) + + it('continues when the newest loaded page contributes only hidden preview drafts', () => { + sourcesQuery.data = { + pages: [ + { items: [source({ id: 'connected', name: 'Connected source' })] }, + { + items: [ + source({ + id: 'preview', + metadata: { preview: true }, + status: 'disabled', + }), + ], + nextCursor: 'next', + }, + ], + } + sourcesQuery.hasNextPage = true + + render() + + expect(screen.getByText('Connected source')).toBeInTheDocument() + expect(sourcesQuery.fetchNextPage).toHaveBeenCalledOnce() + }) + it('opens a source URI from the row action menu', async () => { const user = userEvent.setup() sourcesQuery.data = { pages: [{ items: [source({})] }] } @@ -361,19 +462,21 @@ describe('SourcesPage', () => { sourcesQuery.data = { pages: [ { - items: [source({ id: 'active-source' }), source({ id: 'disabled', status: 'disabled' })], + items: [ + source({ id: 'active-source', name: 'Active source' }), + source({ id: 'disabled', name: 'Disabled source', status: 'disabled' }), + ], }, ], } render() - const actionButtons = screen.getAllByRole('button', { - name: /dataset.newKnowledge.sourceActions/, - }) - const activeSourceActions = actionButtons[0] - const disabledSourceActions = actionButtons[1] - if (!activeSourceActions || !disabledSourceActions) - throw new Error('Expected actions for both sources') + const activeSourceActions = within( + screen.getByRole('row', { name: /Active source/ }), + ).getByRole('button', { name: /dataset.newKnowledge.sourceActions/ }) + const disabledSourceActions = within( + screen.getByRole('row', { name: /Disabled source/ }), + ).getByRole('button', { name: /dataset.newKnowledge.sourceActions/ }) await user.click(activeSourceActions) await user.click(screen.getByRole('menuitem', { name: 'dataset.newKnowledge.disableSource' })) await waitFor(() => diff --git a/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx b/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx index 424d2e5e69c..f8a3ecf6fde 100644 --- a/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx +++ b/web/features/new-rag/__tests__/website-crawl-preview.spec.tsx @@ -1,5 +1,5 @@ import type { Source, SourceWorkflowRun } from '@dify/contracts/knowledge-fs/types.gen' -import { act, screen, waitFor } from '@testing-library/react' +import { act, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { render } from '@/test/console/render' import { WebsiteCrawlPreview } from '../website-crawl-preview' @@ -14,6 +14,40 @@ const clientMock = vi.hoisted(() => ({ startPreview: vi.fn(), })) +const routerMock = vi.hoisted(() => ({ push: vi.fn() })) + +vi.mock('@/next/navigation', () => ({ useRouter: () => routerMock })) + +vi.mock('../crawl-selection-form', () => ({ + CrawlSelectionForm: ({ + busy, + onCancel, + onRecrawl, + pages, + }: { + busy?: boolean + onCancel: () => void + onRecrawl: () => void + pages: Array<{ pageId: string; title?: string }> + }) => ( +
+

dataset.newKnowledge.pagesCrawled

+ {pages.map((page) => ( + + ))} + + +
+ ), +})) + vi.mock('@/service/client', () => ({ consoleClient: { knowledgeFs: { @@ -101,31 +135,34 @@ describe('WebsiteCrawlPreview', () => { ], }) clientMock.listSources.mockResolvedValue({ items: [] }) + routerMock.push.mockReset() }) - afterEach(() => vi.useRealTimers()) + afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() + }) it('validates the URL and starts a real crawl preview with a provisional source', async () => { const user = userEvent.setup() - render( - , - ) + render() + expect(screen.getByRole('status')).toHaveTextContent( + 'dataset.newKnowledge.providerConnected:{"provider":"Firecrawl"}', + ) const start = screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' }) - const rootUrl = screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/) - expect(start).toBeEnabled() - await user.click(start) - expect(rootUrl).toHaveFocus() - expect(rootUrl).toHaveAttribute('aria-invalid', 'true') - await user.type(rootUrl, 'ftp://docs.dify.ai') + expect(start).toBeDisabled() + await user.type(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/), 'ftp://docs.dify.ai') await user.tab() - expect(rootUrl).toHaveAttribute('aria-invalid', 'true') - await user.clear(rootUrl) - await user.type(rootUrl, 'https://docs.dify.ai') + expect(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)).toHaveAttribute( + 'aria-invalid', + 'true', + ) + await user.clear(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)) + await user.type( + screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/), + 'https://docs.dify.ai', + ) await user.type(screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/), 'Dify docs') await user.click(start) @@ -161,92 +198,8 @@ describe('WebsiteCrawlPreview', () => { ).not.toBeInTheDocument() }) - it('uses the shared form controls with stable field names and autocomplete metadata', async () => { - const user = userEvent.setup() - render( - , - ) - - expect(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)).toHaveAttribute( - 'name', - 'root-url', - ) - expect(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)).toHaveAttribute( - 'autocomplete', - 'off', - ) - expect(screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/)).toHaveAttribute( - 'name', - 'source-name', - ) - expect(screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/)).toHaveAttribute( - 'autocomplete', - 'off', - ) - - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.crawlOptions/ })) - - expect( - screen.getByRole('checkbox', { name: 'dataset.newKnowledge.includeSubpages' }), - ).toBeInTheDocument() - expect(document.querySelector('input[name="include-subpages"]')).toBeInTheDocument() - expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.maxPages' })).toHaveAttribute( - 'name', - 'page-limit', - ) - }) - - it('only labels collapsed crawl options as defaults when they are unchanged', async () => { - const user = userEvent.setup() - render( - , - ) - - expect(screen.getByText('dataset.newKnowledge.usingDefaults')).toBeInTheDocument() - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.crawlOptions/ })) - await user.click(screen.getByRole('checkbox', { name: 'dataset.newKnowledge.includeSubpages' })) - await user.click(screen.getByRole('button', { name: /dataset\.newKnowledge\.crawlOptions/ })) - - expect(screen.queryByText('dataset.newKnowledge.usingDefaults')).not.toBeInTheDocument() - }) - - it('keeps completed results associated with the submitted URL while the draft is edited', async () => { - render( - , - ) - const user = await fillValidForm() - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - await screen.findByText('Getting started') - - const rootUrl = screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/) - await user.clear(rootUrl) - await user.type(rootUrl, 'https://example.com') - - const completedStatus = screen.getByText(/^dataset\.newKnowledge\.pagesCrawled/) - expect(completedStatus).toHaveTextContent('docs.dify.ai') - expect(completedStatus).not.toHaveTextContent('example.com') - }) - it('submits the crawl form with Enter and enforces the source name contract limit', async () => { - render( - , - ) + render() const user = await fillValidForm() const name = screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/) expect(name).toHaveAttribute('maxlength', '200') @@ -258,42 +211,434 @@ describe('WebsiteCrawlPreview', () => { await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledOnce()) }) - it('shows pending feedback while a completed crawl is being restarted', async () => { - const restartRequest = deferred() - clientMock.startPreview - .mockResolvedValueOnce(run('running')) - .mockReturnValueOnce(restartRequest.promise) + it('caps crawl previews at the selection contract limit', async () => { + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: /^dataset\.newKnowledge\.crawlOptions/ })) + const pageLimit = screen.getByRole('spinbutton', { name: 'dataset.newKnowledge.maxPages' }) + expect(pageLimit).toHaveAttribute('max', '200') + await user.clear(pageLimit) + await user.type(pageLimit, '1000') + expect(pageLimit).toHaveValue(200) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - render( - , + await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledOnce()) + expect(clientMock.createSource.mock.calls[0]?.[0].body.metadata.crawlOptions.limit).toBe(200) + }) + + it('preserves a replacement crawl page limit after clearing the input', async () => { + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: /^dataset\.newKnowledge\.crawlOptions/ })) + const pageLimit = screen.getByRole('spinbutton', { name: 'dataset.newKnowledge.maxPages' }) + await user.clear(pageLimit) + await user.type(pageLimit, '50') + expect(pageLimit).toHaveValue(50) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + + await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledOnce()) + expect(clientMock.createSource.mock.calls[0]?.[0].body.metadata.crawlOptions.limit).toBe(50) + }) + + it('confirms dirty cancellation and protects an unfinished draft from page unload', async () => { + const user = userEvent.setup() + render() + + await user.type( + screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/), + 'https://docs.dify.ai', ) + const unload = new Event('beforeunload', { cancelable: true }) + window.dispatchEvent(unload) + expect(unload.defaultPrevented).toBe(true) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + expect( + screen.getByRole('alertdialog', { name: 'dataset.newKnowledge.discardSourceChanges' }), + ).toBeInTheDocument() + expect(routerMock.push).not.toHaveBeenCalled() + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.keepEditing' })) + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + }) + + it('cancels an active preview before discarding its provisional source', async () => { + clientMock.getRun.mockReturnValue(new Promise(() => {})) + clientMock.cancel.mockResolvedValue(run('canceled')) + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledOnce()) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + await waitFor(() => expect(clientMock.cancel).toHaveBeenCalledOnce()) + expect(clientMock.cancel).toHaveBeenCalledWith({ + body: { reason: 'user_requested' }, + params: { id: 'space-1', runId: 'run-1' }, + }) + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + }) + + it('cancels a crawl that finishes starting after discard was confirmed', async () => { + const startRequest = deferred() + clientMock.startPreview.mockReturnValue(startRequest.promise) + clientMock.cancel.mockResolvedValue(run('canceled')) + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledOnce()) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.keepEditing' })).toBeDisabled() + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ).toHaveAttribute('aria-disabled', 'true') + + startRequest.resolve(run('running')) + + await waitFor(() => expect(clientMock.cancel).toHaveBeenCalledOnce()) + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + }) + + it('reconciles a response-lost start before discarding its workflow', async () => { + clientMock.startPreview + .mockRejectedValueOnce(new Error('response lost')) + .mockResolvedValueOnce(run('running')) + clientMock.cancel.mockResolvedValue(run('canceled')) + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByRole('alert') + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledTimes(2)) + expect(clientMock.startPreview.mock.calls[0]?.[0].headers).toEqual( + clientMock.startPreview.mock.calls[1]?.[0].headers, + ) + await waitFor(() => expect(clientMock.cancel).toHaveBeenCalledOnce()) + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + }) + + it('does not start a crawl when discard is confirmed during source creation', async () => { + const sourceRequest = deferred() + clientMock.createSource.mockReturnValue(sourceRequest.promise) + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await waitFor(() => expect(clientMock.createSource).toHaveBeenCalledOnce()) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + sourceRequest.resolve(source()) + + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + expect(clientMock.startPreview).not.toHaveBeenCalled() + expect(clientMock.cancel).not.toHaveBeenCalled() + }) + + it('cancels a retry that returns after navigation discard was confirmed', async () => { + const retryRequest = deferred() + clientMock.retry.mockReturnValue(retryRequest.promise) + clientMock.cancel.mockResolvedValue(run('canceled')) + render( + <> + Documents navigation + + , + ) + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByText('Getting started') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reCrawl' })) + await waitFor(() => expect(clientMock.retry).toHaveBeenCalledOnce()) + + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + retryRequest.resolve(run('running')) + + await waitFor(() => expect(clientMock.cancel).toHaveBeenCalledOnce()) + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/documents'), + ) + }) + + it('reconciles a response-lost retry before leaving the preview', async () => { + const previousRun = run('succeeded', { progressCompleted: 1, progressTotal: 1 }) + clientMock.getRun + .mockResolvedValueOnce(previousRun) + .mockResolvedValueOnce(previousRun) + .mockResolvedValueOnce(run('running', { executionAttempts: 2 })) + clientMock.retry.mockRejectedValue(new Error('response lost')) + clientMock.cancel.mockResolvedValue(run('canceled', { executionAttempts: 2 })) + render( + <> + Documents navigation + + , + ) + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByText('Getting started') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reCrawl' })) + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(2)) + + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(3)) + await waitFor(() => expect(clientMock.cancel).toHaveBeenCalledOnce()) + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/documents'), + ) + }) + + it('keeps the started run available when discard cancellation must be retried', async () => { + clientMock.getRun.mockReturnValue(new Promise(() => {})) + clientMock.cancel + .mockRejectedValueOnce(Object.assign(new Error('conflict'), { status: 409 })) + .mockResolvedValueOnce(run('canceled')) + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledOnce()) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + expect(within(screen.getByRole('alertdialog')).getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.crawlFailedDescription', + ) + expect(routerMock.push).not.toHaveBeenCalled() + + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + await waitFor(() => expect(clientMock.cancel).toHaveBeenCalledTimes(2)) + expect(clientMock.cancel.mock.calls[0]?.[0].params.runId).toBe('run-1') + expect(clientMock.cancel.mock.calls[1]?.[0].params.runId).toBe('run-1') + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + }) + + it('restores polling after cancel failure and honors the latest terminal snapshot', async () => { + const retryRequest = deferred() + clientMock.getRun + .mockResolvedValueOnce(run('succeeded', { progressCompleted: 1, progressTotal: 1 })) + .mockResolvedValueOnce( + run('canceled', { + executionAttempts: 2, + progressCompleted: 1, + updatedAt: '2026-07-20T10:02:00Z', + }), + ) + clientMock.getPages + .mockResolvedValueOnce({ + items: [ + { + pageId: 'page-1', + sourceUrl: 'https://docs.dify.ai/getting-started', + title: 'Getting started', + }, + ], + }) + .mockResolvedValueOnce({ items: [] }) + clientMock.retry.mockReturnValue(retryRequest.promise) + clientMock.cancel.mockRejectedValue(Object.assign(new Error('conflict'), { status: 409 })) + render( + <> + Documents navigation + + , + ) + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByText('Getting started') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reCrawl' })) + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + retryRequest.resolve( + run('running', { executionAttempts: 2, updatedAt: '2026-07-20T10:01:00Z' }), + ) + + expect(await within(screen.getByRole('alertdialog')).findByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.crawlFailedDescription', + ) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.keepEditing' })) + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(2)) + expect(await screen.findByText('dataset.newKnowledge.crawlStopped')).toBeInTheDocument() + expect(screen.queryByText('Getting started')).not.toBeInTheDocument() + + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + expect(clientMock.cancel).toHaveBeenCalledOnce() + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/documents'), + ) + }) + + it('guards sidebar links and browser history after a crawl preview is ready', async () => { + render( + <> + Documents navigation + + , + ) + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByText('Getting started') + + act(() => window.history.back()) + expect( + await screen.findByRole('alertdialog', { + name: 'dataset.newKnowledge.discardSourceChanges', + }), + ).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.keepEditing' })) + + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + expect( + screen.getByRole('alertdialog', { name: 'dataset.newKnowledge.discardSourceChanges' }), + ).toBeInTheDocument() + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/documents'), + ) + }) + + it('guards browser history before a crawl preview starts', async () => { + const user = userEvent.setup() + render() + await user.type( + screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/), + 'https://docs.dify.ai', + ) + + act(() => window.history.back()) + + expect( + await screen.findByRole('alertdialog', { + name: 'dataset.newKnowledge.discardSourceChanges', + }), + ).toBeInTheDocument() + }) + + it('leaves through clean Cancel after a dirty form is cleared', async () => { + const user = userEvent.setup() + render() + const rootUrl = screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/) + await user.type(rootUrl, 'x') + await user.clear(rootUrl) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + + it('leaves through a same-origin link after a dirty form is cleared', async () => { + const user = userEvent.setup() + render( + <> + Documents navigation + + , + ) + const rootUrl = screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/) + await user.type(rootUrl, 'x') + await user.clear(rootUrl) + + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/documents'), + ) + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + + it('clears a canceled sidebar destination before the form cancel action', async () => { + render( + <> + Documents navigation + + , + ) + const user = await fillValidForm() + await user.click(screen.getByRole('link', { name: 'Documents navigation' })) + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.keepEditing' })) + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), + ) + + await waitFor(() => + expect(routerMock.push).toHaveBeenCalledWith('/datasets/new/space-1/sources'), + ) + expect(routerMock.push).not.toHaveBeenCalledWith('/datasets/new/space-1/documents') + }) + + it('shows pending feedback while a completed crawl is being restarted', async () => { + const retryRequest = deferred() + clientMock.retry.mockReturnValue(retryRequest.promise) + + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) const reCrawl = await screen.findByRole('button', { name: 'dataset.newKnowledge.reCrawl' }) await user.click(reCrawl) - expect(screen.getByRole('button', { name: 'dataset.newKnowledge.crawling' })).toHaveAttribute( - 'aria-disabled', - 'true', - ) + expect(reCrawl).toHaveAttribute('aria-disabled', 'true') await act(async () => - restartRequest.resolve( - run('running', { - id: 'run-2', - updatedAt: '2026-07-20T10:01:00Z', - }), + retryRequest.resolve( + run('running', { executionAttempts: 2, updatedAt: '2026-07-20T10:01:00Z' }), ), ) - await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledTimes(2)) - const firstKey = clientMock.startPreview.mock.calls[0]?.[0].headers['Idempotency-Key'] - const secondKey = clientMock.startPreview.mock.calls[1]?.[0].headers['Idempotency-Key'] - expect(secondKey).not.toBe(firstKey) - expect(clientMock.createSource).toHaveBeenCalledOnce() - expect(clientMock.retry).not.toHaveBeenCalled() + await waitFor(() => expect(clientMock.retry).toHaveBeenCalledOnce()) }) it('streams page cursors while running and replaces them with the final snapshot', async () => { @@ -330,13 +675,7 @@ describe('WebsiteCrawlPreview', () => { ], }) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) @@ -389,13 +728,7 @@ describe('WebsiteCrawlPreview', () => { clientMock.getRun.mockResolvedValue(run('running', { progressCompleted: 1 })) clientMock.cancel.mockResolvedValue(run('canceled', { progressCompleted: 1 })) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByText('Getting started') @@ -420,13 +753,7 @@ describe('WebsiteCrawlPreview', () => { .mockResolvedValueOnce(run('canceled', { progressCompleted: 1 })) clientMock.cancel.mockRejectedValue(new Error('response lost')) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByText('Getting started') @@ -445,13 +772,7 @@ describe('WebsiteCrawlPreview', () => { Object.assign(new Error('proxy connection lost'), { status: 502 }), ) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByText('Getting started') @@ -472,13 +793,7 @@ describe('WebsiteCrawlPreview', () => { .mockRejectedValueOnce(Object.assign(new Error('rate limited'), { status: 429 })) .mockResolvedValueOnce(run('canceled', { progressCompleted: 1 })) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByText('Getting started') @@ -496,13 +811,7 @@ describe('WebsiteCrawlPreview', () => { clientMock.getPages.mockResolvedValue({ items: [] }) clientMock.retry.mockResolvedValue(run('running')) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) expect(await screen.findByRole('alert')).toHaveTextContent( @@ -519,31 +828,89 @@ describe('WebsiteCrawlPreview', () => { expect(clientMock.startPreview).toHaveBeenCalledOnce() }) + it('accepts a successful retry response even when its counters have not advanced yet', async () => { + const failedRun = run('failed', { lastErrorCode: 'PROVIDER_403', progressFailed: 1 }) + clientMock.getRun + .mockResolvedValueOnce(failedRun) + .mockResolvedValueOnce(failedRun) + .mockReturnValue(new Promise(() => {})) + clientMock.getPages.mockResolvedValue({ items: [] }) + clientMock.retry.mockResolvedValue(run('running')) + + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByRole('alert') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) + + expect( + await screen.findByRole('button', { name: 'dataset.newKnowledge.stopCrawl' }), + ).toBeInTheDocument() + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(2)) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.stopCrawl' }), + ).toBeInTheDocument() + }) + + it('keeps retry causality across stale replicas and retires its fingerprint', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + const failedRun = run('failed', { lastErrorCode: 'PROVIDER_403', progressFailed: 1 }) + clientMock.getRun + .mockResolvedValueOnce(failedRun) + .mockResolvedValueOnce(failedRun) + .mockResolvedValueOnce(run('running')) + .mockResolvedValueOnce(failedRun) + .mockResolvedValueOnce( + run('succeeded', { progressCompleted: 1, updatedAt: '2026-07-20T10:01:00Z' }), + ) + clientMock.getPages.mockResolvedValueOnce({ items: [] }).mockResolvedValue({ + items: [ + { + pageId: 'page-1', + sourceUrl: 'https://docs.dify.ai/getting-started', + title: 'Getting started', + }, + ], + }) + clientMock.retry.mockResolvedValue(run('running')) + + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByRole('alert') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(2)) + + await act(async () => vi.advanceTimersByTime(1500)) + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(3)) + await act(async () => vi.advanceTimersByTime(1500)) + await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledTimes(4)) + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.stopCrawl' }), + ).toBeInTheDocument() + await act(async () => vi.advanceTimersByTime(1500)) + await screen.findByText('Getting started') + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.reCrawl' })) + await waitFor(() => expect(clientMock.retry).toHaveBeenCalledTimes(2)) + }) + it('reconciles a lost Retry response without sending retry twice', async () => { clientMock.getRun .mockResolvedValueOnce(run('failed', { lastErrorCode: 'PROVIDER_403' })) .mockResolvedValueOnce( - run('running', { - executionAttempts: 2, - updatedAt: '2026-07-20T10:01:00Z', + run('succeeded', { + progressCompleted: 1, }), ) .mockResolvedValueOnce( run('succeeded', { - executionAttempts: 2, progressCompleted: 1, - updatedAt: '2026-07-20T10:02:00Z', }), ) clientMock.retry.mockRejectedValue(new Error('response lost')) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') @@ -567,17 +934,13 @@ describe('WebsiteCrawlPreview', () => { clientMock.getPages.mockResolvedValue({ items: [] }) clientMock.retry.mockRejectedValue(Object.assign(new Error('proxy timeout'), { status: 504 })) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) + expect(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)).toBeDisabled() + expect(screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/)).toBeDisabled() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) @@ -594,13 +957,7 @@ describe('WebsiteCrawlPreview', () => { .mockRejectedValueOnce(Object.assign(new Error('rate limited'), { status: 429 })) .mockResolvedValueOnce(run('running', { executionAttempts: 2 })) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') @@ -612,73 +969,36 @@ describe('WebsiteCrawlPreview', () => { it.each([ ['failed', 'PROVIDER_TIMEOUT', 'dataset.newKnowledge.crawlFailedTimeout'], - ['failed', 'PROVIDER_UNAVAILABLE', 'dataset.newKnowledge.crawlFailedProvider'], + [ + 'failed', + 'PROVIDER_UNAVAILABLE', + 'dataset.newKnowledge.crawlFailedProvider:{"provider":"Firecrawl"}', + ], ['timed_out', undefined, 'dataset.newKnowledge.crawlFailedTimeout'], ])('shows a recovery message for %s / %s', async (state, lastErrorCode, message) => { clientMock.getRun.mockResolvedValue(run(state, { lastErrorCode })) clientMock.getPages.mockResolvedValue({ items: [] }) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - const alert = await screen.findByRole('alert') - expect(alert).toHaveTextContent(message) - if (lastErrorCode === 'PROVIDER_UNAVAILABLE') - expect(alert).toHaveTextContent('"provider":"Jina Reader"') + expect(await screen.findByRole('alert')).toHaveTextContent(message) }) it('offers an adjust-and-recrawl path after a successful zero-result crawl', async () => { clientMock.getRun.mockResolvedValue(run('succeeded')) clientMock.getPages.mockResolvedValue({ items: [] }) + clientMock.retry.mockResolvedValue(run('running')) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) const noPages = await screen.findByText(/^dataset\.newKnowledge\.noPagesFound:/) expect(noPages.closest('[role="status"]')).toHaveAttribute('aria-live', 'polite') await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.adjustAndRecrawl' })) - await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledTimes(2)) - const firstKey = clientMock.startPreview.mock.calls[0]?.[0].headers['Idempotency-Key'] - const secondKey = clientMock.startPreview.mock.calls[1]?.[0].headers['Idempotency-Key'] - expect(secondKey).not.toBe(firstKey) - expect(clientMock.createSource).toHaveBeenCalledOnce() - expect(clientMock.retry).not.toHaveBeenCalled() - }) - - it('reports draft and provisional source lifecycle to the parent flow', async () => { - const onDraftChange = vi.fn() - const onProvisionalSource = vi.fn() - const view = render( - , - ) - - const user = await fillValidForm() - expect(onDraftChange).toHaveBeenLastCalledWith(true) - await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) - await waitFor(() => expect(onProvisionalSource).toHaveBeenCalledWith(source())) - - view.unmount() - expect(onDraftChange).toHaveBeenLastCalledWith(false) + await waitFor(() => expect(clientMock.retry).toHaveBeenCalledOnce()) }) it('treats a superseded workflow as terminal and stops polling', async () => { @@ -686,13 +1006,7 @@ describe('WebsiteCrawlPreview', () => { clientMock.getRun.mockResolvedValue(run('superseded')) clientMock.getPages.mockResolvedValue({ items: [] }) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) expect(await screen.findByText('dataset.newKnowledge.crawlStopped')).toHaveAttribute( @@ -712,13 +1026,7 @@ describe('WebsiteCrawlPreview', () => { .mockRejectedValueOnce(new Error('temporary network failure')) .mockResolvedValueOnce(run('succeeded', { progressCompleted: 1 })) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) expect(await screen.findByRole('alert')).toHaveTextContent( @@ -736,16 +1044,12 @@ describe('WebsiteCrawlPreview', () => { .mockRejectedValueOnce(new Error('response lost')) .mockResolvedValueOnce(run('running')) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') + expect(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)).toBeDisabled() + expect(screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/)).toBeDisabled() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledTimes(2)) @@ -767,13 +1071,7 @@ describe('WebsiteCrawlPreview', () => { }, ) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) @@ -789,13 +1087,7 @@ describe('WebsiteCrawlPreview', () => { return { items: [source({ clientRequestId })] } }) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') @@ -806,32 +1098,43 @@ describe('WebsiteCrawlPreview', () => { expect(clientMock.listSources).toHaveBeenCalledTimes(2) }) - it('retries source creation after bounded reconciliation of an ambiguous result', async () => { - clientMock.createSource - .mockRejectedValueOnce(Object.assign(new Error('proxy timeout'), { status: 504 })) - .mockResolvedValueOnce(source()) + it('never repeats source creation while an ambiguous result remains unreconciled', async () => { + clientMock.createSource.mockRejectedValue( + Object.assign(new Error('proxy timeout'), { status: 504 }), + ) clientMock.listSources.mockResolvedValue({ items: [] }) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') + expect(screen.getByLabelText(/^dataset\.newKnowledge\.rootUrl/)).toBeDisabled() + expect(screen.getByLabelText(/^dataset\.newKnowledge\.sourceName/)).toBeDisabled() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) - await waitFor(() => expect(clientMock.listSources).toHaveBeenCalledTimes(2)) await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retryCrawl' })) - await waitFor(() => expect(clientMock.startPreview).toHaveBeenCalledOnce()) - expect(clientMock.listSources).toHaveBeenCalledTimes(3) - expect(clientMock.createSource).toHaveBeenCalledTimes(2) - expect(clientMock.createSource.mock.calls[0]?.[0].body.metadata.clientRequestId).toBe( - clientMock.createSource.mock.calls[1]?.[0].body.metadata.clientRequestId, + await waitFor(() => expect(clientMock.listSources).toHaveBeenCalledTimes(3)) + expect(clientMock.createSource).toHaveBeenCalledOnce() + expect(clientMock.startPreview).not.toHaveBeenCalled() + }) + + it('blocks discard while provisional source creation is still uncertain', async () => { + clientMock.createSource.mockRejectedValue(new Error('response lost')) + clientMock.listSources.mockResolvedValue({ items: [] }) + render() + const user = await fillValidForm() + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) + await screen.findByRole('alert') + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.cancelAddSource' })) + await user.click( + screen.getByRole('button', { name: 'dataset.newKnowledge.discardSourceChangesConfirm' }), ) + + expect(within(screen.getByRole('alertdialog')).getByRole('alert')).toHaveTextContent( + 'dataset.newKnowledge.crawlFailedDescription', + ) + expect(routerMock.push).not.toHaveBeenCalled() }) it('retries source creation after a definitive HTTP rejection', async () => { @@ -839,13 +1142,7 @@ describe('WebsiteCrawlPreview', () => { .mockRejectedValueOnce(Object.assign(new Error('rate limited'), { status: 429 })) .mockResolvedValueOnce(source()) - render( - , - ) + render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await screen.findByRole('alert') @@ -859,13 +1156,7 @@ describe('WebsiteCrawlPreview', () => { it('cleans up polling when the component unmounts', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }) clientMock.getRun.mockResolvedValue(run('running')) - const view = render( - , - ) + const view = render() const user = await fillValidForm() await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' })) await waitFor(() => expect(clientMock.getRun).toHaveBeenCalledOnce()) diff --git a/web/features/new-rag/add-source-page.tsx b/web/features/new-rag/add-source-page.tsx index 6a7d11ae245..ecf5721be3c 100644 --- a/web/features/new-rag/add-source-page.tsx +++ b/web/features/new-rag/add-source-page.tsx @@ -3,36 +3,52 @@ import type { GetKnowledgeSpacesByIdSourceConnectionsResponse, GetSourceProvidersResponse, - Source, } from '@dify/contracts/knowledge-fs/types.gen' +import type { + NewKnowledgeSourceDraft, + NewKnowledgeSourceType, + NewKnowledgeWebsiteProvider, +} from './routes' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' -import Link from '@/next/link' import { useRouter } from '@/next/navigation' import { consoleClient, consoleQuery } from '@/service/client' +import { PendingWebsiteSetup, UnavailableConnectedSourceSetup } from './add-source-placeholder' import { AddSourceExitDialog } from './components/add-source-exit-dialog' -import { newKnowledgeDetailPath } from './routes' +import { + createNewKnowledgeSourceDraft, + newKnowledgeDetailPath, + newKnowledgeSourceDraftStorageKey, + parseNewKnowledgeSourceDraft, +} from './routes' import { WebsiteCrawlPreview } from './website-crawl-preview' type Provider = GetSourceProvidersResponse['items'][number] type ProviderField = Provider['configuration'][number] type Connection = GetKnowledgeSpacesByIdSourceConnectionsResponse['items'][number] type ConnectionAuthKind = 'api-key' | 'endpoint' -type SourceType = 'onlineDocuments' | 'onlineDrive' | 'websiteCrawl' +type SourceType = NewKnowledgeSourceType const CONNECTION_PAGE_SIZE = 200 -const CONNECTION_RECONCILIATION_RETRY_DELAYS = [0, 100, 300] const FIRECRAWL_PROVIDER_ID = 'plugin-daemon-website' -const SOURCE_MARKETPLACE_PATH = '/marketplace?category=datasource' +const FIRECRAWL_CONNECTION_NAME = 'Firecrawl' const FIRECRAWL_CONFIGURATION = { datasource: 'crawl', pluginId: 'langgenius/firecrawl_datasource', provider: 'firecrawl', } as const +const WEBSITE_PROVIDER_OPTIONS: Array<{ + icon: string + value: NewKnowledgeWebsiteProvider +}> = [ + { icon: 'i-ri-fire-fill text-orange-500', value: 'Firecrawl' }, + { icon: 'i-custom-public-llm-jina', value: 'Jina Reader' }, + { icon: 'i-ri-water-flash-line', value: 'WaterCrawl' }, +] const FIRECRAWL_FIXED_FIELD_NAMES = new Set(Object.keys(FIRECRAWL_CONFIGURATION)) const CONNECTION_STATUS_PRIORITY: Record = { active: 0, @@ -41,7 +57,6 @@ const CONNECTION_STATUS_PRIORITY: Record = { expired: 3, revoked: 4, } - function humanizeFieldName(name: string) { return name .replace(/([a-z0-9])([A-Z])/g, '$1 $2') @@ -55,13 +70,8 @@ function fieldValue(value: string, type: ProviderField['type']) { return value.trim() } -function providerIconClass(provider: Provider) { - const identity = `${provider.id} ${provider.displayName}`.toLocaleLowerCase() - if (provider.id === FIRECRAWL_PROVIDER_ID || identity.includes('firecrawl')) - return 'i-ri-fire-fill text-orange-500' - if (identity.includes('jina')) return 'i-custom-public-llm-jina' - if (identity.includes('water')) return 'i-ri-water-flash-line text-util-colors-blue-blue-600' - return 'i-ri-global-line text-text-accent' +function findFirecrawl(providers: Provider[]) { + return providers.find((provider) => provider.id === FIRECRAWL_PROVIDER_ID) } function findProviderConnection(connections: Connection[], providerId?: string) { @@ -86,34 +96,21 @@ function findConnectionById(connections: Connection[], connectionId: string) { )[0] } -function responseStatus(error: unknown) { - if (error instanceof Response) return error.status - if (!error || typeof error !== 'object') return undefined - if ('status' in error && typeof error.status === 'number') return error.status - if ('data' in error && error.data && typeof error.data === 'object' && 'status' in error.data) { - return typeof error.data.status === 'number' ? error.data.status : undefined - } -} - -function getAuthFields(provider: Provider, authKind: ConnectionAuthKind) { - const configurableFields = provider.configuration.filter( - (field) => - provider.id !== FIRECRAWL_PROVIDER_ID || !FIRECRAWL_FIXED_FIELD_NAMES.has(field.name), - ) - if (authKind === 'endpoint') { - return configurableFields.filter((field) => !field.secret) - } - - return configurableFields +function normalizeSourceType(value: string | null): SourceType { + if (value === 'onlineDocuments' || value === 'onlineDrive') return value + return 'websiteCrawl' } function getSupportedAuthKinds(provider: Provider) { + const fields = provider.configuration.filter( + (field) => !FIRECRAWL_FIXED_FIELD_NAMES.has(field.name), + ) const supported: ConnectionAuthKind[] = [] - if (provider.authKinds.includes('api-key') && getAuthFields(provider, 'api-key').length) + if (provider.authKinds.includes('api-key') && fields.some((field) => field.secret)) supported.push('api-key') if ( provider.authKinds.includes('endpoint') && - getAuthFields(provider, 'endpoint').some((field) => field.format === 'uri') + fields.some((field) => !field.secret && field.format === 'uri') ) supported.push('endpoint') return supported @@ -167,67 +164,40 @@ function SourceTypeSelector({ } function ProviderSelector({ - onNavigate, + provider, onChange, - providers, - value, }: { - onNavigate: (path: string) => void - onChange: (providerId: string) => void - providers: Provider[] - value?: string + provider: NewKnowledgeWebsiteProvider + onChange: (provider: NewKnowledgeWebsiteProvider) => void }) { - const { t } = useTranslation('dataset') + const { t } = useTranslation('datasetCreation') return (
- {t(($) => $['newKnowledge.providerLabel'])} -
- - {t(($) => $['newKnowledge.providerLabel'])} - - { - if ( - event.defaultPrevented || - event.button !== 0 || - event.metaKey || - event.ctrlKey || - event.shiftKey || - event.altKey - ) - return - event.preventDefault() - onNavigate(SOURCE_MARKETPLACE_PATH) - }} - > - {t(($) => $['newKnowledge.moreProviders'])} - - -
+ + {t(($) => $['stepOne.website.chooseProvider'])} +
- {providers.map((provider) => ( + {WEBSITE_PROVIDER_OPTIONS.map((option) => ( ))}
@@ -308,35 +278,33 @@ function ConnectionForm({ }: { knowledgeSpaceId: string onConnected: (connection: Connection) => void - onDraftChange?: (dirty: boolean) => void + onDraftChange: (dirty: boolean) => void onReconcile: () => Promise provider: Provider }) { const { t } = useTranslation('dataset') const supportedAuthKinds = getSupportedAuthKinds(provider) - const defaultAuthKind = supportedAuthKinds[0] ?? 'api-key' - const [authKind, setAuthKind] = useState(defaultAuthKind) + const [authKind, setAuthKind] = useState(supportedAuthKinds[0] ?? 'api-key') const [configuration, setConfiguration] = useState>({}) const [credentials, setCredentials] = useState>({}) const [error, setError] = useState(false) const [pending, setPending] = useState(false) - const visibleFields = getAuthFields(provider, authKind) + const configurableFields = provider.configuration.filter( + (field) => !FIRECRAWL_FIXED_FIELD_NAMES.has(field.name), + ) + const visibleFields = configurableFields.filter( + (field) => authKind === 'api-key' || (!field.secret && field.format === 'uri'), + ) const hasDraftChanges = - authKind !== defaultAuthKind || + authKind !== (supportedAuthKinds[0] ?? 'api-key') || Object.values(configuration).some((value) => Boolean(value.trim())) || Object.values(credentials).some((value) => Boolean(value.trim())) useEffect(() => { - onDraftChange?.(hasDraftChanges) + onDraftChange(hasDraftChanges) + return () => onDraftChange(false) }, [hasDraftChanges, onDraftChange]) - useEffect( - () => () => { - onDraftChange?.(false) - }, - [onDraftChange], - ) - const changeAuthKind = (nextAuthKind: ConnectionAuthKind) => { if (nextAuthKind !== authKind) setCredentials({}) setAuthKind(nextAuthKind) @@ -355,17 +323,14 @@ function ConnectionForm({ setError(false) setPending(true) try { - const fixedConfiguration = - provider.id === FIRECRAWL_PROVIDER_ID - ? Object.fromEntries( - provider.configuration - .filter((field) => FIRECRAWL_FIXED_FIELD_NAMES.has(field.name)) - .map((field) => [ - field.name, - FIRECRAWL_CONFIGURATION[field.name as keyof typeof FIRECRAWL_CONFIGURATION], - ]), - ) - : {} + const fixedConfiguration = Object.fromEntries( + provider.configuration + .filter((field) => FIRECRAWL_FIXED_FIELD_NAMES.has(field.name)) + .map((field) => [ + field.name, + FIRECRAWL_CONFIGURATION[field.name as keyof typeof FIRECRAWL_CONFIGURATION], + ]), + ) const safeConfiguration = { ...fixedConfiguration, ...Object.fromEntries( @@ -385,12 +350,13 @@ function ConnectionForm({ authKind, configuration: safeConfiguration, credentials: secretCredentials, - name: provider.displayName, + name: FIRECRAWL_CONNECTION_NAME, providerId: provider.id, }, params: { id: knowledgeSpaceId }, }) setCredentials({}) + onDraftChange(false) onConnected(createdConnection) } catch { setCredentials({}) @@ -444,15 +410,13 @@ function ConnectionForm({ {error && (

- {t(($) => $['newKnowledge.connectionFailed'], { - provider: provider.displayName, - })} + {t(($) => $['newKnowledge.connectionFailed'])}

)} ) @@ -467,7 +431,7 @@ function UnconfiguredProvider({ }: { knowledgeSpaceId: string onConnected: (connection: Connection) => void - onDraftChange?: (dirty: boolean) => void + onDraftChange: (dirty: boolean) => void onReconcile: () => Promise provider: Provider }) { @@ -488,23 +452,22 @@ function UnconfiguredProvider({ return (
- +

{t(($) => $['newKnowledge.providerNotConfigured'], { - provider: provider.displayName, + provider: FIRECRAWL_CONNECTION_NAME, })}

{t(($) => $['newKnowledge.providerNotConfiguredDescription'], { - provider: provider.displayName, + provider: FIRECRAWL_CONNECTION_NAME, })}

) @@ -515,24 +478,20 @@ function ConnectionProblem({ knowledgeSpaceId, onConnected, onReconcile, - onReconfigure, - provider, }: { connection: Connection knowledgeSpaceId: string onConnected: (connection: Connection) => void onReconcile: () => Promise - onReconfigure: () => Promise - provider: Provider }) { const { t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') - const [pendingAction, setPendingAction] = useState<'refresh' | 'reconfigure'>() + const [pending, setPending] = useState(false) const [error, setError] = useState(false) const refresh = async () => { - if (pendingAction) return - setPendingAction('refresh') + if (pending) return + setPending(true) setError(false) try { const refreshed = @@ -552,67 +511,36 @@ function ConnectionProblem({ } if (!reconciledConnection) setError(true) } finally { - setPendingAction(undefined) - } - } - - const reconfigure = async () => { - if (pendingAction) return - setPendingAction('reconfigure') - setError(false) - try { - await onReconfigure() - } catch { - setError(true) - } finally { - setPendingAction(undefined) + setPending(false) } } return (

- {t(($) => $['newKnowledge.connectionNeedsAttention'], { - provider: provider.displayName, - })} + {t(($) => $['newKnowledge.connectionNeedsAttention'])}

- {t(($) => $['newKnowledge.connectionNeedsAttentionDescription'], { - provider: provider.displayName, - })} + {t(($) => $['newKnowledge.connectionNeedsAttentionDescription'])}

{error && (

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

)} -
- - -
+
) } function ProvisioningConnection({ onReconcile, - provider, }: { onReconcile: () => Promise - provider: Provider }) { const { t } = useTranslation('dataset') const [pending, setPending] = useState(false) @@ -635,9 +563,7 @@ function ProvisioningConnection({ return (

- {t(($) => $['newKnowledge.connectionProvisioning'], { - provider: provider.displayName, - })} + {t(($) => $['newKnowledge.connectionProvisioning'])}

{error && (

@@ -651,42 +577,97 @@ function ProvisioningConnection({ ) } -export function AddSourcePage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) { +export function AddSourcePage({ + initialSourceDraft, + initialSourceType, + knowledgeSpaceId, + sourceDraftKey, +}: { + initialSourceDraft?: NewKnowledgeSourceDraft + initialSourceType?: string + knowledgeSpaceId: string + sourceDraftKey?: string +}) { const { t } = useTranslation('dataset') const router = useRouter() const queryClient = useQueryClient() - const [sourceType, setSourceType] = useState('websiteCrawl') - const [selectedProviderId, setSelectedProviderId] = useState() - const [ignoredConnectionIds, setIgnoredConnectionIds] = useState>(() => new Set()) - const [crawlDraftDirty, setCrawlDraftDirty] = useState(false) - const [provisionalSourceCount, setProvisionalSourceCount] = useState(0) + const initialDraftRef = useRef( + initialSourceDraft ?? + createNewKnowledgeSourceDraft(normalizeSourceType(initialSourceType ?? null)), + ) + const [sourceDraft, setSourceDraft] = useState(initialDraftRef.current) + const sourceDraftBaselineRef = useRef( + JSON.stringify(createNewKnowledgeSourceDraft(initialDraftRef.current.sourceType)), + ) + const [sourceDraftResolved, setSourceDraftResolved] = useState(!sourceDraftKey) + const [connectedSourceBoundaryVisible, setConnectedSourceBoundaryVisible] = useState(false) + const [connectionDraftDirty, setConnectionDraftDirty] = useState(false) const [exitOpen, setExitOpen] = useState(false) const [discarding, setDiscarding] = useState(false) const [discardError, setDiscardError] = useState(false) - const detailPath = newKnowledgeDetailPath(knowledgeSpaceId) - const provisionalSourcesRef = useRef(new Map()) - const pendingPreviewOperationsRef = useRef(new Set>()) - const deletionKeysRef = useRef(new Map()) + const [historyGuardReleaseVersion, setHistoryGuardReleaseVersion] = useState(0) const historyGuardArmedRef = useRef(false) + const historyGuardReleaseRef = useRef(false) const browserBackExitRef = useRef(false) const pendingNavigationRef = useRef(undefined) - const exitDestinationRef = useRef(detailPath) - const hasUnsavedChanges = Boolean( - crawlDraftDirty || - provisionalSourceCount || - sourceType !== 'websiteCrawl' || - selectedProviderId, - ) + const exitDestinationRef = useRef(newKnowledgeDetailPath(knowledgeSpaceId)) + const sourceDraftsRef = useRef< + Partial> + >({ [sourceDraft.sourceType]: sourceDraft }) + const sourceType = sourceDraft.sourceType + const websiteSourceSelected = + sourceDraft.sourceType === 'websiteCrawl' && sourceDraft.provider === FIRECRAWL_CONNECTION_NAME + const detailPath = newKnowledgeDetailPath(knowledgeSpaceId) + const updateSourceDraft = (draft: NewKnowledgeSourceDraft) => { + sourceDraftsRef.current[draft.sourceType] = draft + setSourceDraft(draft) + } + + useEffect(() => { + if (!sourceDraftKey) return undefined + let active = true + globalThis.queueMicrotask(() => { + if (!active) return + let draft: NewKnowledgeSourceDraft | undefined + try { + const storageKey = newKnowledgeSourceDraftStorageKey(sourceDraftKey) + const storedDraft = globalThis.sessionStorage.getItem(storageKey) + if (storedDraft) draft = parseNewKnowledgeSourceDraft(storedDraft) + } catch { + // Continue without the optional draft when browser storage is unavailable. + } + if (active) { + const nextDraft = + draft ?? createNewKnowledgeSourceDraft(normalizeSourceType(initialSourceType ?? null)) + sourceDraftsRef.current[nextDraft.sourceType] = nextDraft + setSourceDraft(nextDraft) + setSourceDraftResolved(true) + } + }) + return () => { + active = false + } + }, [initialSourceType, sourceDraftKey]) + const clearStoredSourceDraft = useCallback(() => { + if (!sourceDraftKey) return + try { + globalThis.sessionStorage.removeItem(newKnowledgeSourceDraftStorageKey(sourceDraftKey)) + } catch { + // The draft remains scoped to this browser session when storage cleanup is unavailable. + } + }, [sourceDraftKey]) const providersQuery = useQuery( consoleQuery.knowledgeFs.getSourceProviders.queryOptions({ input: {}, context: { silent: true }, + enabled: websiteSourceSelected, retry: false, }), ) const connectionsQuery = useInfiniteQuery( consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSourceConnections.infiniteOptions({ context: { silent: true }, + enabled: websiteSourceSelected, input: (pageParam) => ({ params: { id: knowledgeSpaceId }, query: { @@ -699,25 +680,12 @@ export function AddSourcePage({ knowledgeSpaceId }: { knowledgeSpaceId: string } retry: false, }), ) - const providers = useMemo( - () => - (providersQuery.data?.items ?? []).filter((candidate) => - candidate.capabilities.includes('website-crawl'), - ), - [providersQuery.data?.items], - ) - const provider = - providers.find((candidate) => candidate.id === selectedProviderId) ?? - providers.find((candidate) => candidate.id === FIRECRAWL_PROVIDER_ID) ?? - providers[0] + const provider = findFirecrawl(providersQuery.data?.items ?? []) const remoteConnections = connectionsQuery.data?.pages.flatMap((page) => page.items) ?? [] - const usableRemoteConnections = remoteConnections.filter( - (candidate) => candidate.status !== 'revoked' && !ignoredConnectionIds.has(candidate.id), - ) - const remoteConnection = findProviderConnection(usableRemoteConnections, provider?.id) + const remoteConnection = findProviderConnection(remoteConnections, provider?.id) const [connectionOverride, setConnectionOverride] = useState() const matchingRemoteConnection = connectionOverride - ? usableRemoteConnections.find((candidate) => candidate.id === connectionOverride.id) + ? remoteConnections.find((candidate) => candidate.id === connectionOverride.id) : undefined const connection = useMemo(() => { const localConnection = connectionOverride @@ -746,231 +714,9 @@ export function AddSourcePage({ knowledgeSpaceId }: { knowledgeSpaceId: string } refetch: refetchConnections, } = connectionsQuery - const armHistoryGuard = useCallback(() => { - globalThis.history.pushState(globalThis.history.state, '', globalThis.location.href) - historyGuardArmedRef.current = true - }, []) - - const replaceAfterHistoryGuard = useCallback( - (path: string) => { - if (!historyGuardArmedRef.current) { - router.replace(path) - return - } - - pendingNavigationRef.current = path - globalThis.history.back() - }, - [router], - ) - - const requestNavigation = useCallback( - (path: string) => { - if (discarding) return - if (hasUnsavedChanges) { - exitDestinationRef.current = path - browserBackExitRef.current = false - setDiscardError(false) - setExitOpen(true) - return - } - replaceAfterHistoryGuard(path) - }, - [discarding, hasUnsavedChanges, replaceAfterHistoryGuard], - ) - - useEffect(() => { - if ( - !hasUnsavedChanges || - historyGuardArmedRef.current || - browserBackExitRef.current || - pendingNavigationRef.current - ) - return - - armHistoryGuard() - }, [armHistoryGuard, hasUnsavedChanges]) - - useEffect(() => { - const handlePopState = () => { - if (!historyGuardArmedRef.current) return - - historyGuardArmedRef.current = false - const pendingNavigation = pendingNavigationRef.current - if (pendingNavigation) { - pendingNavigationRef.current = undefined - router.replace(pendingNavigation) - return - } - if (!hasUnsavedChanges) { - router.replace(detailPath) - return - } - - browserBackExitRef.current = true - exitDestinationRef.current = detailPath - setDiscardError(false) - setExitOpen(true) - } - - globalThis.addEventListener('popstate', handlePopState) - return () => globalThis.removeEventListener('popstate', handlePopState) - }, [detailPath, hasUnsavedChanges, router]) - - useEffect(() => { - if (!hasUnsavedChanges) return - - const handleBeforeUnload = (event: BeforeUnloadEvent) => { - event.preventDefault() - event.returnValue = '' - } - - globalThis.addEventListener('beforeunload', handleBeforeUnload) - return () => globalThis.removeEventListener('beforeunload', handleBeforeUnload) - }, [hasUnsavedChanges]) - - useEffect(() => { - if (!hasUnsavedChanges) return - - const handleDocumentClick = (event: MouseEvent) => { - if ( - event.defaultPrevented || - event.button !== 0 || - event.metaKey || - event.ctrlKey || - event.shiftKey || - event.altKey - ) - return - - const anchor = event - .composedPath() - .find((target): target is HTMLAnchorElement => target instanceof HTMLAnchorElement) - if ( - !anchor || - anchor.hasAttribute('download') || - (anchor.target && anchor.target !== '_self') - ) - return - - const destination = new URL(anchor.href, globalThis.location.href) - if (destination.origin !== globalThis.location.origin) return - - const current = new URL(globalThis.location.href) - if (destination.pathname === current.pathname && destination.search === current.search) return - - event.preventDefault() - requestNavigation(`${destination.pathname}${destination.search}${destination.hash}`) - } - - document.addEventListener('click', handleDocumentClick, true) - return () => document.removeEventListener('click', handleDocumentClick, true) - }, [hasUnsavedChanges, requestNavigation]) - - const rememberProvisionalSource = useCallback((source: Source) => { - provisionalSourcesRef.current.set(source.id, source) - setProvisionalSourceCount(provisionalSourcesRef.current.size) - }, []) - - const getDeletionKey = useCallback((sourceId: string) => { - let idempotencyKey = deletionKeysRef.current.get(sourceId) - if (!idempotencyKey) { - idempotencyKey = globalThis.crypto.randomUUID() - deletionKeysRef.current.set(sourceId, idempotencyKey) - } - return idempotencyKey - }, []) - - const rememberPendingPreviewOperation = useCallback((operation: Promise) => { - pendingPreviewOperationsRef.current.add(operation) - void operation.then( - () => pendingPreviewOperationsRef.current.delete(operation), - () => pendingPreviewOperationsRef.current.delete(operation), - ) - }, []) - - const requestExit = () => requestNavigation(detailPath) - - const cancelExit = () => { - setExitOpen(false) - setDiscardError(false) - exitDestinationRef.current = detailPath - if (!browserBackExitRef.current) return - - browserBackExitRef.current = false - armHistoryGuard() - } - - const confirmExit = async () => { - if (discarding) return - setDiscarding(true) - setDiscardError(false) - try { - await Promise.allSettled([...pendingPreviewOperationsRef.current]) - for (const source of provisionalSourcesRef.current.values()) { - try { - await consoleClient.knowledgeFs.deleteKnowledgeSpacesByIdSourcesBySourceId({ - body: { expectedRevision: source.version ?? 1 }, - headers: { 'idempotency-key': getDeletionKey(source.id) }, - params: { id: knowledgeSpaceId, sourceId: source.id }, - query: { documents: 'cascade' }, - }) - } catch (deletionError) { - let deletionConfirmed = false - try { - await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourcesBySourceId({ - params: { id: knowledgeSpaceId, sourceId: source.id }, - }) - } catch (reconciliationError) { - deletionConfirmed = responseStatus(reconciliationError) === 404 - } - if (!deletionConfirmed) throw deletionError - } - provisionalSourcesRef.current.delete(source.id) - deletionKeysRef.current.delete(source.id) - setProvisionalSourceCount(provisionalSourcesRef.current.size) - } - setCrawlDraftDirty(false) - setExitOpen(false) - browserBackExitRef.current = false - const exitDestination = exitDestinationRef.current - exitDestinationRef.current = detailPath - replaceAfterHistoryGuard(exitDestination) - } catch { - setDiscardError(true) - } finally { - setDiscarding(false) - } - } - - useEffect(() => { - const handlePageHide = () => { - for (const source of provisionalSourcesRef.current.values()) { - void consoleClient.knowledgeFs - .deleteKnowledgeSpacesByIdSourcesBySourceId( - { - body: { expectedRevision: source.version ?? 1 }, - headers: { 'idempotency-key': getDeletionKey(source.id) }, - params: { id: knowledgeSpaceId, sourceId: source.id }, - query: { documents: 'cascade' }, - }, - { - context: { - keepalive: true, - silent: true, - }, - }, - ) - .catch(() => {}) - } - } - - globalThis.addEventListener('pagehide', handlePageHide) - return () => globalThis.removeEventListener('pagehide', handlePageHide) - }, [getDeletionKey, knowledgeSpaceId]) - useEffect(() => { if ( + websiteSourceSelected && hasNextConnectionPage && !isFetchingNextConnectionPage && !connectionsQuery.isFetchNextPageError @@ -981,22 +727,12 @@ export function AddSourcePage({ knowledgeSpaceId }: { knowledgeSpaceId: string } fetchNextConnectionPage, hasNextConnectionPage, isFetchingNextConnectionPage, + websiteSourceSelected, ]) const rememberConnection = useCallback( (updatedConnection: Connection) => { - if (updatedConnection.status === 'revoked') { - setIgnoredConnectionIds((current) => new Set(current).add(updatedConnection.id)) - setConnectionOverride(undefined) - } else { - setIgnoredConnectionIds((current) => { - if (!current.has(updatedConnection.id)) return current - const next = new Set(current) - next.delete(updatedConnection.id) - return next - }) - setConnectionOverride(updatedConnection) - } + setConnectionOverride(updatedConnection) void queryClient.invalidateQueries({ queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSourceConnections.key(), }) @@ -1006,167 +742,323 @@ export function AddSourcePage({ knowledgeSpaceId }: { knowledgeSpaceId: string } const reconcileConnection = useCallback(async () => { if (connection) setConnectionOverride(connection) - const retryDelays = connection ? [0] : CONNECTION_RECONCILIATION_RETRY_DELAYS - let reconciliationError: Error | undefined - - for (const retryDelay of retryDelays) { - if (retryDelay) await new Promise((resolve) => setTimeout(resolve, retryDelay)) - try { - const refreshed = await refetchConnections() - if (refreshed.error) throw refreshed.error - reconciliationError = undefined - const refreshedConnections = ( - refreshed.data?.pages.flatMap((page) => page.items) ?? [] - ).filter( - (candidate) => candidate.status !== 'revoked' && !ignoredConnectionIds.has(candidate.id), - ) - const refreshedCurrentConnection = connection - ? findConnectionById(refreshedConnections, connection.id) - : undefined - const updatedConnection = connection - ? refreshedCurrentConnection - ? findConnectionById([connection, refreshedCurrentConnection], connection.id) - : undefined - : findProviderConnection(refreshedConnections, provider?.id) - if (updatedConnection) { - setConnectionOverride(updatedConnection) - return updatedConnection - } - } catch (error) { - reconciliationError = - error instanceof Error ? error : new Error('Connection reconciliation failed') - } - } - - if (reconciliationError) throw new Error(reconciliationError.message) - return undefined - }, [connection, ignoredConnectionIds, provider?.id, refetchConnections]) - - const reconfigureConnection = useCallback(async () => { - if (!connection) return - await consoleClient.knowledgeFs.deleteKnowledgeSpacesByIdSourceConnectionsByConnectionId({ - params: { connectionId: connection.id, id: knowledgeSpaceId }, - query: { expectedVersion: connection.version }, - }) - setIgnoredConnectionIds((current) => new Set(current).add(connection.id)) - setConnectionOverride(undefined) - await queryClient.invalidateQueries({ - queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSourceConnections.key(), - }) - }, [connection, knowledgeSpaceId, queryClient]) + const refreshed = await refetchConnections() + if (refreshed.error) throw refreshed.error + const refreshedConnections = refreshed.data?.pages.flatMap((page) => page.items) ?? [] + const refreshedCurrentConnection = connection + ? findConnectionById(refreshedConnections, connection.id) + : undefined + const updatedConnection = connection + ? refreshedCurrentConnection + ? findConnectionById([connection, refreshedCurrentConnection], connection.id) + : undefined + : findProviderConnection(refreshedConnections, provider?.id) + if (updatedConnection) setConnectionOverride(updatedConnection) + return updatedConnection + }, [connection, provider?.id, refetchConnections]) const loadingConnections = connectionsQuery.isPending || (!connectionsQuery.isFetchNextPageError && (connectionsQuery.hasNextPage || connectionsQuery.isFetchingNextPage)) - if (providersQuery.isPending || loadingConnections) + const queryError = + providersQuery.error || connectionsQuery.error || connectionsQuery.isFetchNextPageError + const websiteReady = Boolean( + websiteSourceSelected && + !queryError && + provider?.available && + supportsDirectConnection && + connection?.status === 'active', + ) + const websitePreviewReady = + websiteReady && !historyGuardArmedRef.current && !historyGuardReleaseRef.current + const hasUnsavedChanges = + sourceDraftResolved && + !websiteReady && + (connectionDraftDirty || JSON.stringify(sourceDraft) !== sourceDraftBaselineRef.current) + const armHistoryGuard = useCallback(() => { + globalThis.history.pushState(globalThis.history.state, '', globalThis.location.href) + historyGuardArmedRef.current = true + }, []) + const replaceAfterHistoryGuard = useCallback( + (path: string) => { + if (!historyGuardArmedRef.current) { + router.replace(path) + return + } + pendingNavigationRef.current = path + globalThis.history.back() + }, + [router], + ) + const requestNavigation = useCallback( + (path: string) => { + if (discarding) return + if (hasUnsavedChanges) { + exitDestinationRef.current = path + browserBackExitRef.current = false + setDiscardError(false) + setExitOpen(true) + return + } + clearStoredSourceDraft() + replaceAfterHistoryGuard(path) + }, + [clearStoredSourceDraft, discarding, hasUnsavedChanges, replaceAfterHistoryGuard], + ) + + useEffect(() => { + if ( + !hasUnsavedChanges || + historyGuardArmedRef.current || + browserBackExitRef.current || + pendingNavigationRef.current + ) + return + armHistoryGuard() + }, [armHistoryGuard, hasUnsavedChanges]) + + useEffect(() => { + const handlePopState = () => { + if (!historyGuardArmedRef.current) return + historyGuardArmedRef.current = false + if (historyGuardReleaseRef.current) { + historyGuardReleaseRef.current = false + setHistoryGuardReleaseVersion((version) => version + 1) + return + } + const pendingNavigation = pendingNavigationRef.current + if (pendingNavigation) { + pendingNavigationRef.current = undefined + router.replace(pendingNavigation) + return + } + if (!hasUnsavedChanges) { + clearStoredSourceDraft() + router.replace(detailPath) + return + } + browserBackExitRef.current = true + exitDestinationRef.current = detailPath + setDiscardError(false) + setExitOpen(true) + } + + globalThis.addEventListener('popstate', handlePopState) + return () => globalThis.removeEventListener('popstate', handlePopState) + }, [clearStoredSourceDraft, detailPath, hasUnsavedChanges, router]) + + useEffect(() => { + if ( + !websiteReady || + !historyGuardArmedRef.current || + historyGuardReleaseRef.current || + pendingNavigationRef.current + ) + return + historyGuardReleaseRef.current = true + globalThis.history.back() + }, [websiteReady]) + + useEffect(() => { + if (!hasUnsavedChanges) return + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault() + event.returnValue = '' + } + globalThis.addEventListener('beforeunload', handleBeforeUnload) + return () => globalThis.removeEventListener('beforeunload', handleBeforeUnload) + }, [hasUnsavedChanges]) + + useEffect(() => { + if (!hasUnsavedChanges) return + const handleDocumentClick = (event: MouseEvent) => { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return + const anchor = event + .composedPath() + .find((target): target is HTMLAnchorElement => target instanceof HTMLAnchorElement) + if ( + !anchor || + anchor.hasAttribute('download') || + (anchor.target && anchor.target !== '_self') + ) + return + const destination = new URL(anchor.href, globalThis.location.href) + if (destination.origin !== globalThis.location.origin) return + const current = new URL(globalThis.location.href) + if (destination.pathname === current.pathname && destination.search === current.search) return + event.preventDefault() + requestNavigation(`${destination.pathname}${destination.search}${destination.hash}`) + } + + document.addEventListener('click', handleDocumentClick, true) + return () => document.removeEventListener('click', handleDocumentClick, true) + }, [hasUnsavedChanges, requestNavigation]) + + const requestExit = () => requestNavigation(detailPath) + const cancelExit = () => { + setExitOpen(false) + setDiscardError(false) + if (!browserBackExitRef.current) return + browserBackExitRef.current = false + armHistoryGuard() + } + const confirmExit = () => { + if (discarding) return + setDiscarding(true) + setDiscardError(false) + try { + clearStoredSourceDraft() + sourceDraftBaselineRef.current = JSON.stringify(sourceDraft) + setConnectionDraftDirty(false) + setExitOpen(false) + browserBackExitRef.current = false + const destination = exitDestinationRef.current + exitDestinationRef.current = detailPath + replaceAfterHistoryGuard(destination) + } catch { + setDiscardError(true) + } finally { + setDiscarding(false) + } + } + + if ( + !sourceDraftResolved || + (websiteSourceSelected && (providersQuery.isPending || loadingConnections)) + ) return (

) - const queryError = - providersQuery.error || connectionsQuery.error || connectionsQuery.isFetchNextPageError - return ( -
-
-

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

-

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

-
-
- - {sourceType === 'websiteCrawl' ? ( - <> - - {queryError ? ( -
-

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

- -
- ) : !provider ? ( -
- {t(($) => $['newKnowledge.providerUnavailable'])} -
- ) : !provider.available || !supportsDirectConnection ? ( -
-

{provider.displayName}

-

- {provider.unavailableReason ?? t(($) => $['newKnowledge.providerUnavailable'])} -

-
- ) : connection?.status === 'active' ? ( - - ) : connection?.status === 'provisioning' ? ( - - ) : connection ? ( - - ) : ( - - )} - - ) : ( -
-

- {t(($) => $[`newKnowledge.${sourceType}`])} -

-

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

-
- )} -
- - {sourceType === 'websiteCrawl' ? ( + <> +
+
+

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

+

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

+
+
+ { + sourceDraftsRef.current[sourceDraft.sourceType] = sourceDraft + updateSourceDraft( + sourceDraftsRef.current[value] ?? createNewKnowledgeSourceDraft(value), + ) + setConnectedSourceBoundaryVisible(false) + }} + /> + {sourceDraft.sourceType === 'websiteCrawl' ? ( <> + { + updateSourceDraft({ ...sourceDraft, provider }) + setConnectedSourceBoundaryVisible(false) + }} + /> + {!websiteSourceSelected ? ( +
+

{sourceDraft.provider}

+

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

+
+ ) : queryError ? ( +
+

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

+ +
+ ) : !provider ? ( +
+ {t(($) => $['newKnowledge.firecrawlUnavailable'])} +
+ ) : !provider.available || !supportsDirectConnection ? ( +
+

+ {FIRECRAWL_CONNECTION_NAME} +

+

+ {provider.unavailableReason ?? t(($) => $['newKnowledge.providerUnavailable'])} +

+
+ ) : connection?.status === 'active' && websitePreviewReady ? ( + + ) : connection?.status === 'active' ? ( +
+ +
+ ) : connection?.status === 'provisioning' ? ( + + ) : connection ? ( + + ) : ( + + )} + {!websiteReady && ( + + )} + + ) : ( + { + updateSourceDraft(draft) + setConnectedSourceBoundaryVisible(false) + }} + /> + )} + {sourceType === 'websiteCrawl' && !websiteReady && ( +
+ {t(($) => $['newKnowledge.addSourceRequiresSelection'])} @@ -1177,21 +1069,39 @@ export function AddSourcePage({ knowledgeSpaceId }: { knowledgeSpaceId: string } > {t(($) => $['newKnowledge.addSource'])} - - ) : ( - +
+ )} + {sourceType !== 'websiteCrawl' && ( +
+ + +
+ )} + {connectedSourceBoundaryVisible && ( +

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

)}
-
+
void confirmExit()} + onConfirm={confirmExit} open={exitOpen} /> - + ) } diff --git a/web/features/new-rag/add-source-placeholder.tsx b/web/features/new-rag/add-source-placeholder.tsx new file mode 100644 index 00000000000..f877e11d529 --- /dev/null +++ b/web/features/new-rag/add-source-placeholder.tsx @@ -0,0 +1,269 @@ +'use client' + +import type { + NewKnowledgeOnlineDocumentsProvider, + NewKnowledgeOnlineDocumentsSourceDraft, + NewKnowledgeOnlineDriveProvider, + NewKnowledgeOnlineDriveSourceDraft, + NewKnowledgeSourceDraft, + NewKnowledgeWebsiteSourceDraft, +} from './routes' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + isValidWebsiteSourceDraft, + NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH, + NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH, +} from './routes' + +const connectedProviders = { + onlineDocuments: [ + { icon: 'i-custom-public-common-notion', label: 'Notion' }, + { icon: 'i-ri-file-text-line', label: 'Google Docs' }, + { icon: 'i-ri-links-line', label: 'Confluence' }, + ], + onlineDrive: [ + { icon: 'i-custom-public-common-google-drive', label: 'Google Drive' }, + { icon: 'i-ri-cloud-line', label: 'OneDrive' }, + { icon: 'i-ri-box-3-line', label: 'Amazon S3' }, + ], +} as const + +export function PendingWebsiteSetup({ + draft, + onDraftChange, +}: { + draft: NewKnowledgeWebsiteSourceDraft + onDraftChange: (draft: NewKnowledgeWebsiteSourceDraft) => void +}) { + const { t } = useTranslation('dataset') + const [optionsExpanded, setOptionsExpanded] = useState(false) + const [backendBoundaryVisible, setBackendBoundaryVisible] = useState(false) + const updateDraft = (nextDraft: NewKnowledgeWebsiteSourceDraft) => { + onDraftChange(nextDraft) + setBackendBoundaryVisible(false) + } + + return ( +
$['newKnowledge.crawlAndPreview'])}> +
+ + +
+
+ + {optionsExpanded && ( +
+ + +
+ )} +
+ +
+ + + +

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

+

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

+
+ {backendBoundaryVisible && ( +

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

+ )} +
+ ) +} + +export function UnavailableConnectedSourceSetup({ + draft, + onDraftChange, +}: { + draft: NewKnowledgeOnlineDocumentsSourceDraft | NewKnowledgeOnlineDriveSourceDraft + onDraftChange: (draft: NewKnowledgeSourceDraft) => void +}) { + const { t } = useTranslation('dataset') + const { t: tCreation } = useTranslation('datasetCreation') + const sourceType = draft.sourceType + const providers = connectedProviders[sourceType] + const activeProvider = draft.provider + const selectProvider = (provider: string) => { + if (draft.sourceType === 'onlineDocuments') + onDraftChange({ ...draft, provider: provider as NewKnowledgeOnlineDocumentsProvider }) + else onDraftChange({ ...draft, provider: provider as NewKnowledgeOnlineDriveProvider }) + } + + return ( +
+
+ + {tCreation(($) => $['stepOne.website.chooseProvider'])} + +
+ {providers.map((option) => ( + + ))} +
+
+ + {sourceType === 'onlineDrive' && ( +
$['newKnowledge.selectFilesAndFolders'])} + className="flex min-h-40 flex-col items-center justify-center rounded-lg border border-dashed border-divider-regular px-6 text-center" + > + + + +

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

+
+ )} + +
+ + +
+ +
+

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

+
+
+ ) +} diff --git a/web/features/new-rag/components/create-knowledge-dialog-parts.tsx b/web/features/new-rag/components/create-knowledge-dialog-parts.tsx index 3bafa360344..663f6dca37d 100644 --- a/web/features/new-rag/components/create-knowledge-dialog-parts.tsx +++ b/web/features/new-rag/components/create-knowledge-dialog-parts.tsx @@ -1,5 +1,6 @@ 'use client' +import type { ReactNode } from 'react' import type { NewKnowledgeStartMode } from '../routes' import { cn } from '@langgenius/dify-ui/cn' import { RadioControl, RadioItem } from '@langgenius/dify-ui/radio' @@ -7,15 +8,19 @@ import { useId } from 'react' import { useTranslation } from 'react-i18next' export function StartMode({ + children, description, disabled = false, icon, + selected = false, title, value, }: { + children?: ReactNode description: string disabled?: boolean icon: string + selected?: boolean title: string value: NewKnowledgeStartMode }) { @@ -25,44 +30,52 @@ export function StartMode({ const unavailableId = useId() return ( - } - aria-labelledby={titleId} - aria-describedby={disabled ? `${descriptionId} ${unavailableId}` : descriptionId} - disabled={disabled} +
- - - - - - - {title} + } + aria-labelledby={titleId} + aria-describedby={disabled ? `${descriptionId} ${unavailableId}` : descriptionId} + disabled={disabled} + className={cn( + 'relative flex min-h-16 w-full items-center gap-3 px-4 py-3.5 text-left outline-hidden', + 'hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset', + 'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent', + )} + > + + + - - {description} + + + {title} + + + {description} + - - {value === 'source' && ( - - )} - {disabled && ( - - {t(($) => $['cornerLabel.unavailable'])} - - )} - + {value === 'source' && ( + + )} + {disabled && ( + + {t(($) => $['cornerLabel.unavailable'])} + + )} + + {selected && children} +
) } diff --git a/web/features/new-rag/crawl-selection-form.tsx b/web/features/new-rag/crawl-selection-form.tsx new file mode 100644 index 00000000000..c46c4d73a3d --- /dev/null +++ b/web/features/new-rag/crawl-selection-form.tsx @@ -0,0 +1,675 @@ +'use client' + +import type { + GetKnowledgeSpacesByIdSourcesBySourceIdSyncPolicyResponse, + GetKnowledgeSpacesByIdSourceWorkflowsByRunIdPagesResponse, + PutKnowledgeSpacesByIdSourcesBySourceIdSyncPolicyData, + Source, + SourceWorkflowRun, +} from '@dify/contracts/knowledge-fs/types.gen' +import type { FormEvent } from 'react' +import { Button } from '@langgenius/dify-ui/button' +import { Checkbox } from '@langgenius/dify-ui/checkbox' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useId, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter } from '@/next/navigation' +import { consoleClient, consoleQuery } from '@/service/client' +import { createRequestId } from './request-id' +import { newKnowledgeDetailPath } from './routes' + +type PreviewPage = GetKnowledgeSpacesByIdSourceWorkflowsByRunIdPagesResponse['items'][number] +type SyncPolicy = GetKnowledgeSpacesByIdSourcesBySourceIdSyncPolicyResponse +type SyncMode = SyncPolicy['mode'] +type SyncPolicyBody = PutKnowledgeSpacesByIdSourcesBySourceIdSyncPolicyData['body'] + +const MIN_CUSTOM_INTERVAL_HOURS = 1 +const MAX_CUSTOM_INTERVAL_HOURS = 720 +const MAX_SELECTED_PAGES = 200 +const IMPORT_POLL_INTERVAL_MS = 1_000 +const IMPORT_POLL_ATTEMPTS = 120 +const SUCCESSFUL_IMPORT_STATES = new Set(['complete', 'completed', 'success', 'succeeded']) +const TERMINAL_IMPORT_STATES = new Set([ + ...SUCCESSFUL_IMPORT_STATES, + 'canceled', + 'cancelled', + 'error', + 'exhausted', + 'failed', + 'superseded', + 'timed_out', + 'timeout', + 'zero_results', +]) + +type PageSkipReason = 'failed' | 'off-domain' + +function requestStatus(error: unknown) { + if (error instanceof Response) return error.status + if (!error || typeof error !== 'object') return undefined + if ('status' in error && typeof error.status === 'number') return error.status + if ('data' in error && error.data && typeof error.data === 'object' && 'status' in error.data) + return typeof error.data.status === 'number' ? error.data.status : undefined +} + +function isDefinitiveRequestFailure(error: unknown) { + const status = requestStatus(error) + return status !== undefined && [400, 401, 403, 404, 409, 422, 429].includes(status) +} + +function normalizedWorkflowState(run: SourceWorkflowRun) { + return run.state.trim().toLowerCase().replaceAll('-', '_').replaceAll(' ', '_') +} + +function isSuccessfulImport(run: SourceWorkflowRun) { + return SUCCESSFUL_IMPORT_STATES.has(normalizedWorkflowState(run)) +} + +function isTerminalImport(run: SourceWorkflowRun) { + return TERMINAL_IMPORT_STATES.has(normalizedWorkflowState(run)) +} + +async function waitForImportTerminal( + knowledgeSpaceId: string, + initialRun: SourceWorkflowRun, + onWorkflowRun: (run: SourceWorkflowRun) => void, + discardRequested: () => boolean, +) { + let current = initialRun + for (let attempt = 0; attempt < IMPORT_POLL_ATTEMPTS; attempt += 1) { + if (discardRequested() || isTerminalImport(current)) return current + current = await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ + params: { id: knowledgeSpaceId, runId: current.id }, + }) + onWorkflowRun(current) + if (discardRequested() || isTerminalImport(current)) return current + await new Promise((resolve) => setTimeout(resolve, IMPORT_POLL_INTERVAL_MS)) + } + throw new Error('Source import did not reach a terminal state') +} + +function pageSkipReason(page: PreviewPage, rootUrl: string): PageSkipReason | undefined { + try { + const root = new URL(rootUrl) + const candidate = new URL(page.sourceUrl) + if ( + !['http:', 'https:'].includes(candidate.protocol) || + candidate.username || + candidate.password + ) + return 'failed' + if (candidate.hostname.toLocaleLowerCase() !== root.hostname.toLocaleLowerCase()) + return 'off-domain' + return undefined + } catch { + return 'failed' + } +} + +function policyConfiguration(mode: SyncMode, customIntervalHours: number) { + if (mode === 'manual') return { enabled: false, mode } as const + if (mode === 'custom') { + return { + customIntervalSeconds: customIntervalHours * 3600, + enabled: true, + mode, + } as const + } + return { enabled: true, mode } as const +} + +function policyMatches(policy: SyncPolicy, desired: ReturnType) { + return ( + policy.revision > 0 && + policy.enabled === desired.enabled && + policy.mode === desired.mode && + (desired.mode !== 'custom' || policy.customIntervalSeconds === desired.customIntervalSeconds) + ) +} + +function initialSyncPolicy(source: Source): SyncPolicy | undefined { + if (!source.version) return undefined + return { + createdAt: source.createdAt, + enabled: true, + expectedSourceVersion: source.version, + id: source.id, + knowledgeSpaceId: source.knowledgeSpaceId, + mode: 'provider', + revision: 0, + sourceId: source.id, + updatedAt: source.updatedAt, + } +} + +function PolicyLoading() { + const { t } = useTranslation('dataset') + return ( +
$['newKnowledge.loadingSyncPolicy'])} + className="space-y-3" + > +
+
+
+
+ ) +} + +function ReadyCrawlSelectionForm({ + busy, + discardRequested, + initialSyncMode, + knowledgeSpaceId, + onCancel, + onRecrawl, + onSubmissionUncertainChange, + onSubmitted, + onWorkflowPending, + onWorkflowRun, + pages, + policy, + rootUrl, + run, + source, + workflowUncertain, +}: { + busy: boolean + discardRequested: () => boolean + initialSyncMode?: SyncMode + knowledgeSpaceId: string + onCancel: () => void + onRecrawl: () => void + onSubmissionUncertainChange: (uncertain: boolean) => void + onSubmitted: () => Promise | void + onWorkflowPending: (request: Promise) => void + onWorkflowRun: (run: SourceWorkflowRun) => void + pages: PreviewPage[] + policy: SyncPolicy + rootUrl: string + run: SourceWorkflowRun + source: Source + workflowUncertain: boolean +}) { + const { t } = useTranslation('dataset') + const router = useRouter() + const queryClient = useQueryClient() + const customIntervalErrorId = 'crawl-custom-interval-error' + const pageDescriptionPrefixId = useId() + const pageSkipReasons = useMemo( + () => new Map(pages.map((page) => [page.pageId, pageSkipReason(page, rootUrl)])), + [pages, rootUrl], + ) + const selectablePages = useMemo( + () => pages.filter((page) => !pageSkipReasons.get(page.pageId)), + [pageSkipReasons, pages], + ) + const selectablePageIds = useMemo( + () => new Set(selectablePages.map((page) => page.pageId)), + [selectablePages], + ) + const bulkSelectablePages = selectablePages.slice(0, MAX_SELECTED_PAGES) + const [selectedPageIds, setSelectedPageIds] = useState>(() => new Set()) + const [syncMode, setSyncMode] = useState( + initialSyncMode ?? (policy.enabled ? policy.mode : 'manual'), + ) + const [customIntervalHours, setCustomIntervalHours] = useState( + policy.customIntervalSeconds ? policy.customIntervalSeconds / 3600 : MIN_CUSTOM_INTERVAL_HOURS, + ) + const [submitting, setSubmitting] = useState(false) + const [submitError, setSubmitError] = useState(false) + const [policyUncertain, setPolicyUncertain] = useState(false) + const [selectionUncertain, setSelectionUncertain] = useState(false) + const policySnapshotRef = useRef(policy) + const submissionPendingRef = useRef(false) + const selectionRequestRef = useRef<{ fingerprint: string; requestId: string } | undefined>( + undefined, + ) + const updatePolicy = useMutation( + consoleQuery.knowledgeFs.putKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy.mutationOptions(), + ) + const selectPages = useMutation( + consoleQuery.knowledgeFs.postKnowledgeSpacesByIdSourceWorkflowsByRunIdSelection.mutationOptions(), + ) + const allSelected = + bulkSelectablePages.length > 0 && + bulkSelectablePages.every((page) => selectedPageIds.has(page.pageId)) + const someSelected = selectedPageIds.size > 0 + const selectionAtLimit = selectedPageIds.size >= MAX_SELECTED_PAGES + const customIntervalValid = + typeof customIntervalHours === 'number' && + Number.isInteger(customIntervalHours) && + customIntervalHours >= MIN_CUSTOM_INTERVAL_HOURS && + customIntervalHours <= MAX_CUSTOM_INTERVAL_HOURS + const canSubmit = selectedPageIds.size > 0 && (syncMode !== 'custom' || customIntervalValid) + const formBusy = busy || submitting + const submissionLocked = formBusy || policyUncertain || selectionUncertain + const selectionLocked = submissionLocked || workflowUncertain + const updateSelectionUncertain = (uncertain: boolean) => { + setSelectionUncertain(uncertain) + onSubmissionUncertainChange(uncertain) + } + + const togglePage = (pageId: string) => { + if ( + !selectablePageIds.has(pageId) || + submissionPendingRef.current || + policyUncertain || + selectionUncertain + ) + return + setSelectedPageIds((current) => { + const next = new Set(current) + if (next.has(pageId)) next.delete(pageId) + else if (next.size < MAX_SELECTED_PAGES) next.add(pageId) + return next + }) + setSubmitError(false) + } + + const toggleAll = () => { + if (submissionPendingRef.current || policyUncertain || selectionUncertain) return + setSelectedPageIds( + allSelected ? new Set() : new Set(bulkSelectablePages.map((page) => page.pageId)), + ) + setSubmitError(false) + } + + const submit = async (event: FormEvent) => { + event.preventDefault() + if (!canSubmit || busy || submissionPendingRef.current) return + submissionPendingRef.current = true + setSubmitting(true) + setSubmitError(false) + const desiredPolicy = policyConfiguration( + syncMode, + typeof customIntervalHours === 'number' ? customIntervalHours : 0, + ) + const sortedPageIds = [...selectedPageIds].sort() + const fingerprint = JSON.stringify({ pageIds: sortedPageIds, policy: desiredPolicy }) + if ( + (policyUncertain || selectionUncertain) && + selectionRequestRef.current?.fingerprint !== fingerprint + ) { + submissionPendingRef.current = false + setSubmitting(false) + setSubmitError(true) + return + } + if (selectionRequestRef.current?.fingerprint !== fingerprint) { + selectionRequestRef.current = { + fingerprint, + requestId: createRequestId(), + } + } + + let resolveTransaction!: (run: SourceWorkflowRun | undefined) => void + let transactionRun: SourceWorkflowRun | undefined + const transaction = new Promise((resolve) => { + resolveTransaction = resolve + }) + onWorkflowPending(transaction) + + try { + let currentPolicy = policySnapshotRef.current + if (!policyMatches(currentPolicy, desiredPolicy)) { + const body: SyncPolicyBody = { + ...desiredPolicy, + expectedRevision: currentPolicy.revision, + expectedSourceVersion: currentPolicy.expectedSourceVersion, + } + try { + currentPolicy = await updatePolicy.mutateAsync({ + body, + params: { id: knowledgeSpaceId, sourceId: source.id }, + }) + } catch (error) { + let reconciled: SyncPolicy + try { + reconciled = + await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy({ + params: { id: knowledgeSpaceId, sourceId: source.id }, + }) + } catch (reconciliationError) { + setPolicyUncertain(!isDefinitiveRequestFailure(error)) + throw reconciliationError + } + policySnapshotRef.current = reconciled + if (!policyMatches(reconciled, desiredPolicy)) { + setPolicyUncertain(!isDefinitiveRequestFailure(error)) + throw error + } + currentPolicy = reconciled + } + policySnapshotRef.current = currentPolicy + } + setPolicyUncertain(false) + + if (discardRequested()) return + + try { + const selectionRequest = selectPages.mutateAsync({ + body: { pageIds: sortedPageIds }, + headers: { 'Idempotency-Key': selectionRequestRef.current.requestId }, + params: { id: knowledgeSpaceId, runId: run.id }, + }) + const selectionRun = await selectionRequest + transactionRun = selectionRun + onWorkflowRun(selectionRun) + } catch (error) { + updateSelectionUncertain(!isDefinitiveRequestFailure(error)) + throw error + } + updateSelectionUncertain(true) + if (discardRequested()) return + const terminalRun = await waitForImportTerminal( + knowledgeSpaceId, + transactionRun, + onWorkflowRun, + discardRequested, + ) + transactionRun = terminalRun + if (discardRequested()) return + if (!isSuccessfulImport(terminalRun)) { + selectionRequestRef.current = undefined + updateSelectionUncertain(false) + throw new Error('Source import failed') + } + updateSelectionUncertain(false) + await queryClient.invalidateQueries({ + queryKey: consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSources.key(), + }) + if (discardRequested()) return + await onSubmitted() + router.push(newKnowledgeDetailPath(knowledgeSpaceId)) + } catch { + setSubmitError(true) + } finally { + resolveTransaction(transactionRun) + submissionPendingRef.current = false + setSubmitting(false) + } + } + + return ( +
void submit(event)}> +
+
+

+ {t(($) => $['newKnowledge.pagesCrawled'], { + count: pages.length, + host: new URL(rootUrl).host, + })} +

+ + {t(($) => $['newKnowledge.pagesSelected'], { count: selectedPageIds.size })} + + {run.progressFailed > 0 && ( + + {run.progressFailed} {t(($) => $['newKnowledge.skippedFailed'])} + + )} + +
+
+ +
    + {pages.map((page, index) => { + const skipReason = pageSkipReasons.get(page.pageId) + const selectable = !skipReason + const selectionLimitReached = + selectable && selectionAtLimit && !selectedPageIds.has(page.pageId) + const titleId = `${pageDescriptionPrefixId}-title-${index}` + const urlId = `${pageDescriptionPrefixId}-url-${index}` + const reasonId = `${pageDescriptionPrefixId}-reason-${index}` + return ( +
  • + +
  • + ) + })} +
+
+
+ +
+ + {syncMode === 'custom' && ( + + )} +
+ + {submitError && ( +

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

+ )} +
+ + + {!selectedPageIds.size && ( + + {t(($) => $['newKnowledge.addSourceRequiresSelection'])} + + )} +
+
+ ) +} + +export function CrawlSelectionForm({ + busy = false, + discardRequested, + initialSyncMode, + knowledgeSpaceId, + onCancel, + onRecrawl, + onSubmissionUncertainChange, + onSubmitted, + onWorkflowPending, + onWorkflowRun, + pages, + rootUrl, + run, + source, + workflowUncertain = false, +}: { + busy?: boolean + discardRequested: () => boolean + initialSyncMode?: SyncMode + knowledgeSpaceId: string + onCancel: () => void + onRecrawl: () => void + onSubmissionUncertainChange: (uncertain: boolean) => void + onSubmitted: () => Promise | void + onWorkflowPending: (request: Promise) => void + onWorkflowRun: (run: SourceWorkflowRun) => void + pages: PreviewPage[] + rootUrl: string + run: SourceWorkflowRun + source: Source + workflowUncertain?: boolean +}) { + const { t } = useTranslation('dataset') + const policyQuery = useQuery( + consoleQuery.knowledgeFs.getKnowledgeSpacesByIdSourcesBySourceIdSyncPolicy.queryOptions({ + context: { silent: true }, + input: { params: { id: knowledgeSpaceId, sourceId: source.id } }, + retry: false, + }), + ) + const policy = + policyQuery.data ?? + (requestStatus(policyQuery.error) === 404 ? initialSyncPolicy(source) : undefined) + + if (policyQuery.isPending) { + return ( +
+ +
+ + +
+
+ ) + } + if (!policy) { + return ( +
+
+

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

+ +
+
+ + +
+
+ ) + } + + return ( + + ) +} diff --git a/web/features/new-rag/create-knowledge-page.tsx b/web/features/new-rag/create-knowledge-page.tsx index ab32c1f15ec..c12777b8831 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 { NewKnowledgeSourceDraft, NewKnowledgeStartMode } from './routes' import { Button } from '@langgenius/dify-ui/button' import { Dialog, @@ -30,12 +31,13 @@ import { SelectTrigger, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' +import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { useRouter } from '@/next/navigation' +import { useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/client' import { DatasetACLPermission, hasPermission } from '@/utils/permission' import { KnowledgeIllustration, StartMode } from './components/create-knowledge-dialog-parts' @@ -47,12 +49,26 @@ import { KnowledgeCreationError, NAME_MAX_LENGTH, } from './create-knowledge-workflow' -import { newKnowledgeDetailPath, newKnowledgeListPath } from './routes' +import { CreateSourceSetup } from './create-source-setup' +import { createRequestId } from './request-id' +import { + createNewKnowledgeSourceDraft, + isValidWebsiteSourceDraft, + newKnowledgeAddSourcePath, + newKnowledgeDetailPath, + newKnowledgeListPath, + newKnowledgeSourceDraftStorageKey, +} from './routes' + +function normalizeStartMode(value: string | null): NewKnowledgeStartMode { + return value === 'source' ? value : 'empty' +} export function CreateKnowledgePage() { const { t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') const router = useRouter() + const searchParams = useSearchParams() const queryClient = useQueryClient() const dialogTitleId = useId() const permissionDescriptionId = useId() @@ -65,6 +81,14 @@ export function CreateKnowledgePage() { const [name, setName] = useState('') const [description, setDescription] = useState('') const [visibility, setVisibility] = useState(defaultVisibility) + const initialStartMode = normalizeStartMode(searchParams.get('start')) + const [startMode, setStartMode] = useState(initialStartMode) + const [sourceDraft, setSourceDraft] = useState(() => + createNewKnowledgeSourceDraft('websiteCrawl'), + ) + const sourceDraftsRef = useRef< + Partial> + >({}) const [createdKnowledge, setCreatedKnowledge] = useState() const [submissionLocked, setSubmissionLocked] = useState(false) const [exitReason, setExitReason] = useState(null) @@ -73,8 +97,25 @@ export function CreateKnowledgePage() { const browserBackExitRef = useRef(false) const pendingNavigationRef = useRef(undefined) const createMutation = useMutation({ mutationFn: createKnowledge }) + const sourceSubmissionBlocked = + startMode === 'source' && + (sourceDraft.sourceType === 'websiteCrawl' + ? !isValidWebsiteSourceDraft(sourceDraft) + : !sourceDraft.sourceName.trim()) + const sourceDraftChanged = Object.values({ + ...sourceDraftsRef.current, + [sourceDraft.sourceType]: sourceDraft, + }).some( + (draft) => + JSON.stringify(draft) !== JSON.stringify(createNewKnowledgeSourceDraft(draft.sourceType)), + ) const hasUnsavedChanges = Boolean( - name || description || visibility !== defaultVisibility || createdKnowledge, + name || + description || + visibility !== defaultVisibility || + startMode !== initialStartMode || + sourceDraftChanged || + createdKnowledge, ) const armHistoryGuard = useCallback(() => { @@ -153,7 +194,7 @@ export function CreateKnowledgePage() { setExitReason('partial') return } - if (name || description || visibility !== defaultVisibility) { + if (hasUnsavedChanges) { setExitReason('discard') return } @@ -181,13 +222,13 @@ export function CreateKnowledgePage() { } const handleSubmit = async () => { - if (createMutation.isPending) return + if (createMutation.isPending || sourceSubmissionBlocked) return const normalizedName = name.trim() const normalizedDescription = description.trim() if (!normalizedName) return - idempotencyKeyRef.current ??= globalThis.crypto.randomUUID() + idempotencyKeyRef.current ??= createRequestId() setSubmissionLocked(true) try { const created = await createMutation.mutateAsync({ @@ -203,6 +244,21 @@ export function CreateKnowledgePage() { }, visibility, }) + if (startMode === 'source') { + try { + const sourceDraftKey = createRequestId() + globalThis.sessionStorage.setItem( + newKnowledgeSourceDraftStorageKey(sourceDraftKey), + JSON.stringify(sourceDraft), + ) + replaceAfterHistoryGuard( + newKnowledgeAddSourcePath(created.id, sourceDraft.sourceType, sourceDraftKey), + ) + } catch { + toast.error(t(($) => $['newKnowledge.addSourceFailed'])) + } + return + } replaceAfterHistoryGuard(newKnowledgeDetailPath(created.id)) } catch (error) { if (error instanceof KnowledgeCreationError && error.createdKnowledge) @@ -246,7 +302,7 @@ export function CreateKnowledgePage() {
@@ -255,7 +311,7 @@ export function CreateKnowledgePage() {
-
+
{t(($) => $['newKnowledge.startWithHelp'])}

- + value={startMode} aria-label={t(($) => $['newKnowledge.startWith'])} className="mt-2 flex-col items-stretch gap-2" - disabled={createMutation.isPending} + disabled={submissionLocked} + onValueChange={(value) => { + setStartMode(value) + resetUnsubmittedError() + }} > $['newKnowledge.startEmpty'])} description={t(($) => $['newKnowledge.startEmptyDescription'])} /> $['newKnowledge.connectSource'])} description={t(($) => $['newKnowledge.connectSourceDescription'])} - /> + > + { + sourceDraftsRef.current[value.sourceType] = value + setSourceDraft(value) + resetUnsubmittedError() + }} + onSourceTypeChange={(value) => { + sourceDraftsRef.current[sourceDraft.sourceType] = sourceDraft + const nextDraft = + sourceDraftsRef.current[value] ?? createNewKnowledgeSourceDraft(value) + sourceDraftsRef.current[value] = nextDraft + setSourceDraft(nextDraft) + resetUnsubmittedError() + }} + /> + {tCommon(($) => $['operation.cancel'])} -
diff --git a/web/features/new-rag/create-source-setup.tsx b/web/features/new-rag/create-source-setup.tsx new file mode 100644 index 00000000000..d573329a702 --- /dev/null +++ b/web/features/new-rag/create-source-setup.tsx @@ -0,0 +1,391 @@ +'use client' + +import type { + NewKnowledgeOnlineDocumentsProvider, + NewKnowledgeOnlineDriveProvider, + NewKnowledgeSourceDraft, + NewKnowledgeWebsiteProvider, +} from './routes' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + isValidWebsiteSourceDraft, + NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH, + NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH, +} from './routes' + +const sourceTypes = [ + { icon: 'i-ri-global-line', value: 'websiteCrawl' }, + { icon: 'i-ri-file-text-line', value: 'onlineDocuments' }, + { icon: 'i-ri-hard-drive-3-line', value: 'onlineDrive' }, +] as const + +const providers = { + onlineDocuments: [ + { icon: 'i-custom-public-common-notion', label: 'Notion' }, + { icon: 'i-ri-file-text-line', label: 'Google Docs' }, + { icon: 'i-ri-links-line', label: 'Confluence' }, + ], + onlineDrive: [ + { icon: 'i-custom-public-common-google-drive', label: 'Google Drive' }, + { icon: 'i-ri-cloud-line', label: 'OneDrive' }, + { icon: 'i-ri-box-3-line', label: 'Amazon S3' }, + ], + websiteCrawl: [ + { icon: 'i-ri-fire-fill text-orange-500', label: 'Firecrawl', available: true }, + { icon: 'i-custom-public-llm-jina', label: 'Jina Reader' }, + { icon: 'i-ri-water-flash-line', label: 'WaterCrawl' }, + ], +} as const + +function ConnectedSourceConfiguration({ + disabled, + draft, + onDraftChange, +}: { + disabled: boolean + draft: NewKnowledgeSourceDraft + onDraftChange: (draft: NewKnowledgeSourceDraft) => void +}) { + const { t } = useTranslation('dataset') + + return ( +
+ + +
+ ) +} + +export function CreateSourceSetup({ + disabled, + draft, + onDraftChange, + onSourceTypeChange, +}: { + disabled: boolean + draft: NewKnowledgeSourceDraft + onDraftChange: (draft: NewKnowledgeSourceDraft) => void + onSourceTypeChange: (sourceType: NewKnowledgeSourceDraft['sourceType']) => void +}) { + const { t } = useTranslation('dataset') + const { t: tCreation } = useTranslation('datasetCreation') + const [optionsExpanded, setOptionsExpanded] = useState(false) + const [backendBoundaryVisible, setBackendBoundaryVisible] = useState(false) + const sourceType = draft.sourceType + const availableProviders = providers[sourceType] + const activeProvider = availableProviders.some((provider) => provider.label === draft.provider) + ? draft.provider + : availableProviders[0].label + const previewReady = draft.sourceType === 'websiteCrawl' && isValidWebsiteSourceDraft(draft) + const showBackendBoundary = () => setBackendBoundaryVisible(true) + const updateDraft = (nextDraft: NewKnowledgeSourceDraft) => { + onDraftChange(nextDraft) + setBackendBoundaryVisible(false) + } + const selectProvider = (provider: string) => { + if (draft.sourceType === 'onlineDocuments') + updateDraft({ ...draft, provider: provider as NewKnowledgeOnlineDocumentsProvider }) + else if (draft.sourceType === 'onlineDrive') + updateDraft({ ...draft, provider: provider as NewKnowledgeOnlineDriveProvider }) + else updateDraft({ ...draft, provider: provider as NewKnowledgeWebsiteProvider }) + } + + return ( +
+
+ + {t(($) => $['newKnowledge.sourceTypeLabel'])} + +
+ {sourceTypes.map((option) => ( + + ))} +
+
+ +
+ {tCreation(($) => $['stepOne.website.chooseProvider'])} +
+ + {tCreation(($) => $['stepOne.website.chooseProvider'])} + + +
+
+ {providers[sourceType].map((provider) => { + return ( + + ) + })} +
+
+ + {draft.sourceType === 'websiteCrawl' && ( +
+
+ + +
+
+ + {optionsExpanded && ( +
+ + +
+ )} +
+ +
$['newKnowledge.crawlPreview'])} + className="flex min-h-40 flex-col items-center justify-center rounded-lg border border-dashed border-divider-regular px-6 text-center" + > + + + +

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

+

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

+
+
+ )} + + {draft.sourceType !== 'websiteCrawl' && ( +
+ {draft.sourceType === 'onlineDocuments' && activeProvider === 'Notion' && ( +
+
+ provider.label === activeProvider)?.icon, + )} + /> +
+

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

+

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

+
+ +
+
+ )} + +
+ )} + + {backendBoundaryVisible && ( +

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

+ )} +
+ ) +} diff --git a/web/features/new-rag/request-id.ts b/web/features/new-rag/request-id.ts new file mode 100644 index 00000000000..64646f335d2 --- /dev/null +++ b/web/features/new-rag/request-id.ts @@ -0,0 +1,5 @@ +import { v4 as uuidV4 } from 'uuid' + +export function createRequestId() { + return globalThis.crypto?.randomUUID?.() ?? uuidV4() +} diff --git a/web/features/new-rag/routes.ts b/web/features/new-rag/routes.ts index 23b4c5bd95c..8644868d450 100644 --- a/web/features/new-rag/routes.ts +++ b/web/features/new-rag/routes.ts @@ -1,4 +1,179 @@ export type NewKnowledgeStartMode = 'empty' | 'source' | 'upload' +export type NewKnowledgeSourceType = 'onlineDocuments' | 'onlineDrive' | 'websiteCrawl' +type NewKnowledgeSyncPolicy = 'daily' | 'manual' | 'provider' +export type NewKnowledgeWebsiteProvider = 'Firecrawl' | 'Jina Reader' | 'WaterCrawl' +export type NewKnowledgeOnlineDocumentsProvider = 'Confluence' | 'Google Docs' | 'Notion' +export type NewKnowledgeOnlineDriveProvider = 'Amazon S3' | 'Google Drive' | 'OneDrive' + +type NewKnowledgeSourceDraftBase = { + sourceName: string + syncPolicy: NewKnowledgeSyncPolicy +} + +export type NewKnowledgeWebsiteSourceDraft = NewKnowledgeSourceDraftBase & { + includeSubpages: boolean + maxPages: number + provider: NewKnowledgeWebsiteProvider + rootUrl: string + sourceType: 'websiteCrawl' +} + +export type NewKnowledgeOnlineDocumentsSourceDraft = NewKnowledgeSourceDraftBase & { + provider: NewKnowledgeOnlineDocumentsProvider + sourceType: 'onlineDocuments' +} + +export type NewKnowledgeOnlineDriveSourceDraft = NewKnowledgeSourceDraftBase & { + provider: NewKnowledgeOnlineDriveProvider + sourceType: 'onlineDrive' +} + +export type NewKnowledgeSourceDraft = + | NewKnowledgeOnlineDocumentsSourceDraft + | NewKnowledgeOnlineDriveSourceDraft + | NewKnowledgeWebsiteSourceDraft + +export const NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH = 200 +export const NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH = 2048 +const NEW_KNOWLEDGE_SOURCE_DRAFT_STORAGE_PREFIX = 'new-knowledge-source-draft:' + +export function createNewKnowledgeSourceDraft( + sourceType: NewKnowledgeSourceType, +): NewKnowledgeSourceDraft { + if (sourceType === 'onlineDocuments') + return { + provider: 'Notion', + sourceName: '', + sourceType, + syncPolicy: 'provider', + } + if (sourceType === 'onlineDrive') + return { + provider: 'Google Drive', + sourceName: '', + sourceType, + syncPolicy: 'provider', + } + return { + includeSubpages: true, + maxPages: 100, + provider: 'Firecrawl', + rootUrl: '', + sourceName: '', + sourceType, + syncPolicy: 'provider', + } +} + +export function normalizeWebsiteSourceUrl(value: string) { + if (value.length > NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH) return undefined + try { + const url = new URL(value.trim()) + if ( + !['http:', 'https:'].includes(url.protocol) || + !url.hostname || + url.username || + url.password + ) + return undefined + url.hash = '' + return url + } catch { + return undefined + } +} + +export function isValidWebsiteSourceDraft( + draft: NewKnowledgeWebsiteSourceDraft, + { allowEmpty = false }: { allowEmpty?: boolean } = {}, +) { + const hasInput = Boolean( + draft.rootUrl.length || + draft.sourceName.length || + !draft.includeSubpages || + draft.maxPages !== 100, + ) + if (allowEmpty && !hasInput) return true + const sourceName = draft.sourceName.trim() + return Boolean( + normalizeWebsiteSourceUrl(draft.rootUrl) && + sourceName && + sourceName.length <= NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH && + Number.isInteger(draft.maxPages) && + draft.maxPages > 0 && + draft.maxPages <= 200, + ) +} + +export function newKnowledgeSourceDraftStorageKey(draftKey: string) { + return `${NEW_KNOWLEDGE_SOURCE_DRAFT_STORAGE_PREFIX}${draftKey}` +} + +export function parseNewKnowledgeSourceDraft(value: string): NewKnowledgeSourceDraft | undefined { + try { + const draft: unknown = JSON.parse(value) + if (!draft || typeof draft !== 'object') return undefined + const candidate = draft as Record + const syncPolicy = ['daily', 'manual', 'provider'].includes(String(candidate.syncPolicy)) + ? (candidate.syncPolicy as NewKnowledgeSyncPolicy) + : candidate.syncPolicy === undefined + ? 'provider' + : undefined + if ( + typeof candidate.sourceName !== 'string' || + candidate.sourceName.length > NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH || + !syncPolicy + ) + return undefined + if (candidate.sourceType === 'onlineDocuments') { + if (!['Confluence', 'Google Docs', 'Notion'].includes(String(candidate.provider))) + return undefined + return { + provider: candidate.provider as NewKnowledgeOnlineDocumentsProvider, + sourceName: candidate.sourceName, + sourceType: candidate.sourceType, + syncPolicy, + } + } + if (candidate.sourceType === 'onlineDrive') { + if (!['Amazon S3', 'Google Drive', 'OneDrive'].includes(String(candidate.provider))) + return undefined + return { + provider: candidate.provider as NewKnowledgeOnlineDriveProvider, + sourceName: candidate.sourceName, + sourceType: candidate.sourceType, + syncPolicy, + } + } + if ( + (candidate.sourceType !== undefined && candidate.sourceType !== 'websiteCrawl') || + !['Firecrawl', 'Jina Reader', 'WaterCrawl'].includes(String(candidate.provider)) || + typeof candidate.includeSubpages !== 'boolean' || + typeof candidate.maxPages !== 'number' || + !Number.isInteger(candidate.maxPages) || + candidate.maxPages < 1 || + candidate.maxPages > 200 || + typeof candidate.rootUrl !== 'string' || + candidate.rootUrl.length > NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH + ) + return undefined + return { + includeSubpages: candidate.includeSubpages, + maxPages: candidate.maxPages, + provider: candidate.provider as NewKnowledgeWebsiteProvider, + rootUrl: candidate.rootUrl, + sourceName: candidate.sourceName, + sourceType: 'websiteCrawl', + syncPolicy, + } + } catch { + return undefined + } +} + +export function singleSearchParam(value: string | string[] | undefined) { + return typeof value === 'string' ? value : undefined +} const newKnowledgeCreatePath = '/datasets/new/create' @@ -13,5 +188,14 @@ export const newKnowledgeDetailPath = (knowledgeSpaceId: string) => export const newKnowledgeDocumentsPath = (knowledgeSpaceId: string) => `/datasets/new/${knowledgeSpaceId}/documents` -export const newKnowledgeAddSourcePath = (knowledgeSpaceId: string) => - `/datasets/new/${knowledgeSpaceId}/sources/new` +export const newKnowledgeAddSourcePath = ( + knowledgeSpaceId: string, + sourceType?: NewKnowledgeSourceType, + draftKey?: string, +) => { + const searchParams = new URLSearchParams() + if (sourceType) searchParams.set('type', sourceType) + if (draftKey) searchParams.set('draft', draftKey) + const query = searchParams.toString() + return `/datasets/new/${knowledgeSpaceId}/sources/new${query ? `?${query}` : ''}` +} diff --git a/web/features/new-rag/sources-page.tsx b/web/features/new-rag/sources-page.tsx index 91eaa20e90e..ddc37abb246 100644 --- a/web/features/new-rag/sources-page.tsx +++ b/web/features/new-rag/sources-page.tsx @@ -55,6 +55,10 @@ function metadataString(metadata: Source['metadata'], key: string) { return typeof value === 'string' && value.trim() ? value : undefined } +function isPreviewDraft(source: Source) { + return source.metadata.preview === true && source.status === 'disabled' +} + function createIdempotencyKey() { return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}` } @@ -404,15 +408,15 @@ function SourcesEmpty({ return (
- - + + - - - + + +

{t(($) => $['newKnowledge.sourcesEmptyTitle'])} @@ -472,7 +476,12 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) () => (remoteSources ?? []) .filter((source) => !removedSourceIds.has(source.id)) - .map((source) => getCurrentSource(source, sourceOverrides[source.id])), + .map((source) => getCurrentSource(source, sourceOverrides[source.id])) + .filter((source) => !isPreviewDraft(source)) + .sort( + (left, right) => + right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id), + ), [remoteSources, removedSourceIds, sourceOverrides], ) const loadedPageCount = sourcesQuery.data?.pages.length ?? 0 @@ -492,8 +501,18 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) const localTransformActive = filter !== 'all' || Boolean(search.trim()) || Boolean(sort) const canAutoCompleteFilteredResults = localTransformActive && loadedPageCount < MAX_AUTO_FILTER_PAGES + const latestSourcePage = sourcesQuery.data?.pages.at(-1) + const needsVisibleSource = + latestSourcePage !== undefined && + latestSourcePage.items.some((source) => + isPreviewDraft(getCurrentSource(source, sourceOverrides[source.id])), + ) && + !latestSourcePage.items.some((source) => { + if (removedSourceIds.has(source.id)) return false + return !isPreviewDraft(getCurrentSource(source, sourceOverrides[source.id])) + }) const completingFilteredResults = - canAutoCompleteFilteredResults && + (canAutoCompleteFilteredResults || needsVisibleSource) && !sourcesQuery.isFetchNextPageError && (sourcesQuery.hasNextPage || sourcesQuery.isFetchingNextPage) const allFilteredSourcesSelected = @@ -510,7 +529,7 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) useEffect(() => { if ( - canAutoCompleteFilteredResults && + (canAutoCompleteFilteredResults || needsVisibleSource) && hasNextSourcePage && !isFetchingNextSourcePage && !sourcesQuery.isFetchNextPageError @@ -521,6 +540,7 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) fetchNextSourcePage, hasNextSourcePage, isFetchingNextSourcePage, + needsVisibleSource, sourcesQuery.isFetchNextPageError, ]) diff --git a/web/features/new-rag/website-crawl-preview.tsx b/web/features/new-rag/website-crawl-preview.tsx index 643392747ec..0897037f954 100644 --- a/web/features/new-rag/website-crawl-preview.tsx +++ b/web/features/new-rag/website-crawl-preview.tsx @@ -5,16 +5,31 @@ import type { Source, SourceWorkflowRun, } from '@dify/contracts/knowledge-fs/types.gen' +import type { FormEvent } from 'react' +import type { NewKnowledgeWebsiteSourceDraft } from './routes' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { Checkbox } from '@langgenius/dify-ui/checkbox' import { cn } from '@langgenius/dify-ui/cn' -import { Field, FieldError, FieldLabel } from '@langgenius/dify-ui/field' -import { Form } from '@langgenius/dify-ui/form' -import { Input } from '@langgenius/dify-ui/input' -import { NumberField, NumberFieldGroup, NumberFieldInput } from '@langgenius/dify-ui/number-field' import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useRouter } from '@/next/navigation' import { consoleClient } from '@/service/client' +import { CrawlSelectionForm } from './crawl-selection-form' +import { createRequestId } from './request-id' +import { + NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH, + NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH, + newKnowledgeDetailPath, + normalizeWebsiteSourceUrl, +} from './routes' type ConnectionReference = { id: string @@ -35,10 +50,11 @@ type PreviewDraft = { configurationKey: string creationAttempted?: boolean previewRequestId: string - reconciliationAttempts?: number source?: Source } +type PendingNavigation = { type: 'back' } | { href: string; type: 'push' } + function previewPagesEqual(left: PreviewPage, right: PreviewPage) { return ( left.pageId === right.pageId && @@ -53,9 +69,7 @@ const PAGE_SIZE = 200 const MAX_CURSOR_PAGES = 100 const POLL_INTERVAL_MS = 1500 const DEFAULT_PAGE_LIMIT = 100 -const MAX_PAGE_LIMIT = 1000 -const MAX_SOURCE_NAME_LENGTH = 200 -const MAX_SOURCE_RECONCILIATION_ATTEMPTS = 3 +const MAX_PAGE_LIMIT = 200 const SUCCESS_STATES = new Set(['complete', 'completed', 'preview_ready', 'success', 'succeeded']) const FAILURE_STATES = new Set(['error', 'exhausted', 'failed', 'timed_out', 'timeout']) const CANCELED_STATES = new Set(['canceled', 'cancelled', 'superseded']) @@ -80,23 +94,6 @@ function isTerminal(state: string) { return isSuccessful(state) || isFailed(state) || isCanceled(state) } -function normalizeURL(value: string) { - try { - const url = new URL(value.trim()) - if ( - !['http:', 'https:'].includes(url.protocol) || - !url.hostname || - url.username || - url.password - ) - return undefined - url.hash = '' - return url - } catch { - return undefined - } -} - function configurationKey(configuration: CrawlConfiguration) { return JSON.stringify(configuration) } @@ -123,16 +120,44 @@ function isRetryConfirmed(previous: SourceWorkflowRun, current: SourceWorkflowRu return ( previous.id === current.id && (current.executionAttempts > previous.executionAttempts || - (isTerminal(previous.state) && !isTerminal(current.state))) + current.updatedAt > previous.updatedAt || + current.checkpoint !== previous.checkpoint || + normalizedState(current.state) !== normalizedState(previous.state)) ) } -function isCancelConfirmed(current: SourceWorkflowRun) { - return isTerminal(current.state) +function sameWorkflowSnapshot(left: SourceWorkflowRun, right: SourceWorkflowRun) { + return ( + left.id === right.id && + left.executionAttempts === right.executionAttempts && + left.updatedAt === right.updatedAt && + left.checkpoint === right.checkpoint && + normalizedState(left.state) === normalizedState(right.state) + ) } -function createRequestId() { - return globalThis.crypto.randomUUID() +function isCancelConfirmed(target: SourceWorkflowRun, current: SourceWorkflowRun) { + return ( + target.id === current.id && + current.executionAttempts >= target.executionAttempts && + isTerminal(current.state) + ) +} + +function latestWorkflowRun( + current: SourceWorkflowRun | undefined, + candidate: SourceWorkflowRun | undefined, +) { + if (!current) return candidate + if (!candidate) return current + if (current.id !== candidate.id) return candidate + if (current.executionAttempts !== candidate.executionAttempts) + return current.executionAttempts > candidate.executionAttempts ? current : candidate + if (current.updatedAt !== candidate.updatedAt) + return current.updatedAt > candidate.updatedAt ? current : candidate + if (isTerminal(current.state) !== isTerminal(candidate.state)) + return isTerminal(current.state) ? current : candidate + return candidate } async function listWorkflowPageUpdates( @@ -225,10 +250,10 @@ const CrawlPageList = memo( aria-hidden className="flex items-start gap-2.5 px-4 py-2.5" > - + - - + + ))} @@ -256,35 +281,45 @@ function EmptyPreview() { export function WebsiteCrawlPreview({ connection, + initialDraft, knowledgeSpaceId, - onDraftChange, - onPendingOperation, - onProvisionalSource, - providerName, + onDraftFinished, + providerName = 'Firecrawl', }: { connection: ConnectionReference + initialDraft?: NewKnowledgeWebsiteSourceDraft knowledgeSpaceId: string - onDraftChange?: (dirty: boolean) => void - onPendingOperation?: (operation: Promise) => void - onProvisionalSource?: (source: Source) => void - providerName: string + onDraftFinished?: () => void + providerName?: string }) { const { t } = useTranslation('dataset') + const router = useRouter() const rootUrlErrorId = useId() - const [rootUrl, setRootUrl] = useState('') - const [sourceName, setSourceName] = useState('') + const [rootUrl, setRootUrl] = useState(initialDraft?.rootUrl ?? '') + const [sourceName, setSourceName] = useState(initialDraft?.sourceName ?? '') const [urlTouched, setUrlTouched] = useState(false) const [optionsExpanded, setOptionsExpanded] = useState(false) - const [includeSubpages, setIncludeSubpages] = useState(true) - const [pageLimit, setPageLimit] = useState(DEFAULT_PAGE_LIMIT) + const [includeSubpages, setIncludeSubpages] = useState(initialDraft?.includeSubpages ?? true) + const [pageLimit, setPageLimit] = useState(() => { + const initialLimit = initialDraft?.maxPages + return initialLimit && initialLimit > 0 && initialLimit <= MAX_PAGE_LIMIT + ? initialLimit + : DEFAULT_PAGE_LIMIT + }) const [run, setRun] = useState() - const [previewConfiguration, setPreviewConfiguration] = useState() const [pages, setPages] = useState([]) const [pagesLoaded, setPagesLoaded] = useState(false) const [starting, setStarting] = useState(false) const [stopping, setStopping] = useState(false) const [pollPaused, setPollPaused] = useState(false) const [requestError, setRequestError] = useState() + const [cancelConfirmationOpen, setCancelConfirmationOpen] = useState(false) + const [discarding, setDiscarding] = useState(false) + const [discardError, setDiscardError] = useState(false) + const [workflowUncertain, setWorkflowUncertain] = useState(false) + const [selectionUncertain, setSelectionUncertain] = useState(false) + const workflowStatusUncertainRef = useRef(false) + const selectionUncertainRef = useRef(false) const draftRef = useRef(undefined) const actionPendingRef = useRef(false) const retryFingerprintRef = useRef(undefined) @@ -293,6 +328,20 @@ export function WebsiteCrawlPreview({ const sourceNameInputRef = useRef(null) const pageMapRef = useRef(new Map()) const pageCursorRef = useRef(undefined) + const submittedRef = useRef(false) + const discardRequestedRef = useRef(false) + const pendingWorkflowPromiseRef = useRef | undefined>( + undefined, + ) + const pendingCancelRunRef = useRef(undefined) + const runRef = useRef(undefined) + const retryPredecessorRef = useRef(undefined) + const uncertainWorkflowRef = useRef<(() => Promise) | undefined>( + undefined, + ) + const pendingNavigationRef = useRef(undefined) + const historyGuardRef = useRef(undefined) + const historyGuardCompletionRef = useRef<(() => void) | undefined>(undefined) const resetPreviewPages = useCallback(() => { pageMapRef.current.clear() @@ -301,11 +350,41 @@ export function WebsiteCrawlPreview({ setPagesLoaded(false) }, []) - const normalizedURL = useMemo(() => normalizeURL(rootUrl), [rootUrl]) - const normalizedLimit = Math.min(Math.max(Math.trunc(pageLimit) || 1, 1), MAX_PAGE_LIMIT) + const updateRun = useCallback((nextRun: SourceWorkflowRun | undefined) => { + if (nextRun && pendingCancelRunRef.current?.id === nextRun.id) + pendingCancelRunRef.current = nextRun + runRef.current = nextRun + setRun(nextRun) + }, []) + + const trackPendingWorkflow = useCallback((request: Promise) => { + pendingWorkflowPromiseRef.current = request + void request.finally(() => { + if (pendingWorkflowPromiseRef.current === request) + pendingWorkflowPromiseRef.current = undefined + }) + }, []) + + const updateWorkflowUncertain = useCallback((uncertain: boolean) => { + workflowStatusUncertainRef.current = uncertain + setWorkflowUncertain(uncertain) + }, []) + + const updateSelectionUncertain = useCallback((uncertain: boolean) => { + selectionUncertainRef.current = uncertain + setSelectionUncertain(uncertain) + }, []) + + const normalizedURL = useMemo(() => normalizeWebsiteSourceUrl(rootUrl), [rootUrl]) + const normalizedLimit = + typeof pageLimit === 'number' + ? Math.min(Math.max(Math.trunc(pageLimit) || 1, 1), MAX_PAGE_LIMIT) + : DEFAULT_PAGE_LIMIT const configuration = useMemo( () => - normalizedURL && sourceName.trim() && sourceName.trim().length <= MAX_SOURCE_NAME_LENGTH + normalizedURL && + sourceName.trim() && + sourceName.trim().length <= NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH ? { includeSubpages, limit: normalizedLimit, @@ -316,41 +395,128 @@ export function WebsiteCrawlPreview({ [includeSubpages, normalizedLimit, normalizedURL, sourceName], ) const active = Boolean(run && !isTerminal(run.state)) + const successfulPreview = Boolean( + run && isSuccessful(run.state) && pagesLoaded && pages.length > 0, + ) + const uncertainOperation = workflowUncertain || selectionUncertain const shouldPoll = Boolean( run && !starting && !stopping && !pollPaused && (active || !pagesLoaded), ) const runId = run?.id - const locked = starting || stopping || active - const draftHost = normalizedURL?.host ?? '' - const previewHost = previewConfiguration ? new URL(previewConfiguration.url).host : draftHost + const locked = starting || stopping || active || successfulPreview || uncertainOperation + const dirty = Boolean( + rootUrl || sourceName || run || !includeSubpages || pageLimit !== DEFAULT_PAGE_LIMIT, + ) + const host = normalizedURL?.host ?? '' const completedCount = Math.max(run?.progressCompleted ?? 0, pages.length) const crawlingStatusText = t(($) => $['newKnowledge.crawlingPages'], { count: completedCount, - host: previewHost, + host, }) - const completedStatusText = t(($) => $['newKnowledge.pagesCrawled'], { - count: pages.length, - host: previewHost, - }) - const hasDraftChanges = Boolean( - rootUrl || - sourceName || - !includeSubpages || - pageLimit !== DEFAULT_PAGE_LIMIT || - run || - requestError, - ) useEffect(() => { - onDraftChange?.(hasDraftChanges) - }, [hasDraftChanges, onDraftChange]) + if (!dirty) return + const preventUnsavedUnload = (event: BeforeUnloadEvent) => { + if (submittedRef.current) return + event.preventDefault() + event.returnValue = '' + } + window.addEventListener('beforeunload', preventUnsavedUnload) + return () => window.removeEventListener('beforeunload', preventUnsavedUnload) + }, [dirty]) - useEffect( - () => () => { - onDraftChange?.(false) - }, - [onDraftChange], - ) + useEffect(() => { + if ((!dirty && !historyGuardRef.current) || submittedRef.current) return + const guardId = historyGuardRef.current ?? createRequestId() + const currentState = + window.history.state && typeof window.history.state === 'object' ? window.history.state : {} + if (!historyGuardRef.current) { + window.history.pushState( + { ...currentState, difyUnsavedSourceGuard: guardId }, + '', + location.href, + ) + historyGuardRef.current = guardId + } + + const handlePopState = () => { + if (submittedRef.current) { + historyGuardRef.current = undefined + const complete = historyGuardCompletionRef.current + historyGuardCompletionRef.current = undefined + complete?.() + return + } + if (!dirty) { + window.removeEventListener('popstate', handlePopState) + historyGuardRef.current = undefined + window.history.back() + return + } + window.history.pushState( + { ...currentState, difyUnsavedSourceGuard: guardId }, + '', + location.href, + ) + pendingNavigationRef.current = { type: 'back' } + setDiscardError(false) + setCancelConfirmationOpen(true) + } + const handleLinkClick = (event: MouseEvent) => { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return + const target = event.target + if (!(target instanceof Element)) return + const anchor = target.closest('a[href]') + if ( + !(anchor instanceof HTMLAnchorElement) || + anchor.target === '_blank' || + anchor.hasAttribute('download') + ) + return + const destination = new URL(anchor.href, location.href) + if (destination.origin !== location.origin || destination.href === location.href) return + event.preventDefault() + event.stopPropagation() + if (!dirty) { + submittedRef.current = true + onDraftFinished?.() + historyGuardCompletionRef.current = () => + router.push(`${destination.pathname}${destination.search}${destination.hash}`) + window.history.back() + return + } + pendingNavigationRef.current = { + href: `${destination.pathname}${destination.search}${destination.hash}`, + type: 'push', + } + setDiscardError(false) + setCancelConfirmationOpen(true) + } + + window.addEventListener('popstate', handlePopState) + document.addEventListener('click', handleLinkClick, true) + return () => { + window.removeEventListener('popstate', handlePopState) + document.removeEventListener('click', handleLinkClick, true) + } + }, [dirty, onDraftFinished, router]) + + const leaveHistoryGuard = useCallback((complete: () => void) => { + if (!historyGuardRef.current) { + complete() + return + } + historyGuardCompletionRef.current = complete + window.history.back() + }, []) const ensureProvisionalSource = useCallback( async (nextConfiguration: CrawlConfiguration) => { @@ -370,14 +536,11 @@ export function WebsiteCrawlPreview({ const reconciled = await findProvisionalSource(knowledgeSpaceId, draft.clientRequestId) if (reconciled) { draft.source = reconciled - onProvisionalSource?.(reconciled) + updateWorkflowUncertain(false) return draft } - draft.reconciliationAttempts = (draft.reconciliationAttempts ?? 0) + 1 - if (draft.reconciliationAttempts < MAX_SOURCE_RECONCILIATION_ATTEMPTS) - throw new Error('Provisional source creation is still reconciling') - draft.creationAttempted = false - draft.reconciliationAttempts = 0 + updateWorkflowUncertain(true) + throw new Error('Provisional source creation is still reconciling') } draft.creationAttempted = true @@ -401,117 +564,187 @@ export function WebsiteCrawlPreview({ }, params: { id: knowledgeSpaceId }, }) - onProvisionalSource?.(draft.source) + updateWorkflowUncertain(false) } catch (error) { if (isDefinitiveRequestFailure(error)) { draft.creationAttempted = false - draft.reconciliationAttempts = 0 + updateWorkflowUncertain(false) throw error } const reconciled = await findProvisionalSource(knowledgeSpaceId, draft.clientRequestId) if (!reconciled) { - draft.reconciliationAttempts = (draft.reconciliationAttempts ?? 0) + 1 + updateWorkflowUncertain(true) throw error } draft.source = reconciled - onProvisionalSource?.(reconciled) + updateWorkflowUncertain(false) } return draft }, - [connection.id, connection.providerId, knowledgeSpaceId, onProvisionalSource], + [connection.id, connection.providerId, knowledgeSpaceId, updateWorkflowUncertain], ) const startPreview = useCallback( - async (nextConfiguration: CrawlConfiguration, forceNewRun = false) => { - if (actionPendingRef.current) return + (nextConfiguration: CrawlConfiguration) => { + if (actionPendingRef.current) return undefined actionPendingRef.current = true + const request = (async () => { + setStarting(true) + setRequestError(undefined) + setPollPaused(false) + resetPreviewPages() + updateRun(undefined) + let draft: PreviewDraft | undefined + const existingUncertainWorkflow = uncertainWorkflowRef.current + try { + draft = await ensureProvisionalSource(nextConfiguration) + if (!draft.source) throw new Error('Provisional source is missing') + if (discardRequestedRef.current) return undefined + const nextRun = + await consoleClient.knowledgeFs.postKnowledgeSpacesByIdSourcesBySourceIdCrawlPreview({ + headers: { 'Idempotency-Key': draft.previewRequestId }, + params: { id: knowledgeSpaceId, sourceId: draft.source.id }, + }) + uncertainWorkflowRef.current = undefined + retryFingerprintRef.current = undefined + cancelFingerprintRef.current = undefined + retryPredecessorRef.current = undefined + updateWorkflowUncertain(false) + if (!discardRequestedRef.current) updateRun(nextRun) + return nextRun + } catch (error) { + if (!isDefinitiveRequestFailure(error) && draft?.source) { + const previewRequestId = draft.previewRequestId + const sourceId = draft.source.id + uncertainWorkflowRef.current = async () => { + try { + return await consoleClient.knowledgeFs.postKnowledgeSpacesByIdSourcesBySourceIdCrawlPreview( + { + headers: { 'Idempotency-Key': previewRequestId }, + params: { id: knowledgeSpaceId, sourceId }, + }, + ) + } catch { + return undefined + } + } + updateWorkflowUncertain(true) + } else if (isDefinitiveRequestFailure(error) && !existingUncertainWorkflow) { + uncertainWorkflowRef.current = undefined + updateWorkflowUncertain(false) + } + setRequestError('START_FAILED') + return undefined + } finally { + actionPendingRef.current = false + setStarting(false) + } + })() + pendingWorkflowPromiseRef.current = request + void request.finally(() => { + if (pendingWorkflowPromiseRef.current === request) + pendingWorkflowPromiseRef.current = undefined + }) + return request + }, + [ + ensureProvisionalSource, + knowledgeSpaceId, + resetPreviewPages, + updateRun, + updateWorkflowUncertain, + ], + ) + + const retryRun = useCallback(() => { + if (!run || actionPendingRef.current) return undefined + const attemptKey = workflowAttemptKey(run) + const retryAlreadySent = retryFingerprintRef.current === attemptKey + const existingUncertainWorkflow = uncertainWorkflowRef.current + actionPendingRef.current = true + const request = (async () => { setStarting(true) setRequestError(undefined) setPollPaused(false) - setPreviewConfiguration(nextConfiguration) - resetPreviewPages() - setRun(undefined) + const acceptRun = (nextRun: SourceWorkflowRun) => { + uncertainWorkflowRef.current = undefined + retryFingerprintRef.current = undefined + cancelFingerprintRef.current = undefined + retryPredecessorRef.current = run + updateWorkflowUncertain(false) + if (!discardRequestedRef.current) { + resetPreviewPages() + updateRun(nextRun) + } + return nextRun + } try { - const draft = await ensureProvisionalSource(nextConfiguration) - if (!draft.source) throw new Error('Provisional source is missing') - if (forceNewRun) draft.previewRequestId = createRequestId() - const nextRun = - await consoleClient.knowledgeFs.postKnowledgeSpacesByIdSourcesBySourceIdCrawlPreview({ - headers: { 'Idempotency-Key': draft.previewRequestId }, - params: { id: knowledgeSpaceId, sourceId: draft.source.id }, - }) - setRun(nextRun) - } catch { - setRequestError('START_FAILED') + if (retryAlreadySent) { + try { + const reconciled = + await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ + params: { id: knowledgeSpaceId, runId: run.id }, + }) + if (!isRetryConfirmed(run, reconciled)) { + setRequestError('RETRY_FAILED') + return undefined + } + return acceptRun(reconciled) + } catch { + setRequestError('RETRY_FAILED') + return undefined + } + } + + retryFingerprintRef.current = attemptKey + const reconcileRetry = async () => { + try { + const reconciled = + await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ + params: { id: knowledgeSpaceId, runId: run.id }, + }) + return isRetryConfirmed(run, reconciled) ? reconciled : undefined + } catch { + return undefined + } + } + try { + const retried = + await consoleClient.knowledgeFs.postKnowledgeSpacesByIdSourceWorkflowsByRunIdRetry({ + params: { id: knowledgeSpaceId, runId: run.id }, + }) + return acceptRun(retried) + } catch (error) { + if (isDefinitiveRequestFailure(error)) { + if (!existingUncertainWorkflow) { + retryFingerprintRef.current = undefined + uncertainWorkflowRef.current = undefined + updateWorkflowUncertain(false) + } + setRequestError('RETRY_FAILED') + return undefined + } + uncertainWorkflowRef.current = reconcileRetry + updateWorkflowUncertain(true) + const reconciled = await reconcileRetry() + if (!reconciled) { + setRequestError('RETRY_FAILED') + return undefined + } + return acceptRun(reconciled) + } } finally { actionPendingRef.current = false setStarting(false) } - }, - [ensureProvisionalSource, knowledgeSpaceId, resetPreviewPages], - ) - - const retryRun = useCallback(async () => { - if (!run || actionPendingRef.current) return - const attemptKey = workflowAttemptKey(run) - const retryAlreadySent = retryFingerprintRef.current === attemptKey - actionPendingRef.current = true - setStarting(true) - setRequestError(undefined) - setPollPaused(false) - try { - if (retryAlreadySent) { - try { - const reconciled = - await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ - params: { id: knowledgeSpaceId, runId: run.id }, - }) - if (!isRetryConfirmed(run, reconciled)) { - setRequestError('RETRY_FAILED') - return - } - resetPreviewPages() - setRun(reconciled) - } catch { - setRequestError('RETRY_FAILED') - } - return - } - - retryFingerprintRef.current = attemptKey - try { - const retried = - await consoleClient.knowledgeFs.postKnowledgeSpacesByIdSourceWorkflowsByRunIdRetry({ - params: { id: knowledgeSpaceId, runId: run.id }, - }) - resetPreviewPages() - setRun(retried) - } catch (error) { - if (isDefinitiveRequestFailure(error)) { - retryFingerprintRef.current = undefined - setRequestError('RETRY_FAILED') - return - } - try { - const reconciled = - await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ - params: { id: knowledgeSpaceId, runId: run.id }, - }) - if (!isRetryConfirmed(run, reconciled)) { - setRequestError('RETRY_FAILED') - } else { - resetPreviewPages() - setRun(reconciled) - } - } catch { - setRequestError('RETRY_FAILED') - } - } - } finally { - actionPendingRef.current = false - setStarting(false) - } - }, [knowledgeSpaceId, resetPreviewPages, run]) + })() + pendingWorkflowPromiseRef.current = request + void request.finally(() => { + if (pendingWorkflowPromiseRef.current === request) + pendingWorkflowPromiseRef.current = undefined + }) + return request + }, [knowledgeSpaceId, resetPreviewPages, run, updateRun, updateWorkflowUncertain]) useEffect(() => { if (!runId || !shouldPoll) return @@ -525,6 +758,24 @@ export function WebsiteCrawlPreview({ params: { id: knowledgeSpaceId, runId }, }) if (disposed) return + const currentRun = runRef.current + const retryPredecessor = retryPredecessorRef.current + if (retryPredecessor && sameWorkflowSnapshot(retryPredecessor, nextRun)) { + if (currentRun && !isTerminal(currentRun.state)) + timer = setTimeout(() => void poll(), POLL_INTERVAL_MS) + return + } + if ( + retryPredecessor && + (nextRun.executionAttempts > retryPredecessor.executionAttempts || + nextRun.updatedAt > retryPredecessor.updatedAt || + nextRun.checkpoint !== retryPredecessor.checkpoint) + ) + retryPredecessorRef.current = undefined + if (currentRun && latestWorkflowRun(currentRun, nextRun) === currentRun) { + if (!isTerminal(currentRun.state)) timer = setTimeout(() => void poll(), POLL_INTERVAL_MS) + return + } let pageUpdates: Awaited> const finalSnapshot = isSuccessful(nextRun.state) try { @@ -535,7 +786,9 @@ export function WebsiteCrawlPreview({ ) } catch (error) { if (isFailed(nextRun.state) || isCanceled(nextRun.state)) { - setRun(nextRun) + if (cancelFingerprintRef.current === workflowAttemptKey(nextRun)) + cancelFingerprintRef.current = undefined + updateRun(nextRun) setPagesLoaded(true) setRequestError(undefined) return @@ -564,7 +817,12 @@ export function WebsiteCrawlPreview({ } } pageCursorRef.current = pageUpdates.resumeCursor - setRun(nextRun) + if ( + isTerminal(nextRun.state) && + cancelFingerprintRef.current === workflowAttemptKey(nextRun) + ) + cancelFingerprintRef.current = undefined + updateRun(nextRun) if (pagesChanged) setPages([...pageMapRef.current.values()]) setPagesLoaded(true) setRequestError(undefined) @@ -581,11 +839,12 @@ export function WebsiteCrawlPreview({ disposed = true if (timer) clearTimeout(timer) } - }, [knowledgeSpaceId, runId, shouldPoll]) + }, [knowledgeSpaceId, runId, shouldPoll, updateRun]) - const stop = async () => { - if (!run || actionPendingRef.current || !active) return - const attemptKey = workflowAttemptKey(run) + const stop = async (targetRun = run) => { + if (!targetRun || isTerminal(targetRun.state)) return true + if (actionPendingRef.current) return false + const attemptKey = workflowAttemptKey(targetRun) const cancelAlreadySent = cancelFingerprintRef.current === attemptKey actionPendingRef.current = true setStopping(true) @@ -595,18 +854,19 @@ export function WebsiteCrawlPreview({ try { const reconciled = await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ - params: { id: knowledgeSpaceId, runId: run.id }, + params: { id: knowledgeSpaceId, runId: targetRun.id }, }) - if (!isCancelConfirmed(reconciled)) { + if (!isCancelConfirmed(targetRun, reconciled)) { setRequestError('CANCEL_FAILED') - return + return false } cancelFingerprintRef.current = undefined - setRun(reconciled) + updateRun(reconciled) + return true } catch { setRequestError('CANCEL_FAILED') + return false } - return } cancelFingerprintRef.current = attemptKey @@ -614,29 +874,33 @@ export function WebsiteCrawlPreview({ const canceled = await consoleClient.knowledgeFs.postKnowledgeSpacesByIdSourceWorkflowsByRunIdCancel({ body: { reason: 'user_requested' }, - params: { id: knowledgeSpaceId, runId: run.id }, + params: { id: knowledgeSpaceId, runId: targetRun.id }, }) cancelFingerprintRef.current = undefined - setRun(canceled) + updateRun(canceled) + return true } catch (error) { if (isDefinitiveRequestFailure(error)) { cancelFingerprintRef.current = undefined setRequestError('CANCEL_FAILED') - return + return false } try { const reconciled = await consoleClient.knowledgeFs.getKnowledgeSpacesByIdSourceWorkflowsByRunId({ - params: { id: knowledgeSpaceId, runId: run.id }, + params: { id: knowledgeSpaceId, runId: targetRun.id }, }) - if (!isCancelConfirmed(reconciled)) { + if (!isCancelConfirmed(targetRun, reconciled)) { setRequestError('CANCEL_FAILED') + return false } else { cancelFingerprintRef.current = undefined - setRun(reconciled) + updateRun(reconciled) + return true } } catch { setRequestError('CANCEL_FAILED') + return false } } } finally { @@ -645,11 +909,6 @@ export function WebsiteCrawlPreview({ } } - const dispatchOperation = (operation: Promise) => { - onPendingOperation?.(operation) - void operation - } - const handlePrimaryAction = () => { if (!configuration) { setUrlTouched(true) @@ -662,24 +921,20 @@ export function WebsiteCrawlPreview({ setPollPaused(false) return } - if (run && isSuccessful(run.state)) { - const currentKey = configurationKey(configuration) - dispatchOperation( - startPreview(configuration, draftRef.current?.configurationKey === currentKey), - ) - return - } - if (run && (isFailed(run.state) || isCanceled(run.state))) { + if (run && (isFailed(run.state) || isSuccessful(run.state) || isCanceled(run.state))) { const currentKey = configurationKey(configuration) if (draftRef.current?.configurationKey === currentKey) { - dispatchOperation(retryRun()) + void retryRun() return } } - dispatchOperation(startPreview(configuration)) + void startPreview(configuration) } - const handleSubmit = () => handlePrimaryAction() + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + handlePrimaryAction() + } const primaryLabel = starting || (active && !pollPaused) @@ -690,6 +945,8 @@ export function WebsiteCrawlPreview({ : run && isSuccessful(run.state) && pagesLoaded && pages.length === 0 ? t(($) => $['newKnowledge.adjustAndRecrawl']) : t(($) => $['newKnowledge.crawlAndPreview']) + const canReconcileUncertainOperation = + uncertainOperation && (requestError === 'START_FAILED' || requestError === 'RETRY_FAILED') const showFailure = Boolean( requestError === 'START_FAILED' || @@ -698,7 +955,7 @@ export function WebsiteCrawlPreview({ (run && isFailed(run.state)), ) const showZero = Boolean(run && isSuccessful(run.state) && pagesLoaded && pages.length === 0) - const showSuccess = Boolean(run && isSuccessful(run.state) && pages.length > 0) + const showSuccess = successfulPreview const showCanceled = Boolean(run && isCanceled(run.state)) const errorCode = run?.lastErrorCode ?? requestError const is403 = errorCode?.includes('403') @@ -707,70 +964,138 @@ export function WebsiteCrawlPreview({ (run ? ['timed_out', 'timeout'].includes(normalizedState(run.state)) : false) const isProviderError = errorCode?.toUpperCase().includes('PROVIDER') + const cancel = () => { + if (dirty) { + pendingNavigationRef.current = undefined + setDiscardError(false) + setCancelConfirmationOpen(true) + return + } + submittedRef.current = true + onDraftFinished?.() + leaveHistoryGuard(() => router.push(newKnowledgeDetailPath(knowledgeSpaceId))) + } + + const discardAndCancel = async () => { + if (discarding) return + setDiscarding(true) + setDiscardError(false) + discardRequestedRef.current = true + let pendingRun = await pendingWorkflowPromiseRef.current + if (!pendingRun && uncertainWorkflowRef.current) + pendingRun = await uncertainWorkflowRef.current() + if ( + !pendingRun && + (uncertainWorkflowRef.current || + ((workflowStatusUncertainRef.current || selectionUncertainRef.current) && + !pendingCancelRunRef.current)) + ) { + discardRequestedRef.current = false + setDiscarding(false) + setDiscardError(true) + return + } + if (pendingRun) { + uncertainWorkflowRef.current = undefined + updateWorkflowUncertain(false) + } + const runToCancel = pendingRun ?? pendingCancelRunRef.current ?? runRef.current + if (runToCancel && !isTerminal(runToCancel.state) && !(await stop(runToCancel))) { + pendingCancelRunRef.current = runToCancel + discardRequestedRef.current = false + resetPreviewPages() + setPollPaused(false) + updateRun(runToCancel) + setDiscarding(false) + setDiscardError(true) + return + } + pendingCancelRunRef.current = undefined + retryPredecessorRef.current = undefined + submittedRef.current = true + onDraftFinished?.() + setCancelConfirmationOpen(false) + const pendingNavigation = pendingNavigationRef.current + pendingNavigationRef.current = undefined + leaveHistoryGuard(() => { + if (pendingNavigation?.type === 'back') window.history.back() + else + router.push( + pendingNavigation?.type === 'push' + ? pendingNavigation.href + : newKnowledgeDetailPath(knowledgeSpaceId), + ) + }) + } + + const handleCancelConfirmationOpenChange = (open: boolean) => { + if (discarding) return + setCancelConfirmationOpen(open) + if (!open) { + pendingNavigationRef.current = undefined + setDiscardError(false) + } + } + return (
$['newKnowledge.crawlAndPreview'])}>

{t(($) => $['newKnowledge.providerConnected'], { provider: providerName })}

- -
+ +
- - + - * + + $['newKnowledge.rootUrlPlaceholder'])} + aria-invalid={urlTouched && !normalizedURL} + aria-describedby={urlTouched && !normalizedURL ? rootUrlErrorId : undefined} onBlur={() => setUrlTouched(true)} - onValueChange={setRootUrl} + onChange={(event) => setRootUrl(event.target.value)} + className={cn( + 'mt-1.5 h-9 w-full rounded-lg border-0 bg-components-input-bg-normal px-3 system-sm-regular text-text-primary outline-hidden focus:ring-2 focus:ring-state-accent-solid', + urlTouched && !normalizedURL && 'ring-1 ring-text-destructive', + )} /> - - {t(($) => $['newKnowledge.invalidRootUrl'])} - - - - - {t(($) => $['newKnowledge.sourceName'])} - - * + {urlTouched && !normalizedURL && ( + + {t(($) => $['newKnowledge.invalidRootUrl'])} - - + +
{optionsExpanded && (
- - - - {t(($) => $['newKnowledge.includeSubpages'])} - - - - + + - + setPageLimit(value ?? 1)} - > - - - - - + onBlur={() => { + if (pageLimit === '') setPageLimit(DEFAULT_PAGE_LIMIT) + }} + onChange={(event) => + setPageLimit( + Number.isFinite(event.target.valueAsNumber) + ? Math.min( + Math.max(Math.trunc(event.target.valueAsNumber), 1), + MAX_PAGE_LIMIT, + ) + : '', + ) + } + className="ml-auto h-8 w-24 rounded-lg border-0 bg-components-input-bg-normal px-2 system-xs-regular text-text-primary outline-hidden focus:ring-2 focus:ring-state-accent-solid" + /> +
)}
-
+

{!showSuccess && ( )} - +
{!run && !requestError && } @@ -847,7 +1178,7 @@ export function WebsiteCrawlPreview({
dispatchOperation(stop())} + onClick={() => void stop()} > {stopping ? t(($) => $['newKnowledge.stoppingCrawl']) @@ -879,38 +1210,42 @@ export function WebsiteCrawlPreview({ $['newKnowledge.crawlProgress'], { host: previewHost })} + aria-label={t(($) => $['newKnowledge.crawlProgress'], { host })} className="block h-1 w-full accent-state-accent-solid" /> )}
)} - {showSuccess && ( -
-
-

- {completedStatusText} -

- -
- -
+ {showSuccess && run && draftRef.current?.source && configuration && ( + discardRequestedRef.current} + initialSyncMode={ + initialDraft?.syncPolicy === 'daily' ? 'interval' : initialDraft?.syncPolicy + } + knowledgeSpaceId={knowledgeSpaceId} + onCancel={cancel} + onRecrawl={handlePrimaryAction} + onSubmissionUncertainChange={updateSelectionUncertain} + onSubmitted={() => + new Promise((resolve) => { + pendingNavigationRef.current = undefined + submittedRef.current = true + onDraftFinished?.() + leaveHistoryGuard(resolve) + }) + } + onWorkflowPending={trackPendingWorkflow} + onWorkflowRun={(nextRun) => { + pendingCancelRunRef.current = nextRun + }} + pages={pages} + rootUrl={configuration.url} + run={run} + source={draftRef.current.source} + workflowUncertain={workflowUncertain} + /> )} {showCanceled && (
@@ -931,7 +1266,7 @@ export function WebsiteCrawlPreview({ >

- {t(($) => $['newKnowledge.crawlFailed'], { host: previewHost })} + {t(($) => $['newKnowledge.crawlFailed'], { host })}

{is403 @@ -958,7 +1293,7 @@ export function WebsiteCrawlPreview({

- {t(($) => $['newKnowledge.noPagesFound'], { host: previewHost })} + {t(($) => $['newKnowledge.noPagesFound'], { host })}

{t(($) => $['newKnowledge.noPagesFoundDescription'])} @@ -966,6 +1301,48 @@ export function WebsiteCrawlPreview({

)}
+ {!showSuccess && ( +
+ + + {t(($) => $['newKnowledge.addSourceRequiresSelection'])} + + +
+ )} + + +
+ + {t(($) => $['newKnowledge.discardSourceChanges'])} + + + {t(($) => $['newKnowledge.discardSourceChangesDescription'])} + + {discardError && ( +

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

+ )} +
+ + + {t(($) => $['newKnowledge.keepEditing'])} + + void discardAndCancel()} + > + {t(($) => $['newKnowledge.discardSourceChangesConfirm'])} + + +
+
) } diff --git a/web/i18n/ar-TN/dataset.json b/web/i18n/ar-TN/dataset.json index f21baac32c9..c71f2673509 100644 --- a/web/i18n/ar-TN/dataset.json +++ b/web/i18n/ar-TN/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "الإجراءات", "newKnowledge.addSource": "أضف المصدر", "newKnowledge.addSourceDescription": "قم بتوصيل مصدر بقاعدة المعرفة هذه - تتم مزامنته تلقائيًا ويظل محدثًا.", + "newKnowledge.addSourceFailed": "تعذّرت إضافة هذا المصدر. راجع الإعدادات وحاول مرة أخرى.", "newKnowledge.addSourceRequiresSelection": "حدّد صفحة واحدة على الأقل من الصفحات التي تم الزحف إليها قبل إضافة المصدر.", "newKnowledge.adjustAndRecrawl": "الضبط وإعادة الزحف", "newKnowledge.allSources": "جميع المصادر", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "إلغاء", "newKnowledge.cardType": "مساحة معرفة", "newKnowledge.configureProvider": "تكوين {{provider}}", + "newKnowledge.connectNotion": "ربط Notion", "newKnowledge.connectProvider": "توصيل {{provider}}", + "newKnowledge.connectProviderGeneric": "ربط المزوّد", "newKnowledge.connectSource": "ربط مصدر", "newKnowledge.connectSourceDescription": "يبقى المحتوى متزامنًا مع Notion والمواقع الإلكترونية والخدمات الأخرى.", "newKnowledge.connectingProvider": "جارٍ الاتصال…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "تعذّر على برنامج الزحف إكمال هذه المعاينة. تحقّق من عنوان URL أو عدّل خيارات الزحف أو أعد المحاولة.", "newKnowledge.crawlFailedProvider": "أبلغ {{provider}} عن خطأ. تحقّق من اتصال الموفّر وأعد المحاولة.", "newKnowledge.crawlFailedTimeout": "انتهت مهلة الزحف. جرّب عنوان URL أكثر تحديدًا أو قلّل الحد الأقصى لعدد الصفحات.", + "newKnowledge.crawlNotStarted": "لم يبدأ", "newKnowledge.crawlOptions": "خيارات الزحف", + "newKnowledge.crawlPreview": "معاينة الزحف", "newKnowledge.crawlProgress": "تقدّم الزحف إلى {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "يتم تعطيل تكوين الزحف إلى موقع الويب حتى يتوفر سير عمل الزحف.", "newKnowledge.crawlSetupUnavailableTitle": "الزحف الناري جاهز", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "جارٍ الزحف…", "newKnowledge.crawlingPages": "جارٍ الزحف إلى {{host}}… تم العثور على {{count}} صفحة", "newKnowledge.createFailed": "لم نتمكن من إنشاء قاعدة المعرفة هذه. تحقق من وصولك وحاول مرة أخرى.", + "newKnowledge.createIllustrationTitle": "اربط محتواك مرة واحدة — وسيجيب كل تطبيق بأحدث المعارف.", "newKnowledge.createTitle": "خلق المعرفة", "newKnowledge.creators": "المنشئون", + "newKnowledge.customIntervalHours": "فاصل زمني مخصّص (بالساعات)", + "newKnowledge.customIntervalInvalid": "أدخل عددًا صحيحًا من 1 إلى 720 ساعة.", "newKnowledge.description": "الوصف", "newKnowledge.descriptionHelp": "يساعد الوصف الواضح التطبيقات على تحديد وقت البحث في هذه المعرفة.", "newKnowledge.descriptionPlaceholder": "على سبيل المثال سياسة استرداد الأموال، والأسئلة الشائعة حول الشحن، وخطوات التصعيد - تُستخدم لطرح أسئلة دعم العملاء", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "تجاهل المسودة", "newKnowledge.discardDraftDescription": "ستُفقد تغييراتك على مسودة قاعدة المعرفة هذه.", "newKnowledge.discardDraftTitle": "تجاهل مسودة قاعدة المعرفة هذه؟", + "newKnowledge.discardSourceChanges": "هل تريد تجاهل تغييرات المصدر؟", + "newKnowledge.discardSourceChangesConfirm": "تجاهل التغييرات", + "newKnowledge.discardSourceChangesDescription": "ستفقد معاينة الزحف وتحديد الصفحات وإعدادات المزامنة.", "newKnowledge.discardSourceDraftDescription": "ستتم إزالة إعداد المصدر ومعاينة الزحف.", "newKnowledge.discardSourceDraftFailed": "تعذّر تجاهل مسودة المصدر. أعد المحاولة.", "newKnowledge.discardSourceDraftTitle": "هل تريد تجاهل مسودة المصدر هذه؟", "newKnowledge.documents": "المستندات", "newKnowledge.documentsUnavailable": "— مستندات", + "newKnowledge.editSource": "تحرير", "newKnowledge.emptyDescription": "أضف مستنداتك ليتمكن الوكلاء من قراءتها مباشرة — من دون إعداد الفهرسة.", "newKnowledge.emptyTitle": "طريقة أذكى لبناء قواعد المعرفة", "newKnowledge.errorDescription": "تعذر تحميل قواعد المعرفة الجديدة. حاول مرة أخرى.", "newKnowledge.errorTitle": "حدث خطأ ما", "newKnowledge.filtersUnavailable": "سيصبح هذا المرشح متاحًا عندما يوفر KnowledgeFS بياناته الوصفية.", + "newKnowledge.firecrawlUnavailable": "الزحف الناري غير متوفر في كتالوج الموفر لمساحة العمل هذه.", "newKnowledge.gotIt": "فهمت", "newKnowledge.guideDescription": "اربط مصدرًا وستبقى مستنداته متزامنة ومفهرسة تلقائيًا وجاهزة للاستخدام، من دون إعدادات تحتاج إلى إدارة. ستواصل قواعد المعرفة السابقة العمل كما هي.", "newKnowledge.guideTitle": "تعرّف على قاعدة المعرفة الجديدة ✨", "newKnowledge.illustrationHeadline": "اربط محتواك مرة واحدة — ليجيب كل تطبيق بأحدث معرفة.", "newKnowledge.includeSubpages": "تضمين الصفحات الفرعية", "newKnowledge.invalidRootUrl": "أدخل عنوان URL صالحًا يبدأ بـ http(s).", + "newKnowledge.keepEditing": "متابعة التحرير", "newKnowledge.lastSyncColumn": "آخر مزامنة", "newKnowledge.learnMore": "معرفة المزيد", "newKnowledge.leavePartialSetupConfirm": "مغادرة الإعداد", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "مغادرة الإعداد غير المكتمل؟", "newKnowledge.legacy": "قديم", "newKnowledge.loadMore": "تحميل المزيد", + "newKnowledge.loadingSyncPolicy": "جارٍ تحميل سياسة المزامنة", "newKnowledge.maxPages": "الحد الأقصى للصفحات", - "newKnowledge.moreProviders": "عرض المزيد", + "newKnowledge.moreProviders": "مزوّدون آخرون", "newKnowledge.name": "اسم المعرفة", "newKnowledge.namePlaceholder": "على سبيل المثال دليل دعم العملاء", "newKnowledge.nameRequired": "أدخل اسم قاعدة المعرفة.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "وصل برنامج الزحف إلى الموقع لكنه لم يجد صفحات قابلة للفهرسة. جرّب عنوان URL مختلفًا أو وسّع خيارات الزحف.", "newKnowledge.notFoundDescription": "ربما تم حذف قاعدة المعرفة هذه أو ربما لا يمكنك الوصول إليها.", "newKnowledge.notFoundTitle": "لم يتم العثور على قاعدة المعرفة", + "newKnowledge.notionNotConnected": "Notion غير متصل", + "newKnowledge.notionNotConnectedDescription": "اربط Notion لاختيار الصفحات والحفاظ على مزامنتها.", "newKnowledge.onlineDocuments": "المستندات عبر الإنترنت", "newKnowledge.onlineDrive": "محرك الأقراص عبر الإنترنت", "newKnowledge.pagesAppearDescription": "ازحف إلى الموقع لمراجعة الصفحات التي ستتم إضافتها.", "newKnowledge.pagesAppearTitle": "ستظهر الصفحات هنا", "newKnowledge.pagesCrawled": "تم الزحف إلى {{count}} صفحة في {{host}}", + "newKnowledge.pagesCrawled_one": "تم الزحف إلى {{count}} صفحة في {{host}}", + "newKnowledge.pagesCrawled_other": "تم الزحف إلى {{count}} صفحة في {{host}}", + "newKnowledge.pagesSelected": "تم تحديد {{count}}", "newKnowledge.permission": "إذن", "newKnowledge.permissionAllMembers": "مساحة العمل · يمكن لجميع الأعضاء العرض والتحرير", "newKnowledge.permissionOnlyMe": "أنا فقط", "newKnowledge.permissionRestricted": "لا تسمح أذونات مساحة العمل الخاصة بك بتغيير هذا الإعداد.", "newKnowledge.permissionUpdateFailed": "تم إنشاء قاعدة المعرفة، لكن تعذّر تأكيد أذوناتها. أعد المحاولة لإكمال الإعداد.", + "newKnowledge.preview": "معاينة", + "newKnowledge.previewUnavailable": "تتوفر المعاينة بعد أن تنتهي الواجهة الخلفية من معالجة هذا الملف.", "newKnowledge.providerConnected": "تم توصيل {{provider}}", "newKnowledge.providerLabel": "اختر مزودًا", "newKnowledge.providerLoadFailed": "لم يتمكن الموفرون من التحميل", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "قم بإزالة المصدر", "newKnowledge.retryCrawl": "إعادة محاولة الزحف", "newKnowledge.retryProviderLoad": "أعد المحاولة", + "newKnowledge.retrySyncPolicy": "إعادة محاولة سياسة المزامنة", "newKnowledge.rootUrl": "عنوان URL الجذر", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "سيصبح هذا القسم متاحًا في خطوة الإعداد التالية.", "newKnowledge.searchSources": "مصادر البحث", + "newKnowledge.selectAll": "تحديد الكل", + "newKnowledge.selectAllSources": "تحديد كل المصادر", + "newKnowledge.selectFilesAndFolders": "حدد الملفات والمجلدات للمزامنة", + "newKnowledge.skippedFailed": "تم التخطي (فشل)", + "newKnowledge.skippedOffDomain": "تم التخطي (خارج النطاق)", "newKnowledge.sourceActions": "إجراءات {{name}}", "newKnowledge.sourceColumn": "المصدر", "newKnowledge.sourceFilterLabel": "تصفية المصادر حسب الحالة", "newKnowledge.sourceName": "اسم المصدر", "newKnowledge.sourceNamePlaceholder": "مثال: الزحف إلى وثائق المنتج", "newKnowledge.sourceNameRequired": "أدخل اسمًا للمصدر.", + "newKnowledge.sourceSetupBackendDependency": "يُعرض إعداد المصدر بالكامل؛ تتطلب إجراءات التعديل والمعاينة مسار الواجهة الخلفية لمصدر المسودة.", "newKnowledge.sourceStatus.active": "نشط", "newKnowledge.sourceStatus.disabled": "معطل", "newKnowledge.sourceStatus.error": "خطأ", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "إيقاف", "newKnowledge.stoppingCrawl": "جارٍ الإيقاف…", "newKnowledge.syncNow": "المزامنة الآن", + "newKnowledge.syncPolicy": "سياسة المزامنة", "newKnowledge.syncPolicyColumn": "سياسة المزامنة", + "newKnowledge.syncPolicyCustom": "فاصل زمني مخصّص", + "newKnowledge.syncPolicyDaily": "كل 24 ساعة", + "newKnowledge.syncPolicyLoadFailed": "تعذّر تحميل سياسة المزامنة.", + "newKnowledge.syncPolicyManual": "يدوي فقط", + "newKnowledge.syncPolicyProvider": "استخدام جدول المزوّد", "newKnowledge.tags": "الوسوم", "newKnowledge.unavailableDescription": "KnowledgeFS غير متاح لمساحة العمل هذه حاليًا.", "newKnowledge.unavailableTitle": "المعرفة الجديدة غير متاحة", "newKnowledge.updated": "تم التحديث في {{date}}", + "newKnowledge.uploadCharactersUnavailable": "الأحرف: —", "newKnowledge.uploadFiles": "رفع الملفات", "newKnowledge.uploadFilesDescription": "أضف ملفات من جهازك — ستتم فهرستها بمجرد إنشائها.", + "newKnowledge.uploadingFiles": "جارٍ الرفع…", "newKnowledge.usingDefaults": "استخدام الإعدادات الافتراضية", "newKnowledge.viewLabel": "عرض المعرفة", "newKnowledge.websiteCrawl": "الزحف إلى موقع الويب", diff --git a/web/i18n/de-DE/dataset.json b/web/i18n/de-DE/dataset.json index 84a3231095f..4b0f8fc5c61 100644 --- a/web/i18n/de-DE/dataset.json +++ b/web/i18n/de-DE/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Aktionen", "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.allSources": "Alle Quellen", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Abbrechen", "newKnowledge.cardType": "Wissensbereich", "newKnowledge.configureProvider": "{{provider}} konfigurieren", + "newKnowledge.connectNotion": "Notion verbinden", "newKnowledge.connectProvider": "Verbinden Sie {{provider}}", + "newKnowledge.connectProviderGeneric": "Anbieter verbinden", "newKnowledge.connectSource": "Quelle verbinden", "newKnowledge.connectSourceDescription": "Inhalte bleiben mit Notion, Websites und anderen Diensten synchron.", "newKnowledge.connectingProvider": "Verbinden…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Der Crawler konnte diese Vorschau nicht abschließen. Prüfe die URL, passe die Crawl-Optionen an oder versuche es erneut.", "newKnowledge.crawlFailedProvider": "{{provider}} hat einen Fehler gemeldet. Prüfe die Anbieterverbindung und versuche es erneut.", "newKnowledge.crawlFailedTimeout": "Zeitüberschreitung beim Crawlen. Versuche eine spezifischere URL oder reduziere die maximale Seitenzahl.", + "newKnowledge.crawlNotStarted": "Nicht gestartet", "newKnowledge.crawlOptions": "Crawl-Optionen", + "newKnowledge.crawlPreview": "Crawl-Vorschau", "newKnowledge.crawlProgress": "Crawl-Fortschritt für {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Die Website-Crawling-Konfiguration ist deaktiviert, bis der Crawl-Workflow verfügbar ist.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl ist bereit", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Wird gecrawlt…", "newKnowledge.crawlingPages": "{{host}} wird gecrawlt… {{count}} Seiten gefunden", "newKnowledge.createFailed": "Wir konnten diese Wissensdatenbank nicht erstellen. Überprüfen Sie Ihren Zugang und versuchen Sie es erneut.", + "newKnowledge.createIllustrationTitle": "Verbinde deine Inhalte einmal – jede App antwortet mit dem aktuellsten Wissen.", "newKnowledge.createTitle": "Wissen schaffen", "newKnowledge.creators": "Ersteller", + "newKnowledge.customIntervalHours": "Benutzerdefiniertes Intervall (Stunden)", + "newKnowledge.customIntervalInvalid": "Gib eine ganze Zahl zwischen 1 und 720 Stunden ein.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Entwurf verwerfen", "newKnowledge.discardDraftDescription": "Ihre Änderungen an diesem Wissensdatenbank-Entwurf gehen verloren.", "newKnowledge.discardDraftTitle": "Diesen Wissensdatenbank-Entwurf verwerfen?", + "newKnowledge.discardSourceChanges": "Änderungen an der Quelle verwerfen?", + "newKnowledge.discardSourceChangesConfirm": "Änderungen verwerfen", + "newKnowledge.discardSourceChangesDescription": "Crawl-Vorschau, Seitenauswahl und Synchronisierungseinstellungen gehen verloren.", "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.documents": "Dokumente", "newKnowledge.documentsUnavailable": "— Dokumente", + "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", "newKnowledge.errorDescription": "Die neuen Wissensdatenbanken konnten nicht geladen werden. Versuchen Sie es erneut.", "newKnowledge.errorTitle": "Etwas ist schiefgelaufen", "newKnowledge.filtersUnavailable": "Dieser Filter wird verfügbar, sobald KnowledgeFS Metadaten bereitstellt.", + "newKnowledge.firecrawlUnavailable": "Firecrawl ist im Anbieterkatalog für diesen Arbeitsbereich nicht verfügbar.", "newKnowledge.gotIt": "Verstanden", "newKnowledge.guideDescription": "Verbinden Sie eine Quelle, und ihre Dokumente bleiben synchron — automatisch indiziert und sofort einsatzbereit, ohne Verwaltungsaufwand. Ihre bisherigen Wissensdatenbanken funktionieren unverändert weiter.", "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.invalidRootUrl": "Gib eine gültige http(s)-URL ein.", + "newKnowledge.keepEditing": "Weiter bearbeiten", "newKnowledge.lastSyncColumn": "Letzte Synchronisierung", "newKnowledge.learnMore": "Mehr erfahren", "newKnowledge.leavePartialSetupConfirm": "Einrichtung verlassen", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Unvollständige Einrichtung verlassen?", "newKnowledge.legacy": "Legacy", "newKnowledge.loadMore": "Mehr laden", + "newKnowledge.loadingSyncPolicy": "Synchronisierungsrichtlinie wird geladen", "newKnowledge.maxPages": "Maximale Seitenzahl", - "newKnowledge.moreProviders": "Mehr anzeigen", + "newKnowledge.moreProviders": "Weitere Anbieter", "newKnowledge.name": "Wissensname", "newKnowledge.namePlaceholder": "z.B. Kundendiensthandbuch", "newKnowledge.nameRequired": "Geben Sie einen Namen für die Wissensdatenbank ein.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Der Crawler hat die Website erreicht, aber keine indexierbaren Seiten gefunden. Versuche eine andere URL oder erweitere die Crawl-Optionen.", "newKnowledge.notFoundDescription": "Diese Wissensdatenbank wurde möglicherweise gelöscht oder Sie haben keinen Zugriff darauf.", "newKnowledge.notFoundTitle": "Wissensdatenbank nicht gefunden", + "newKnowledge.notionNotConnected": "Notion ist nicht verbunden", + "newKnowledge.notionNotConnectedDescription": "Verbinden Sie Notion, um Seiten auszuwählen und synchron zu halten.", "newKnowledge.onlineDocuments": "Online-Dokumente", "newKnowledge.onlineDrive": "Online-Laufwerk", "newKnowledge.pagesAppearDescription": "Crawle die Website, um zu prüfen, welche Seiten hinzugefügt werden.", "newKnowledge.pagesAppearTitle": "Seiten werden hier angezeigt", "newKnowledge.pagesCrawled": "{{count}} Seiten unter {{host}} gecrawlt", + "newKnowledge.pagesCrawled_one": "{{count}} Seiten unter {{host}} gecrawlt", + "newKnowledge.pagesCrawled_other": "{{count}} Seiten unter {{host}} gecrawlt", + "newKnowledge.pagesSelected": "{{count}} ausgewählt", "newKnowledge.permission": "Erlaubnis", "newKnowledge.permissionAllMembers": "Arbeitsbereich · Alle Mitglieder können ihn ansehen und bearbeiten", "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.providerConnected": "{{provider}} verbunden", "newKnowledge.providerLabel": "Wählen Sie einen Anbieter", "newKnowledge.providerLoadFailed": "Anbieter konnten nicht geladen werden", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Quelle entfernen", "newKnowledge.retryCrawl": "Crawl wiederholen", "newKnowledge.retryProviderLoad": "Wiederholen", + "newKnowledge.retrySyncPolicy": "Synchronisierungsrichtlinie erneut laden", "newKnowledge.rootUrl": "Stamm-URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Dieser Abschnitt wird im nächsten Einrichtungsschritt verfügbar.", "newKnowledge.searchSources": "Quellen durchsuchen", + "newKnowledge.selectAll": "Alle auswählen", + "newKnowledge.selectAllSources": "Alle Quellen auswählen", + "newKnowledge.selectFilesAndFolders": "Dateien und Ordner zum Synchronisieren auswählen", + "newKnowledge.skippedFailed": "Übersprungen (fehlgeschlagen)", + "newKnowledge.skippedOffDomain": "Übersprungen (außerhalb der Domain)", "newKnowledge.sourceActions": "Aktionen für {{name}}", "newKnowledge.sourceColumn": "Quelle", "newKnowledge.sourceFilterLabel": "Quellen nach Status filtern", "newKnowledge.sourceName": "Quellenname", "newKnowledge.sourceNamePlaceholder": "z. B. Crawl der Produktdokumentation", "newKnowledge.sourceNameRequired": "Gib einen Quellennamen ein.", + "newKnowledge.sourceSetupBackendDependency": "Die Quelleneinrichtung wird vollständig angezeigt; Bearbeitung und Vorschau erfordern den Backend-Ablauf für Entwurfsquellen.", "newKnowledge.sourceStatus.active": "Aktiv", "newKnowledge.sourceStatus.disabled": "Deaktiviert", "newKnowledge.sourceStatus.error": "Fehler", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Stoppen", "newKnowledge.stoppingCrawl": "Wird gestoppt…", "newKnowledge.syncNow": "Jetzt synchronisieren", + "newKnowledge.syncPolicy": "Synchronisierungsrichtlinie", "newKnowledge.syncPolicyColumn": "Synchronisierungsrichtlinie", + "newKnowledge.syncPolicyCustom": "Benutzerdefiniertes Intervall", + "newKnowledge.syncPolicyDaily": "Alle 24 Stunden", + "newKnowledge.syncPolicyLoadFailed": "Die Synchronisierungsrichtlinie konnte nicht geladen werden.", + "newKnowledge.syncPolicyManual": "Nur manuell", + "newKnowledge.syncPolicyProvider": "Zeitplan des Anbieters verwenden", "newKnowledge.tags": "Tags", "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.uploadCharactersUnavailable": "Zeichen: —", "newKnowledge.uploadFiles": "Dateien hochladen", "newKnowledge.uploadFilesDescription": "Fügen Sie Dateien von Ihrem Computer hinzu — sie werden beim Erstellen sofort indiziert.", + "newKnowledge.uploadingFiles": "Wird hochgeladen…", "newKnowledge.usingDefaults": "Standardeinstellungen werden verwendet", "newKnowledge.viewLabel": "Wissensansicht", "newKnowledge.websiteCrawl": "Website-Crawling", diff --git a/web/i18n/en-US/dataset.json b/web/i18n/en-US/dataset.json index 8dfbc5eff0f..72175950b5e 100644 --- a/web/i18n/en-US/dataset.json +++ b/web/i18n/en-US/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Actions", "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.allSources": "All sources", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Cancel", "newKnowledge.cardType": "Knowledge space", "newKnowledge.configureProvider": "Configure {{provider}}", + "newKnowledge.connectNotion": "Connect Notion", "newKnowledge.connectProvider": "Connect {{provider}}", + "newKnowledge.connectProviderGeneric": "Connect provider", "newKnowledge.connectSource": "Connect a source", "newKnowledge.connectSourceDescription": "Content stays in sync with Notion, websites, and other services.", "newKnowledge.connectingProvider": "Connecting…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "The crawler couldn't finish this preview. Check the URL, adjust crawl options, or try again.", "newKnowledge.crawlFailedProvider": "{{provider}} reported an error. Check the provider connection and try again.", "newKnowledge.crawlFailedTimeout": "The crawl timed out. Try a narrower URL or reduce the maximum page count.", + "newKnowledge.crawlNotStarted": "Not started", "newKnowledge.crawlOptions": "Crawl options", + "newKnowledge.crawlPreview": "Crawl preview", "newKnowledge.crawlProgress": "Crawl progress for {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Website crawl configuration is disabled until the crawl workflow is available.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl is ready", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Crawling…", "newKnowledge.crawlingPages": "Crawling {{host}}… {{count}} pages found", "newKnowledge.createFailed": "We couldn't create this knowledge base. Check your access and try again.", + "newKnowledge.createIllustrationTitle": "Connect your content once — every app answers with the freshest knowledge.", "newKnowledge.createTitle": "Create Knowledge", "newKnowledge.creators": "Creators", + "newKnowledge.customIntervalHours": "Custom interval (hours)", + "newKnowledge.customIntervalInvalid": "Enter a whole number from 1 to 720 hours.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Discard draft", "newKnowledge.discardDraftDescription": "Your changes to this knowledge base draft will be lost.", "newKnowledge.discardDraftTitle": "Discard this knowledge base draft?", + "newKnowledge.discardSourceChanges": "Discard source changes?", + "newKnowledge.discardSourceChangesConfirm": "Discard changes", + "newKnowledge.discardSourceChangesDescription": "Your crawl preview, page selection, and sync settings will be lost.", "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.documents": "Documents", "newKnowledge.documentsUnavailable": "— documents", + "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", "newKnowledge.errorDescription": "We couldn't load your new knowledge bases. Try again.", "newKnowledge.errorTitle": "Something went wrong", "newKnowledge.filtersUnavailable": "This filter will become available when KnowledgeFS exposes its metadata.", + "newKnowledge.firecrawlUnavailable": "Firecrawl isn't available in the provider catalog for this workspace.", "newKnowledge.gotIt": "Got it", "newKnowledge.guideDescription": "Connect a Source and its Documents stay in sync — indexed automatically and ready to use, with no setup to manage. Your Legacy knowledge bases keep working as-is.", "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.invalidRootUrl": "Enter a valid http(s) URL.", + "newKnowledge.keepEditing": "Keep editing", "newKnowledge.lastSyncColumn": "Last sync", "newKnowledge.learnMore": "Learn more", "newKnowledge.leavePartialSetupConfirm": "Leave setup", @@ -205,6 +219,7 @@ "newKnowledge.leavePartialSetupTitle": "Leave incomplete setup?", "newKnowledge.legacy": "Legacy", "newKnowledge.loadMore": "Load more", + "newKnowledge.loadingSyncPolicy": "Loading sync policy", "newKnowledge.maxPages": "Maximum pages", "newKnowledge.moreProviders": "More providers", "newKnowledge.name": "Knowledge name", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "The crawler reached the site but found no indexable pages. Try a different URL, or expand crawl options.", "newKnowledge.notFoundDescription": "This knowledge base may have been deleted or you may not have access.", "newKnowledge.notFoundTitle": "Knowledge base not found", + "newKnowledge.notionNotConnected": "Notion isn't connected", + "newKnowledge.notionNotConnectedDescription": "Connect Notion to choose pages and keep them synchronized.", "newKnowledge.onlineDocuments": "Online documents", "newKnowledge.onlineDrive": "Online drive", "newKnowledge.pagesAppearDescription": "Crawl the site to review which pages get added.", "newKnowledge.pagesAppearTitle": "Pages will appear here", "newKnowledge.pagesCrawled": "{{count}} pages crawled at {{host}}", + "newKnowledge.pagesCrawled_one": "{{count}} page crawled at {{host}}", + "newKnowledge.pagesCrawled_other": "{{count}} pages crawled at {{host}}", + "newKnowledge.pagesSelected": "{{count}} selected", "newKnowledge.permission": "Permission", "newKnowledge.permissionAllMembers": "Workspace · all members can view and edit", "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.providerConnected": "{{provider}} connected", "newKnowledge.providerLabel": "Provider", "newKnowledge.providerLoadFailed": "Providers couldn't load", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Remove source", "newKnowledge.retryCrawl": "Retry crawl", "newKnowledge.retryProviderLoad": "Retry", + "newKnowledge.retrySyncPolicy": "Retry sync policy", "newKnowledge.rootUrl": "Root URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "This section will become available in the next setup step.", "newKnowledge.searchSources": "Search sources", + "newKnowledge.selectAll": "Select all", + "newKnowledge.selectAllSources": "Select all sources", + "newKnowledge.selectFilesAndFolders": "Select files & folders to sync", + "newKnowledge.skippedFailed": "Skipped (failed)", + "newKnowledge.skippedOffDomain": "Skipped (off-domain)", "newKnowledge.sourceActions": "Actions for {{name}}", "newKnowledge.sourceColumn": "Source", "newKnowledge.sourceFilterLabel": "Filter sources by status", "newKnowledge.sourceName": "Source name", "newKnowledge.sourceNamePlaceholder": "e.g. Product docs crawl", "newKnowledge.sourceNameRequired": "Enter a source name.", + "newKnowledge.sourceSetupBackendDependency": "Source setup is shown in full; editing and preview actions require the draft source backend workflow.", "newKnowledge.sourceStatus.active": "Active", "newKnowledge.sourceStatus.disabled": "Disabled", "newKnowledge.sourceStatus.error": "Error", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Stop", "newKnowledge.stoppingCrawl": "Stopping…", "newKnowledge.syncNow": "Sync now", + "newKnowledge.syncPolicy": "Sync policy", "newKnowledge.syncPolicyColumn": "Sync policy", + "newKnowledge.syncPolicyCustom": "Custom interval", + "newKnowledge.syncPolicyDaily": "Every 24 hours", + "newKnowledge.syncPolicyLoadFailed": "We couldn't load the sync policy.", + "newKnowledge.syncPolicyManual": "Manual only", + "newKnowledge.syncPolicyProvider": "Use provider schedule", "newKnowledge.tags": "Tags", "newKnowledge.unavailableDescription": "KnowledgeFS isn't available for this workspace right now.", "newKnowledge.unavailableTitle": "New Knowledge is unavailable", "newKnowledge.updated": "Updated {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Characters: —", "newKnowledge.uploadFiles": "Upload files", "newKnowledge.uploadFilesDescription": "Add files from your computer — they're indexed as soon as you create.", + "newKnowledge.uploadingFiles": "Uploading…", "newKnowledge.usingDefaults": "Using defaults", "newKnowledge.viewLabel": "Knowledge view", "newKnowledge.websiteCrawl": "Website crawl", diff --git a/web/i18n/es-ES/dataset.json b/web/i18n/es-ES/dataset.json index 07b5bc04a68..8ef08a6ad6d 100644 --- a/web/i18n/es-ES/dataset.json +++ b/web/i18n/es-ES/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Acciones", "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.allSources": "Todas las fuentes", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Cancelar", "newKnowledge.cardType": "Espacio de conocimiento", "newKnowledge.configureProvider": "Configurar {{provider}}", + "newKnowledge.connectNotion": "Conectar Notion", "newKnowledge.connectProvider": "Conectar {{provider}}", + "newKnowledge.connectProviderGeneric": "Conectar proveedor", "newKnowledge.connectSource": "Conectar una fuente", "newKnowledge.connectSourceDescription": "El contenido se mantiene sincronizado con Notion, sitios web y otros servicios.", "newKnowledge.connectingProvider": "Conectando…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "El rastreador no pudo completar esta vista previa. Comprueba la URL, ajusta las opciones de rastreo o vuelve a intentarlo.", "newKnowledge.crawlFailedProvider": "{{provider}} informó de un error. Comprueba la conexión del proveedor y vuelve a intentarlo.", "newKnowledge.crawlFailedTimeout": "El rastreo agotó el tiempo de espera. Prueba una URL más específica o reduce el número máximo de páginas.", + "newKnowledge.crawlNotStarted": "Sin iniciar", "newKnowledge.crawlOptions": "Opciones de rastreo", + "newKnowledge.crawlPreview": "Vista previa del rastreo", "newKnowledge.crawlProgress": "Progreso del rastreo de {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "La configuración de rastreo del sitio web está deshabilitada hasta que el flujo de trabajo de rastreo esté disponible.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl está listo", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Rastreando…", "newKnowledge.crawlingPages": "Rastreando {{host}}… {{count}} páginas encontradas", "newKnowledge.createFailed": "No pudimos crear esta base de conocimientos. Comprueba tu acceso y vuelve a intentarlo.", + "newKnowledge.createIllustrationTitle": "Conecta tu contenido una vez: cada aplicación responderá con el conocimiento más reciente.", "newKnowledge.createTitle": "Crear conocimiento", "newKnowledge.creators": "Creadores", + "newKnowledge.customIntervalHours": "Intervalo personalizado (horas)", + "newKnowledge.customIntervalInvalid": "Introduce un número entero de 1 a 720 horas.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Descartar borrador", "newKnowledge.discardDraftDescription": "Se perderán los cambios realizados en este borrador de base de conocimientos.", "newKnowledge.discardDraftTitle": "¿Descartar este borrador de base de conocimientos?", + "newKnowledge.discardSourceChanges": "¿Descartar los cambios de la fuente?", + "newKnowledge.discardSourceChangesConfirm": "Descartar cambios", + "newKnowledge.discardSourceChangesDescription": "Se perderán la vista previa del rastreo, la selección de páginas y la configuración de sincronización.", "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.documents": "Documentos", "newKnowledge.documentsUnavailable": "— documentos", + "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", "newKnowledge.errorDescription": "No pudimos cargar tus nuevas bases de conocimiento. Inténtalo de nuevo.", "newKnowledge.errorTitle": "Algo salió mal", "newKnowledge.filtersUnavailable": "Este filtro estará disponible cuando KnowledgeFS exponga sus metadatos.", + "newKnowledge.firecrawlUnavailable": "Firecrawl no está disponible en el catálogo de proveedores para este espacio de trabajo.", "newKnowledge.gotIt": "Entendido", "newKnowledge.guideDescription": "Conecta una fuente y sus documentos se mantendrán sincronizados, indexados automáticamente y listos para usar, sin nada que administrar. Tus bases de conocimiento anteriores seguirán funcionando igual.", "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.invalidRootUrl": "Introduce una URL http(s) válida.", + "newKnowledge.keepEditing": "Seguir editando", "newKnowledge.lastSyncColumn": "Última sincronización", "newKnowledge.learnMore": "Más información", "newKnowledge.leavePartialSetupConfirm": "Salir de la configuración", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "¿Salir de la configuración incompleta?", "newKnowledge.legacy": "Anterior", "newKnowledge.loadMore": "Cargar más", + "newKnowledge.loadingSyncPolicy": "Cargando la política de sincronización", "newKnowledge.maxPages": "Máximo de páginas", - "newKnowledge.moreProviders": "Ver más", + "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.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "El rastreador accedió al sitio, pero no encontró páginas indexables. Prueba otra URL o amplía las opciones de rastreo.", "newKnowledge.notFoundDescription": "Es posible que esta base de conocimientos haya sido eliminada o que no tengas acceso.", "newKnowledge.notFoundTitle": "Base de conocimientos no encontrada", + "newKnowledge.notionNotConnected": "Notion no está conectado", + "newKnowledge.notionNotConnectedDescription": "Conecta Notion para elegir páginas y mantenerlas sincronizadas.", "newKnowledge.onlineDocuments": "Documentos en línea", "newKnowledge.onlineDrive": "Unidad en línea", "newKnowledge.pagesAppearDescription": "Rastrea el sitio para revisar qué páginas se añadirán.", "newKnowledge.pagesAppearTitle": "Las páginas aparecerán aquí", "newKnowledge.pagesCrawled": "{{count}} páginas rastreadas en {{host}}", + "newKnowledge.pagesCrawled_one": "{{count}} páginas rastreadas en {{host}}", + "newKnowledge.pagesCrawled_other": "{{count}} páginas rastreadas en {{host}}", + "newKnowledge.pagesSelected": "{{count}} seleccionados", "newKnowledge.permission": "Permiso", "newKnowledge.permissionAllMembers": "Espacio de trabajo · todos los miembros pueden ver y editar", "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.providerConnected": "{{provider}} conectado", "newKnowledge.providerLabel": "Seleccione un proveedor", "newKnowledge.providerLoadFailed": "Los proveedores no pudieron cargar", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Eliminar fuente", "newKnowledge.retryCrawl": "Reintentar rastreo", "newKnowledge.retryProviderLoad": "Reintentar", + "newKnowledge.retrySyncPolicy": "Reintentar la política de sincronización", "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.searchSources": "Buscar fuentes", + "newKnowledge.selectAll": "Seleccionar todo", + "newKnowledge.selectAllSources": "Seleccionar todos los orígenes", + "newKnowledge.selectFilesAndFolders": "Selecciona archivos y carpetas para sincronizar", + "newKnowledge.skippedFailed": "Omitido (fallido)", + "newKnowledge.skippedOffDomain": "Omitido (fuera del dominio)", "newKnowledge.sourceActions": "Acciones para {{name}}", "newKnowledge.sourceColumn": "Fuente", "newKnowledge.sourceFilterLabel": "Filtrar fuentes por estado", "newKnowledge.sourceName": "Nombre de la fuente", "newKnowledge.sourceNamePlaceholder": "p. ej., rastreo de documentación del producto", "newKnowledge.sourceNameRequired": "Introduce un nombre para la fuente.", + "newKnowledge.sourceSetupBackendDependency": "La configuración del origen se muestra completa; la edición y la vista previa requieren el flujo de backend del origen en borrador.", "newKnowledge.sourceStatus.active": "Activo", "newKnowledge.sourceStatus.disabled": "Deshabilitado", "newKnowledge.sourceStatus.error": "Error", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Detener", "newKnowledge.stoppingCrawl": "Deteniendo…", "newKnowledge.syncNow": "Sincronizar ahora", + "newKnowledge.syncPolicy": "Política de sincronización", "newKnowledge.syncPolicyColumn": "Política de sincronización", + "newKnowledge.syncPolicyCustom": "Intervalo personalizado", + "newKnowledge.syncPolicyDaily": "Cada 24 horas", + "newKnowledge.syncPolicyLoadFailed": "No se pudo cargar la política de sincronización.", + "newKnowledge.syncPolicyManual": "Solo manual", + "newKnowledge.syncPolicyProvider": "Usar la programación del proveedor", "newKnowledge.tags": "Etiquetas", "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.uploadCharactersUnavailable": "Caracteres: —", "newKnowledge.uploadFiles": "Subir archivos", "newKnowledge.uploadFilesDescription": "Añade archivos desde tu ordenador; se indexarán en cuanto los crees.", + "newKnowledge.uploadingFiles": "Subiendo…", "newKnowledge.usingDefaults": "Usando valores predeterminados", "newKnowledge.viewLabel": "Vista de conocimiento", "newKnowledge.websiteCrawl": "Rastreo del sitio web", diff --git a/web/i18n/fa-IR/dataset.json b/web/i18n/fa-IR/dataset.json index 0658e3d1a7c..05412b7c00d 100644 --- a/web/i18n/fa-IR/dataset.json +++ b/web/i18n/fa-IR/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "اقدامات", "newKnowledge.addSource": "اضافه کردن منبع", "newKnowledge.addSourceDescription": "منبعی را به این پایگاه دانش متصل کنید - به طور خودکار همگام‌سازی می‌شود و تازه می‌ماند.", + "newKnowledge.addSourceFailed": "افزودن این منبع ممکن نشد. تنظیمات را بررسی کنید و دوباره تلاش کنید.", "newKnowledge.addSourceRequiresSelection": "پیش از افزودن منبع، حداقل یک صفحهٔ پیمایش‌شده را انتخاب کنید.", "newKnowledge.adjustAndRecrawl": "تنظیم و پیمایش دوباره", "newKnowledge.allSources": "همه منابع", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "لغو", "newKnowledge.cardType": "فضای دانش", "newKnowledge.configureProvider": "{{provider}} را پیکربندی کنید", + "newKnowledge.connectNotion": "اتصال Notion", "newKnowledge.connectProvider": "{{provider}} را وصل کنید", + "newKnowledge.connectProviderGeneric": "اتصال ارائه‌دهنده", "newKnowledge.connectSource": "اتصال منبع", "newKnowledge.connectSourceDescription": "محتوا با Notion، وب‌سایت‌ها و سرویس‌های دیگر همگام می‌ماند.", "newKnowledge.connectingProvider": "در حال اتصال…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "خزنده نتوانست این پیش‌نمایش را کامل کند. نشانی را بررسی کنید، گزینه‌های پیمایش را تغییر دهید یا دوباره تلاش کنید.", "newKnowledge.crawlFailedProvider": "{{provider}} خطایی گزارش کرد. اتصال ارائه‌دهنده را بررسی کنید و دوباره تلاش کنید.", "newKnowledge.crawlFailedTimeout": "زمان پیمایش به پایان رسید. نشانی دقیق‌تری را امتحان کنید یا حداکثر تعداد صفحه‌ها را کاهش دهید.", + "newKnowledge.crawlNotStarted": "شروع نشده", "newKnowledge.crawlOptions": "گزینه‌های پیمایش", + "newKnowledge.crawlPreview": "پیش‌نمایش خزش", "newKnowledge.crawlProgress": "پیشرفت پیمایش {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "پیکربندی خزیدن وب‌سایت تا زمانی که گردش کار خزیدن در دسترس نباشد، غیرفعال است.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl آماده است", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "در حال پیمایش…", "newKnowledge.crawlingPages": "در حال پیمایش {{host}}… {{count}} صفحه پیدا شد", "newKnowledge.createFailed": "ما نتوانستیم این پایگاه دانش را ایجاد کنیم. دسترسی خود را بررسی کنید و دوباره امتحان کنید.", + "newKnowledge.createIllustrationTitle": "محتوای خود را یک‌بار متصل کنید — هر برنامه با تازه‌ترین دانش پاسخ می‌دهد.", "newKnowledge.createTitle": "ایجاد دانش", "newKnowledge.creators": "ایجادکنندگان", + "newKnowledge.customIntervalHours": "بازه سفارشی (ساعت)", + "newKnowledge.customIntervalInvalid": "یک عدد صحیح بین ۱ تا ۷۲۰ ساعت وارد کنید.", "newKnowledge.description": "توضیحات", "newKnowledge.descriptionHelp": "توضیح واضح به برنامه‌ها کمک می‌کند تصمیم بگیرند که چه زمانی این دانش را جستجو کنند.", "newKnowledge.descriptionPlaceholder": "به عنوان مثال خط مشی بازپرداخت، سؤالات متداول حمل و نقل، و مراحل افزایش - برای سؤالات پشتیبانی مشتری استفاده کنید", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "حذف پیش‌نویس", "newKnowledge.discardDraftDescription": "تغییرات شما در این پیش‌نویس پایگاه دانش از بین خواهد رفت.", "newKnowledge.discardDraftTitle": "این پیش‌نویس پایگاه دانش حذف شود؟", + "newKnowledge.discardSourceChanges": "تغییرات منبع کنار گذاشته شود؟", + "newKnowledge.discardSourceChangesConfirm": "کنار گذاشتن تغییرات", + "newKnowledge.discardSourceChangesDescription": "پیش‌نمایش خزش، انتخاب صفحه‌ها و تنظیمات همگام‌سازی از دست می‌روند.", "newKnowledge.discardSourceDraftDescription": "پیکربندی منبع و پیش‌نمایش پیمایش حذف خواهند شد.", "newKnowledge.discardSourceDraftFailed": "حذف پیش‌نویس منبع ممکن نشد. دوباره تلاش کنید.", "newKnowledge.discardSourceDraftTitle": "این پیش‌نویس منبع حذف شود؟", "newKnowledge.documents": "اسناد", "newKnowledge.documentsUnavailable": "— سند", + "newKnowledge.editSource": "ویرایش", "newKnowledge.emptyDescription": "اسناد خود را اضافه کنید تا عامل‌ها بتوانند مستقیماً آن‌ها را بخوانند — بدون نیاز به تنظیم نمایه‌سازی.", "newKnowledge.emptyTitle": "روشی هوشمندتر برای ساخت پایگاه‌های دانش", "newKnowledge.errorDescription": "پایگاه‌های دانش جدید بارگذاری نشدند. دوباره تلاش کنید.", "newKnowledge.errorTitle": "مشکلی پیش آمد", "newKnowledge.filtersUnavailable": "این فیلتر پس از ارائه فراداده توسط KnowledgeFS در دسترس خواهد بود.", + "newKnowledge.firecrawlUnavailable": "Firecrawl در کاتالوگ ارائه دهنده برای این فضای کاری موجود نیست.", "newKnowledge.gotIt": "متوجه شدم", "newKnowledge.guideDescription": "یک منبع را متصل کنید تا اسناد آن همگام، خودکار نمایه‌سازی و آماده استفاده بمانند؛ بدون تنظیماتی برای مدیریت. پایگاه‌های دانش قبلی شما همچنان مانند گذشته کار می‌کنند.", "newKnowledge.guideTitle": "با پایگاه دانش جدید آشنا شوید ✨", "newKnowledge.illustrationHeadline": "محتوای خود را یک‌بار متصل کنید تا هر برنامه با تازه‌ترین دانش پاسخ دهد.", "newKnowledge.includeSubpages": "شامل صفحه‌های فرعی", "newKnowledge.invalidRootUrl": "یک نشانی معتبر http(s) وارد کنید.", + "newKnowledge.keepEditing": "ادامه ویرایش", "newKnowledge.lastSyncColumn": "آخرین همگام سازی", "newKnowledge.learnMore": "بیشتر بدانید", "newKnowledge.leavePartialSetupConfirm": "خروج از راه‌اندازی", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "از راه‌اندازی ناتمام خارج می‌شوید؟", "newKnowledge.legacy": "قدیمی", "newKnowledge.loadMore": "بارگذاری بیشتر", + "newKnowledge.loadingSyncPolicy": "در حال بارگیری سیاست همگام‌سازی", "newKnowledge.maxPages": "حداکثر صفحه‌ها", - "newKnowledge.moreProviders": "بیشتر ببینید", + "newKnowledge.moreProviders": "ارائه‌دهندگان بیشتر", "newKnowledge.name": "نام دانش", "newKnowledge.namePlaceholder": "به عنوان مثال راهنمای پشتیبانی مشتری", "newKnowledge.nameRequired": "یک نام پایگاه دانش وارد کنید.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "خزنده به سایت رسید اما صفحهٔ قابل نمایه‌سازی پیدا نکرد. نشانی دیگری را امتحان کنید یا گزینه‌های پیمایش را گسترش دهید.", "newKnowledge.notFoundDescription": "این پایگاه دانش ممکن است حذف شده باشد یا ممکن است دسترسی نداشته باشید.", "newKnowledge.notFoundTitle": "پایگاه دانش یافت نشد", + "newKnowledge.notionNotConnected": "Notion متصل نیست", + "newKnowledge.notionNotConnectedDescription": "برای انتخاب صفحه‌ها و همگام نگه‌داشتن آن‌ها، Notion را متصل کنید.", "newKnowledge.onlineDocuments": "اسناد آنلاین", "newKnowledge.onlineDrive": "درایو آنلاین", "newKnowledge.pagesAppearDescription": "سایت را پیمایش کنید تا صفحه‌هایی را که افزوده می‌شوند بررسی کنید.", "newKnowledge.pagesAppearTitle": "صفحه‌ها اینجا نمایش داده می‌شوند", "newKnowledge.pagesCrawled": "{{count}} صفحه در {{host}} پیمایش شد", + "newKnowledge.pagesCrawled_one": "{{count}} صفحه در {{host}} پیمایش شد", + "newKnowledge.pagesCrawled_other": "{{count}} صفحه در {{host}} پیمایش شد", + "newKnowledge.pagesSelected": "{{count}} مورد انتخاب شد", "newKnowledge.permission": "اجازه", "newKnowledge.permissionAllMembers": "فضای کاری · همه اعضا می توانند مشاهده و ویرایش کنند", "newKnowledge.permissionOnlyMe": "فقط من", "newKnowledge.permissionRestricted": "مجوزهای فضای کاری شما اجازه تغییر این تنظیم را نمی دهد.", "newKnowledge.permissionUpdateFailed": "پایگاه دانش ایجاد شد، اما مجوز آن تأیید نشد. برای تکمیل راه‌اندازی دوباره تلاش کنید.", + "newKnowledge.preview": "پیش‌نمایش", + "newKnowledge.previewUnavailable": "پس از پایان پردازش این فایل در بخش پشتیبان، پیش‌نمایش در دسترس می‌شود.", "newKnowledge.providerConnected": "{{provider}} متصل شد", "newKnowledge.providerLabel": "یک ارائه دهنده را انتخاب کنید", "newKnowledge.providerLoadFailed": "ارائه‌دهندگان نتوانستند بارگیری کنند", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "حذف منبع", "newKnowledge.retryCrawl": "تلاش دوباره برای پیمایش", "newKnowledge.retryProviderLoad": "دوباره امتحان کنید", + "newKnowledge.retrySyncPolicy": "تلاش دوباره برای سیاست همگام‌سازی", "newKnowledge.rootUrl": "نشانی ریشه", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "این بخش در مرحله راه اندازی بعدی در دسترس خواهد بود.", "newKnowledge.searchSources": "منابع جستجو", + "newKnowledge.selectAll": "انتخاب همه", + "newKnowledge.selectAllSources": "انتخاب همه منابع", + "newKnowledge.selectFilesAndFolders": "فایل‌ها و پوشه‌ها را برای همگام‌سازی انتخاب کنید", + "newKnowledge.skippedFailed": "رد شد (ناموفق)", + "newKnowledge.skippedOffDomain": "رد شد (خارج از دامنه)", "newKnowledge.sourceActions": "اقدامات برای {{name}}", "newKnowledge.sourceColumn": "منبع", "newKnowledge.sourceFilterLabel": "منابع را بر اساس وضعیت فیلتر کنید", "newKnowledge.sourceName": "نام منبع", "newKnowledge.sourceNamePlaceholder": "مثلاً پیمایش مستندات محصول", "newKnowledge.sourceNameRequired": "نام منبع را وارد کنید.", + "newKnowledge.sourceSetupBackendDependency": "تنظیم منبع به‌طور کامل نمایش داده شده است؛ ویرایش و پیش‌نمایش به گردش‌کار پشتیبان منبع پیش‌نویس نیاز دارد.", "newKnowledge.sourceStatus.active": "فعال", "newKnowledge.sourceStatus.disabled": "غیرفعال شد", "newKnowledge.sourceStatus.error": "خطا", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "توقف", "newKnowledge.stoppingCrawl": "در حال توقف…", "newKnowledge.syncNow": "اکنون همگام سازی کنید", + "newKnowledge.syncPolicy": "سیاست همگام‌سازی", "newKnowledge.syncPolicyColumn": "خط مشی همگام سازی", + "newKnowledge.syncPolicyCustom": "بازه سفارشی", + "newKnowledge.syncPolicyDaily": "هر ۲۴ ساعت", + "newKnowledge.syncPolicyLoadFailed": "بارگیری سیاست همگام‌سازی ممکن نشد.", + "newKnowledge.syncPolicyManual": "فقط دستی", + "newKnowledge.syncPolicyProvider": "استفاده از برنامه ارائه‌دهنده", "newKnowledge.tags": "برچسب‌ها", "newKnowledge.unavailableDescription": "KnowledgeFS اکنون برای این فضای کاری در دسترس نیست.", "newKnowledge.unavailableTitle": "دانش جدید در دسترس نیست", "newKnowledge.updated": "به‌روزرسانی در {{date}}", + "newKnowledge.uploadCharactersUnavailable": "نویسه‌ها: —", "newKnowledge.uploadFiles": "بارگذاری فایل‌ها", "newKnowledge.uploadFilesDescription": "فایل‌ها را از رایانه اضافه کنید — به‌محض ایجاد نمایه‌سازی می‌شوند.", + "newKnowledge.uploadingFiles": "در حال بارگذاری…", "newKnowledge.usingDefaults": "استفاده از تنظیمات پیش‌فرض", "newKnowledge.viewLabel": "نمای دانش", "newKnowledge.websiteCrawl": "خزیدن وب سایت", diff --git a/web/i18n/fr-FR/dataset.json b/web/i18n/fr-FR/dataset.json index bf82e5da864..f4ba89e25d6 100644 --- a/web/i18n/fr-FR/dataset.json +++ b/web/i18n/fr-FR/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Actions", "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.allSources": "Toutes les sources", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Annuler", "newKnowledge.cardType": "Espace de connaissances", "newKnowledge.configureProvider": "Configurer {{provider}}", + "newKnowledge.connectNotion": "Connecter Notion", "newKnowledge.connectProvider": "Connecter {{provider}}", + "newKnowledge.connectProviderGeneric": "Connecter un fournisseur", "newKnowledge.connectSource": "Connecter une source", "newKnowledge.connectSourceDescription": "Le contenu reste synchronisé avec Notion, les sites web et d’autres services.", "newKnowledge.connectingProvider": "Connexion…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Le robot n’a pas pu terminer cet aperçu. Vérifiez l’URL, ajustez les options d’exploration ou réessayez.", "newKnowledge.crawlFailedProvider": "{{provider}} a signalé une erreur. Vérifiez la connexion au fournisseur et réessayez.", "newKnowledge.crawlFailedTimeout": "Le délai d’exploration a expiré. Essayez une URL plus précise ou réduisez le nombre maximal de pages.", + "newKnowledge.crawlNotStarted": "Non démarré", "newKnowledge.crawlOptions": "Options d’exploration", + "newKnowledge.crawlPreview": "Aperçu de l’exploration", "newKnowledge.crawlProgress": "Progression de l’exploration de {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "La configuration de l'analyse du site Web est désactivée jusqu'à ce que le flux de travail d'analyse soit disponible.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl est prêt", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Exploration en cours…", "newKnowledge.crawlingPages": "Exploration de {{host}}… {{count}} pages trouvées", "newKnowledge.createFailed": "Nous n'avons pas pu créer cette base de connaissances. Vérifiez votre accès et réessayez.", + "newKnowledge.createIllustrationTitle": "Connectez votre contenu une fois — chaque application répond avec les connaissances les plus récentes.", "newKnowledge.createTitle": "Créer des connaissances", "newKnowledge.creators": "Créateurs", + "newKnowledge.customIntervalHours": "Intervalle personnalisé (heures)", + "newKnowledge.customIntervalInvalid": "Saisissez un nombre entier compris entre 1 et 720 heures.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Abandonner le brouillon", "newKnowledge.discardDraftDescription": "Les modifications apportées à ce brouillon de base de connaissances seront perdues.", "newKnowledge.discardDraftTitle": "Abandonner ce brouillon de base de connaissances ?", + "newKnowledge.discardSourceChanges": "Ignorer les modifications de la source ?", + "newKnowledge.discardSourceChangesConfirm": "Ignorer les modifications", + "newKnowledge.discardSourceChangesDescription": "L’aperçu de l’exploration, la sélection des pages et les paramètres de synchronisation seront perdus.", "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.documents": "Documents", "newKnowledge.documentsUnavailable": "— documents", + "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", "newKnowledge.errorDescription": "Impossible de charger vos nouvelles bases de connaissances. Réessayez.", "newKnowledge.errorTitle": "Une erreur s’est produite", "newKnowledge.filtersUnavailable": "Ce filtre sera disponible lorsque KnowledgeFS exposera ses métadonnées.", + "newKnowledge.firecrawlUnavailable": "Firecrawl n'est pas disponible dans le catalogue des fournisseurs pour cet espace de travail.", "newKnowledge.gotIt": "Compris", "newKnowledge.guideDescription": "Connectez une source et ses documents restent synchronisés, indexés automatiquement et prêts à l’emploi, sans configuration à gérer. Vos anciennes bases de connaissances continuent de fonctionner telles quelles.", "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.invalidRootUrl": "Saisissez une URL http(s) valide.", + "newKnowledge.keepEditing": "Continuer la modification", "newKnowledge.lastSyncColumn": "Dernière synchronisation", "newKnowledge.learnMore": "En savoir plus", "newKnowledge.leavePartialSetupConfirm": "Quitter la configuration", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Quitter la configuration incomplète ?", "newKnowledge.legacy": "Ancien", "newKnowledge.loadMore": "Charger plus", + "newKnowledge.loadingSyncPolicy": "Chargement de la politique de synchronisation", "newKnowledge.maxPages": "Nombre maximal de pages", - "newKnowledge.moreProviders": "Voir plus", + "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.", @@ -217,16 +232,23 @@ "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.", "newKnowledge.notFoundDescription": "Cette base de connaissances a peut-être été supprimée ou vous n'y avez peut-être pas accès.", "newKnowledge.notFoundTitle": "Base de connaissances introuvable", + "newKnowledge.notionNotConnected": "Notion n’est pas connecté", + "newKnowledge.notionNotConnectedDescription": "Connectez Notion pour choisir des pages et les maintenir synchronisées.", "newKnowledge.onlineDocuments": "Documents en ligne", "newKnowledge.onlineDrive": "Drive en ligne", "newKnowledge.pagesAppearDescription": "Explorez le site pour vérifier les pages qui seront ajoutées.", "newKnowledge.pagesAppearTitle": "Les pages apparaîtront ici", "newKnowledge.pagesCrawled": "{{count}} pages explorées sur {{host}}", + "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.permission": "Autorisation", "newKnowledge.permissionAllMembers": "Espace de travail · tous les membres peuvent consulter et modifier", "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.providerConnected": "{{provider}} connecté", "newKnowledge.providerLabel": "Sélectionnez un fournisseur", "newKnowledge.providerLoadFailed": "Les fournisseurs n'ont pas pu charger", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Supprimer la source", "newKnowledge.retryCrawl": "Réessayer l’exploration", "newKnowledge.retryProviderLoad": "Réessayer", + "newKnowledge.retrySyncPolicy": "Réessayer la politique de synchronisation", "newKnowledge.rootUrl": "URL racine", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Cette section deviendra disponible lors de la prochaine étape de configuration.", "newKnowledge.searchSources": "Sources de recherche", + "newKnowledge.selectAll": "Tout sélectionner", + "newKnowledge.selectAllSources": "Sélectionner toutes les sources", + "newKnowledge.selectFilesAndFolders": "Sélectionnez les fichiers et dossiers à synchroniser", + "newKnowledge.skippedFailed": "Ignoré (échec)", + "newKnowledge.skippedOffDomain": "Ignoré (hors domaine)", "newKnowledge.sourceActions": "Actions pour {{name}}", "newKnowledge.sourceColumn": "Origine", "newKnowledge.sourceFilterLabel": "Filtrer les sources par statut", "newKnowledge.sourceName": "Nom de la source", "newKnowledge.sourceNamePlaceholder": "p. ex. exploration de la documentation produit", "newKnowledge.sourceNameRequired": "Saisissez un nom de source.", + "newKnowledge.sourceSetupBackendDependency": "La configuration de la source est affichée en entier ; la modification et l’aperçu nécessitent le flux backend de source brouillon.", "newKnowledge.sourceStatus.active": "Actif", "newKnowledge.sourceStatus.disabled": "Désactivé", "newKnowledge.sourceStatus.error": "Erreur", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Arrêter", "newKnowledge.stoppingCrawl": "Arrêt en cours…", "newKnowledge.syncNow": "Synchroniser maintenant", + "newKnowledge.syncPolicy": "Politique de synchronisation", "newKnowledge.syncPolicyColumn": "Politique de synchronisation", + "newKnowledge.syncPolicyCustom": "Intervalle personnalisé", + "newKnowledge.syncPolicyDaily": "Toutes les 24 heures", + "newKnowledge.syncPolicyLoadFailed": "Impossible de charger la politique de synchronisation.", + "newKnowledge.syncPolicyManual": "Manuel uniquement", + "newKnowledge.syncPolicyProvider": "Utiliser la planification du fournisseur", "newKnowledge.tags": "Étiquettes", "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.uploadCharactersUnavailable": "Caractères : —", "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…", "newKnowledge.usingDefaults": "Valeurs par défaut utilisées", "newKnowledge.viewLabel": "Vue des connaissances", "newKnowledge.websiteCrawl": "Exploration du site Web", diff --git a/web/i18n/hi-IN/dataset.json b/web/i18n/hi-IN/dataset.json index cc302120137..d33d9984d3f 100644 --- a/web/i18n/hi-IN/dataset.json +++ b/web/i18n/hi-IN/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "क्रियाएँ", "newKnowledge.addSource": "स्रोत जोड़ें", "newKnowledge.addSourceDescription": "इस ज्ञानकोष से एक स्रोत कनेक्ट करें - यह स्वचालित रूप से समन्वयित होता है और ताज़ा रहता है।", + "newKnowledge.addSourceFailed": "यह स्रोत जोड़ा नहीं जा सका। सेटिंग की समीक्षा करें और फिर से कोशिश करें।", "newKnowledge.addSourceRequiresSelection": "स्रोत जोड़ने से पहले कम से कम एक क्रॉल किया गया पेज चुनें।", "newKnowledge.adjustAndRecrawl": "समायोजित करें और फिर से क्रॉल करें", "newKnowledge.allSources": "सभी स्रोत", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "रद्द करें", "newKnowledge.cardType": "नॉलेज स्पेस", "newKnowledge.configureProvider": "{{provider}} कॉन्फ़िगर करें", + "newKnowledge.connectNotion": "Notion कनेक्ट करें", "newKnowledge.connectProvider": "{{provider}} कनेक्ट करें", + "newKnowledge.connectProviderGeneric": "प्रदाता कनेक्ट करें", "newKnowledge.connectSource": "स्रोत कनेक्ट करें", "newKnowledge.connectSourceDescription": "कंटेंट Notion, वेबसाइटों और अन्य सेवाओं के साथ सिंक रहता है।", "newKnowledge.connectingProvider": "कनेक्ट हो रहा है...", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "क्रॉलर यह पूर्वावलोकन पूरा नहीं कर सका। URL जाँचें, क्रॉल विकल्प बदलें या फिर से प्रयास करें।", "newKnowledge.crawlFailedProvider": "{{provider}} ने त्रुटि की सूचना दी। प्रदाता कनेक्शन जाँचें और फिर से प्रयास करें।", "newKnowledge.crawlFailedTimeout": "क्रॉल का समय समाप्त हो गया। अधिक विशिष्ट URL आज़माएँ या पेज की अधिकतम संख्या घटाएँ।", + "newKnowledge.crawlNotStarted": "शुरू नहीं हुआ", "newKnowledge.crawlOptions": "क्रॉल विकल्प", + "newKnowledge.crawlPreview": "क्रॉल पूर्वावलोकन", "newKnowledge.crawlProgress": "{{host}} के लिए क्रॉल प्रगति", "newKnowledge.crawlSetupUnavailableDescription": "क्रॉल वर्कफ़्लो उपलब्ध होने तक वेबसाइट क्रॉल कॉन्फ़िगरेशन अक्षम है।", "newKnowledge.crawlSetupUnavailableTitle": "फायरक्रॉल तैयार है", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "क्रॉल किया जा रहा है…", "newKnowledge.crawlingPages": "{{host}} को क्रॉल किया जा रहा है… {{count}} पेज मिले", "newKnowledge.createFailed": "हम यह ज्ञानकोष नहीं बना सके। अपनी पहुंच जांचें और पुनः प्रयास करें।", + "newKnowledge.createIllustrationTitle": "अपनी सामग्री को एक बार जोड़ें — हर ऐप नवीनतम ज्ञान के साथ उत्तर देगा।", "newKnowledge.createTitle": "ज्ञान पैदा करें", "newKnowledge.creators": "निर्माता", + "newKnowledge.customIntervalHours": "कस्टम अंतराल (घंटे)", + "newKnowledge.customIntervalInvalid": "1 से 720 घंटे तक की पूर्ण संख्या दर्ज करें।", "newKnowledge.description": "विवरण", "newKnowledge.descriptionHelp": "एक स्पष्ट विवरण ऐप्स को यह निर्णय लेने में मदद करता है कि इस ज्ञान को कब खोजना है।", "newKnowledge.descriptionPlaceholder": "जैसे रिफंड नीति, शिपिंग एफएक्यू और एस्केलेशन चरण - ग्राहक सहायता प्रश्नों के लिए उपयोग करें", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "ड्राफ़्ट हटाएँ", "newKnowledge.discardDraftDescription": "इस नॉलेज बेस ड्राफ़्ट में किए गए आपके बदलाव खो जाएँगे।", "newKnowledge.discardDraftTitle": "क्या यह नॉलेज बेस ड्राफ़्ट हटाना है?", + "newKnowledge.discardSourceChanges": "स्रोत में किए बदलाव छोड़ें?", + "newKnowledge.discardSourceChangesConfirm": "बदलाव छोड़ें", + "newKnowledge.discardSourceChangesDescription": "क्रॉल पूर्वावलोकन, पेज चयन और सिंक सेटिंग खो जाएँगी।", "newKnowledge.discardSourceDraftDescription": "स्रोत कॉन्फ़िगरेशन और क्रॉल पूर्वावलोकन हटा दिए जाएँगे।", "newKnowledge.discardSourceDraftFailed": "स्रोत ड्राफ़्ट हटाया नहीं जा सका। फिर से प्रयास करें।", "newKnowledge.discardSourceDraftTitle": "यह स्रोत ड्राफ़्ट हटाएँ?", "newKnowledge.documents": "दस्तावेज़", "newKnowledge.documentsUnavailable": "— दस्तावेज़", + "newKnowledge.editSource": "संपादित करें", "newKnowledge.emptyDescription": "अपने दस्तावेज़ जोड़ें ताकि एजेंट उन्हें सीधे पढ़ सकें — इंडेक्सिंग कॉन्फ़िगर करने की ज़रूरत नहीं।", "newKnowledge.emptyTitle": "नॉलेज बेस बनाने का अधिक स्मार्ट तरीका", "newKnowledge.errorDescription": "नए नॉलेज बेस लोड नहीं हो सके। फिर से कोशिश करें।", "newKnowledge.errorTitle": "कुछ गलत हुआ", "newKnowledge.filtersUnavailable": "KnowledgeFS के मेटाडेटा उपलब्ध कराने पर यह फ़िल्टर उपलब्ध होगा।", + "newKnowledge.firecrawlUnavailable": "इस कार्यक्षेत्र के लिए प्रदाता कैटलॉग में फायरक्रॉल उपलब्ध नहीं है।", "newKnowledge.gotIt": "समझ गया", "newKnowledge.guideDescription": "किसी स्रोत को कनेक्ट करें और उसके दस्तावेज़ सिंक, अपने-आप इंडेक्स और उपयोग के लिए तैयार रहेंगे—किसी सेटअप को संभालने की ज़रूरत नहीं। आपके पुराने नॉलेज बेस पहले की तरह काम करते रहेंगे।", "newKnowledge.guideTitle": "नए नॉलेज बेस से मिलें ✨", "newKnowledge.illustrationHeadline": "अपनी सामग्री एक बार कनेक्ट करें — हर ऐप नवीनतम ज्ञान के साथ जवाब देगा।", "newKnowledge.includeSubpages": "उप-पेज शामिल करें", "newKnowledge.invalidRootUrl": "मान्य http(s) URL दर्ज करें।", + "newKnowledge.keepEditing": "संपादन जारी रखें", "newKnowledge.lastSyncColumn": "अंतिम सिंक", "newKnowledge.learnMore": "और जानें", "newKnowledge.leavePartialSetupConfirm": "सेटअप छोड़ें", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "क्या अधूरा सेटअप छोड़ना है?", "newKnowledge.legacy": "पुराना", "newKnowledge.loadMore": "और लोड करें", + "newKnowledge.loadingSyncPolicy": "सिंक नीति लोड हो रही है", "newKnowledge.maxPages": "अधिकतम पेज", - "newKnowledge.moreProviders": "और देखें", + "newKnowledge.moreProviders": "और प्रदाता", "newKnowledge.name": "ज्ञान नाम", "newKnowledge.namePlaceholder": "जैसे ग्राहक सहायता पुस्तिका", "newKnowledge.nameRequired": "नॉलेज बेस नाम दर्ज करें.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "क्रॉलर साइट तक पहुँचा, लेकिन कोई इंडेक्स करने योग्य पेज नहीं मिला। कोई दूसरा URL आज़माएँ या क्रॉल विकल्प बढ़ाएँ।", "newKnowledge.notFoundDescription": "हो सकता है कि यह ज्ञानकोष हटा दिया गया हो या आपके पास पहुंच न हो।", "newKnowledge.notFoundTitle": "ज्ञान का आधार नहीं मिला", + "newKnowledge.notionNotConnected": "Notion कनेक्ट नहीं है", + "newKnowledge.notionNotConnectedDescription": "पृष्ठ चुनने और उन्हें सिंक रखने के लिए Notion कनेक्ट करें।", "newKnowledge.onlineDocuments": "ऑनलाइन दस्तावेज़", "newKnowledge.onlineDrive": "ऑनलाइन ड्राइव", "newKnowledge.pagesAppearDescription": "जोड़े जाने वाले पेज देखने के लिए साइट को क्रॉल करें।", "newKnowledge.pagesAppearTitle": "पेज यहाँ दिखाई देंगे", "newKnowledge.pagesCrawled": "{{host}} पर {{count}} पेज क्रॉल किए गए", + "newKnowledge.pagesCrawled_one": "{{host}} पर {{count}} पेज क्रॉल किए गए", + "newKnowledge.pagesCrawled_other": "{{host}} पर {{count}} पेज क्रॉल किए गए", + "newKnowledge.pagesSelected": "{{count}} चयनित", "newKnowledge.permission": "अनुमति", "newKnowledge.permissionAllMembers": "कार्यक्षेत्र · सभी सदस्य देख और संपादित कर सकते हैं", "newKnowledge.permissionOnlyMe": "केवल मैं", "newKnowledge.permissionRestricted": "आपकी कार्यस्थान अनुमतियाँ इस सेटिंग को बदलने की अनुमति नहीं देतीं।", "newKnowledge.permissionUpdateFailed": "नॉलेज बेस बन गया है, लेकिन उसकी अनुमति की पुष्टि नहीं हो सकी। सेटअप पूरा करने के लिए फिर से कोशिश करें।", + "newKnowledge.preview": "पूर्वावलोकन", + "newKnowledge.previewUnavailable": "बैकएंड द्वारा इस फ़ाइल की प्रोसेसिंग पूरी होने के बाद पूर्वावलोकन उपलब्ध होगा।", "newKnowledge.providerConnected": "{{provider}} जुड़ा", "newKnowledge.providerLabel": "एक प्रदाता का चयन करें", "newKnowledge.providerLoadFailed": "प्रदाता लोड नहीं कर सके", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "स्रोत हटाएं", "newKnowledge.retryCrawl": "क्रॉल का फिर से प्रयास करें", "newKnowledge.retryProviderLoad": "पुनः प्रयास करें", + "newKnowledge.retrySyncPolicy": "सिंक नीति फिर से लोड करें", "newKnowledge.rootUrl": "रूट URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "यह अनुभाग अगले सेटअप चरण में उपलब्ध हो जाएगा.", "newKnowledge.searchSources": "स्रोत खोजें", + "newKnowledge.selectAll": "सभी चुनें", + "newKnowledge.selectAllSources": "सभी स्रोत चुनें", + "newKnowledge.selectFilesAndFolders": "सिंक करने के लिए फ़ाइलें और फ़ोल्डर चुनें", + "newKnowledge.skippedFailed": "छोड़ा गया (विफल)", + "newKnowledge.skippedOffDomain": "छोड़ा गया (डोमेन से बाहर)", "newKnowledge.sourceActions": "{{name}} के लिए कार्रवाइयां", "newKnowledge.sourceColumn": "स्रोत", "newKnowledge.sourceFilterLabel": "स्थिति के अनुसार स्रोतों को फ़िल्टर करें", "newKnowledge.sourceName": "स्रोत का नाम", "newKnowledge.sourceNamePlaceholder": "जैसे, उत्पाद दस्तावेज़ क्रॉल", "newKnowledge.sourceNameRequired": "स्रोत का नाम दर्ज करें।", + "newKnowledge.sourceSetupBackendDependency": "स्रोत सेटअप पूरा दिखाया गया है; संपादन और पूर्वावलोकन के लिए ड्राफ़्ट स्रोत बैकएंड कार्यप्रवाह आवश्यक है।", "newKnowledge.sourceStatus.active": "सक्रिय", "newKnowledge.sourceStatus.disabled": "विकलांग", "newKnowledge.sourceStatus.error": "त्रुटि", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "रोकें", "newKnowledge.stoppingCrawl": "रोका जा रहा है…", "newKnowledge.syncNow": "अभी सिंक करें", + "newKnowledge.syncPolicy": "सिंक नीति", "newKnowledge.syncPolicyColumn": "सिंक नीति", + "newKnowledge.syncPolicyCustom": "कस्टम अंतराल", + "newKnowledge.syncPolicyDaily": "हर 24 घंटे", + "newKnowledge.syncPolicyLoadFailed": "सिंक नीति लोड नहीं की जा सकी।", + "newKnowledge.syncPolicyManual": "केवल मैन्युअल", + "newKnowledge.syncPolicyProvider": "प्रदाता का शेड्यूल उपयोग करें", "newKnowledge.tags": "टैग", "newKnowledge.unavailableDescription": "KnowledgeFS अभी इस कार्यस्थान के लिए उपलब्ध नहीं है।", "newKnowledge.unavailableTitle": "नया नॉलेज उपलब्ध नहीं है", "newKnowledge.updated": "{{date}} को अपडेट किया गया", + "newKnowledge.uploadCharactersUnavailable": "वर्ण: —", "newKnowledge.uploadFiles": "फ़ाइलें अपलोड करें", "newKnowledge.uploadFilesDescription": "अपने कंप्यूटर से फ़ाइलें जोड़ें — बनाते ही वे इंडेक्स हो जाएँगी।", + "newKnowledge.uploadingFiles": "अपलोड हो रहा है…", "newKnowledge.usingDefaults": "डिफ़ॉल्ट सेटिंग का उपयोग हो रहा है", "newKnowledge.viewLabel": "नॉलेज दृश्य", "newKnowledge.websiteCrawl": "वेबसाइट क्रॉल", diff --git a/web/i18n/id-ID/dataset.json b/web/i18n/id-ID/dataset.json index 1158be21f15..020d8825cf9 100644 --- a/web/i18n/id-ID/dataset.json +++ b/web/i18n/id-ID/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Tindakan", "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.allSources": "Semua sumber", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Batal", "newKnowledge.cardType": "Ruang pengetahuan", "newKnowledge.configureProvider": "Konfigurasikan {{provider}}", + "newKnowledge.connectNotion": "Hubungkan Notion", "newKnowledge.connectProvider": "Hubungkan {{provider}}", + "newKnowledge.connectProviderGeneric": "Hubungkan penyedia", "newKnowledge.connectSource": "Hubungkan sumber", "newKnowledge.connectSourceDescription": "Konten tetap tersinkronisasi dengan Notion, situs web, dan layanan lainnya.", "newKnowledge.connectingProvider": "Menghubungkan…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Perayap tidak dapat menyelesaikan pratinjau ini. Periksa URL, sesuaikan opsi perayapan, atau coba lagi.", "newKnowledge.crawlFailedProvider": "{{provider}} melaporkan kesalahan. Periksa koneksi penyedia dan coba lagi.", "newKnowledge.crawlFailedTimeout": "Waktu perayapan habis. Coba URL yang lebih spesifik atau kurangi jumlah maksimum halaman.", + "newKnowledge.crawlNotStarted": "Belum dimulai", "newKnowledge.crawlOptions": "Opsi perayapan", + "newKnowledge.crawlPreview": "Pratinjau perayapan", "newKnowledge.crawlProgress": "Progres perayapan untuk {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Konfigurasi perayapan situs web dinonaktifkan hingga alur kerja perayapan tersedia.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl sudah siap", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Merayapi…", "newKnowledge.crawlingPages": "Merayapi {{host}}… {{count}} halaman ditemukan", "newKnowledge.createFailed": "Kami tidak dapat membuat basis pengetahuan ini. Periksa akses Anda dan coba lagi.", + "newKnowledge.createIllustrationTitle": "Hubungkan konten sekali — setiap aplikasi menjawab dengan pengetahuan terbaru.", "newKnowledge.createTitle": "Ciptakan Pengetahuan", "newKnowledge.creators": "Pembuat", + "newKnowledge.customIntervalHours": "Interval khusus (jam)", + "newKnowledge.customIntervalInvalid": "Masukkan bilangan bulat antara 1 hingga 720 jam.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Buang draf", "newKnowledge.discardDraftDescription": "Perubahan pada draf basis pengetahuan ini akan hilang.", "newKnowledge.discardDraftTitle": "Buang draf basis pengetahuan ini?", + "newKnowledge.discardSourceChanges": "Buang perubahan sumber?", + "newKnowledge.discardSourceChangesConfirm": "Buang perubahan", + "newKnowledge.discardSourceChangesDescription": "Pratinjau crawl, pilihan halaman, dan pengaturan sinkronisasi akan hilang.", "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.documents": "Dokumen", "newKnowledge.documentsUnavailable": "— dokumen", + "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", "newKnowledge.errorDescription": "Kami tidak dapat memuat basis pengetahuan baru Anda. Coba lagi.", "newKnowledge.errorTitle": "Terjadi kesalahan", "newKnowledge.filtersUnavailable": "Filter ini akan tersedia saat KnowledgeFS menyediakan metadatanya.", + "newKnowledge.firecrawlUnavailable": "Firecrawl tidak tersedia di katalog penyedia untuk ruang kerja ini.", "newKnowledge.gotIt": "Mengerti", "newKnowledge.guideDescription": "Hubungkan sumber dan dokumennya akan tetap tersinkronisasi, diindeks otomatis, serta siap digunakan tanpa pengaturan yang perlu dikelola. Basis pengetahuan lama Anda tetap berfungsi seperti biasa.", "newKnowledge.guideTitle": "Kenali Basis Pengetahuan baru ✨", "newKnowledge.illustrationHeadline": "Hubungkan konten Anda sekali — setiap aplikasi menjawab dengan pengetahuan terbaru.", "newKnowledge.includeSubpages": "Sertakan subhalaman", "newKnowledge.invalidRootUrl": "Masukkan URL http(s) yang valid.", + "newKnowledge.keepEditing": "Lanjutkan mengedit", "newKnowledge.lastSyncColumn": "Sinkronisasi terakhir", "newKnowledge.learnMore": "Pelajari lebih lanjut", "newKnowledge.leavePartialSetupConfirm": "Keluar dari penyiapan", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Keluar dari penyiapan yang belum selesai?", "newKnowledge.legacy": "Lama", "newKnowledge.loadMore": "Muat lebih banyak", + "newKnowledge.loadingSyncPolicy": "Memuat kebijakan sinkronisasi", "newKnowledge.maxPages": "Halaman maksimum", - "newKnowledge.moreProviders": "Lihat lebih banyak", + "newKnowledge.moreProviders": "Penyedia lainnya", "newKnowledge.name": "Nama pengetahuan", "newKnowledge.namePlaceholder": "misalnya Buku Pegangan Dukungan Pelanggan", "newKnowledge.nameRequired": "Masukkan nama basis pengetahuan.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Perayap mencapai situs tetapi tidak menemukan halaman yang dapat diindeks. Coba URL lain atau perluas opsi perayapan.", "newKnowledge.notFoundDescription": "Basis pengetahuan ini mungkin telah dihapus atau Anda mungkin tidak memiliki akses.", "newKnowledge.notFoundTitle": "Basis pengetahuan tidak ditemukan", + "newKnowledge.notionNotConnected": "Notion belum terhubung", + "newKnowledge.notionNotConnectedDescription": "Hubungkan Notion untuk memilih halaman dan menjaganya tetap tersinkron.", "newKnowledge.onlineDocuments": "Dokumen daring", "newKnowledge.onlineDrive": "Perjalanan daring", "newKnowledge.pagesAppearDescription": "Rayapi situs untuk meninjau halaman yang akan ditambahkan.", "newKnowledge.pagesAppearTitle": "Halaman akan muncul di sini", "newKnowledge.pagesCrawled": "{{count}} halaman dirayapi di {{host}}", + "newKnowledge.pagesCrawled_one": "{{count}} halaman dirayapi di {{host}}", + "newKnowledge.pagesCrawled_other": "{{count}} halaman dirayapi di {{host}}", + "newKnowledge.pagesSelected": "{{count}} dipilih", "newKnowledge.permission": "Izin", "newKnowledge.permissionAllMembers": "Ruang Kerja · semua anggota dapat melihat dan mengedit", "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.providerConnected": "{{provider}} terhubung", "newKnowledge.providerLabel": "Pilih penyedia", "newKnowledge.providerLoadFailed": "Penyedia tidak dapat memuat", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Hapus sumber", "newKnowledge.retryCrawl": "Coba perayapan lagi", "newKnowledge.retryProviderLoad": "Coba lagi", + "newKnowledge.retrySyncPolicy": "Coba lagi kebijakan sinkronisasi", "newKnowledge.rootUrl": "URL root", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Bagian ini akan tersedia pada langkah penyiapan berikutnya.", "newKnowledge.searchSources": "Cari sumber", + "newKnowledge.selectAll": "Pilih semua", + "newKnowledge.selectAllSources": "Pilih semua sumber", + "newKnowledge.selectFilesAndFolders": "Pilih file dan folder untuk disinkronkan", + "newKnowledge.skippedFailed": "Dilewati (gagal)", + "newKnowledge.skippedOffDomain": "Dilewati (di luar domain)", "newKnowledge.sourceActions": "Tindakan untuk {{name}}", "newKnowledge.sourceColumn": "Sumber", "newKnowledge.sourceFilterLabel": "Filter sumber berdasarkan status", "newKnowledge.sourceName": "Nama sumber", "newKnowledge.sourceNamePlaceholder": "mis. perayapan dokumentasi produk", "newKnowledge.sourceNameRequired": "Masukkan nama sumber.", + "newKnowledge.sourceSetupBackendDependency": "Penyiapan sumber ditampilkan secara lengkap; tindakan edit dan pratinjau memerlukan alur backend sumber draf.", "newKnowledge.sourceStatus.active": "Aktif", "newKnowledge.sourceStatus.disabled": "Dinonaktifkan", "newKnowledge.sourceStatus.error": "Kesalahan", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Hentikan", "newKnowledge.stoppingCrawl": "Menghentikan…", "newKnowledge.syncNow": "Sinkronkan sekarang", + "newKnowledge.syncPolicy": "Kebijakan sinkronisasi", "newKnowledge.syncPolicyColumn": "Kebijakan sinkronisasi", + "newKnowledge.syncPolicyCustom": "Interval khusus", + "newKnowledge.syncPolicyDaily": "Setiap 24 jam", + "newKnowledge.syncPolicyLoadFailed": "Kebijakan sinkronisasi tidak dapat dimuat.", + "newKnowledge.syncPolicyManual": "Hanya manual", + "newKnowledge.syncPolicyProvider": "Gunakan jadwal penyedia", "newKnowledge.tags": "Tag", "newKnowledge.unavailableDescription": "KnowledgeFS saat ini tidak tersedia untuk ruang kerja ini.", "newKnowledge.unavailableTitle": "Pengetahuan Baru tidak tersedia", "newKnowledge.updated": "Diperbarui {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Karakter: —", "newKnowledge.uploadFiles": "Unggah file", "newKnowledge.uploadFilesDescription": "Tambahkan file dari komputer — file akan diindeks segera setelah dibuat.", + "newKnowledge.uploadingFiles": "Mengunggah…", "newKnowledge.usingDefaults": "Menggunakan pengaturan default", "newKnowledge.viewLabel": "Tampilan pengetahuan", "newKnowledge.websiteCrawl": "Perayapan situs web", diff --git a/web/i18n/it-IT/dataset.json b/web/i18n/it-IT/dataset.json index df33708a7c8..16f6d733e00 100644 --- a/web/i18n/it-IT/dataset.json +++ b/web/i18n/it-IT/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Azioni", "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.allSources": "Tutte le fonti", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Annulla", "newKnowledge.cardType": "Spazio di conoscenza", "newKnowledge.configureProvider": "Configura {{provider}}", + "newKnowledge.connectNotion": "Connetti Notion", "newKnowledge.connectProvider": "Connetti {{provider}}", + "newKnowledge.connectProviderGeneric": "Connetti provider", "newKnowledge.connectSource": "Collega una fonte", "newKnowledge.connectSourceDescription": "I contenuti restano sincronizzati con Notion, siti web e altri servizi.", "newKnowledge.connectingProvider": "Connessione…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Il crawler non ha potuto completare questa anteprima. Controlla l’URL, modifica le opzioni di scansione o riprova.", "newKnowledge.crawlFailedProvider": "{{provider}} ha segnalato un errore. Controlla la connessione al provider e riprova.", "newKnowledge.crawlFailedTimeout": "La scansione ha superato il tempo limite. Prova un URL più specifico o riduci il numero massimo di pagine.", + "newKnowledge.crawlNotStarted": "Non avviato", "newKnowledge.crawlOptions": "Opzioni di scansione", + "newKnowledge.crawlPreview": "Anteprima scansione", "newKnowledge.crawlProgress": "Avanzamento della scansione di {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "La configurazione della scansione del sito web è disabilitata finché il flusso di lavoro di scansione non è disponibile.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl è pronto", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Scansione in corso…", "newKnowledge.crawlingPages": "Scansione di {{host}}… {{count}} pagine trovate", "newKnowledge.createFailed": "Non è stato possibile creare questa base di conoscenza. Controlla il tuo accesso e riprova.", + "newKnowledge.createIllustrationTitle": "Collega i contenuti una sola volta: ogni app risponde con le conoscenze più aggiornate.", "newKnowledge.createTitle": "Creare Conoscenza", "newKnowledge.creators": "Autori", + "newKnowledge.customIntervalHours": "Intervallo personalizzato (ore)", + "newKnowledge.customIntervalInvalid": "Inserisci un numero intero compreso tra 1 e 720 ore.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Scarta bozza", "newKnowledge.discardDraftDescription": "Le modifiche a questa bozza della knowledge base andranno perse.", "newKnowledge.discardDraftTitle": "Scartare questa bozza della knowledge base?", + "newKnowledge.discardSourceChanges": "Ignorare le modifiche all’origine?", + "newKnowledge.discardSourceChangesConfirm": "Ignora modifiche", + "newKnowledge.discardSourceChangesDescription": "L’anteprima della scansione, la selezione delle pagine e le impostazioni di sincronizzazione andranno perse.", "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.documents": "Documenti", "newKnowledge.documentsUnavailable": "— documenti", + "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", "newKnowledge.errorDescription": "Non è stato possibile caricare le nuove knowledge base. Riprova.", "newKnowledge.errorTitle": "Si è verificato un problema", "newKnowledge.filtersUnavailable": "Questo filtro sarà disponibile quando KnowledgeFS esporrà i relativi metadati.", + "newKnowledge.firecrawlUnavailable": "Firecrawl non è disponibile nel catalogo del provider per questa area di lavoro.", "newKnowledge.gotIt": "Ho capito", "newKnowledge.guideDescription": "Collega una fonte e i suoi documenti resteranno sincronizzati, indicizzati automaticamente e pronti all’uso, senza nulla da gestire. Le knowledge base precedenti continueranno a funzionare come prima.", "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.invalidRootUrl": "Inserisci un URL http(s) valido.", + "newKnowledge.keepEditing": "Continua a modificare", "newKnowledge.lastSyncColumn": "Ultima sincronizzazione", "newKnowledge.learnMore": "Scopri di più", "newKnowledge.leavePartialSetupConfirm": "Esci dalla configurazione", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Uscire dalla configurazione incompleta?", "newKnowledge.legacy": "Legacy", "newKnowledge.loadMore": "Carica altro", + "newKnowledge.loadingSyncPolicy": "Caricamento dei criteri di sincronizzazione", "newKnowledge.maxPages": "Numero massimo di pagine", - "newKnowledge.moreProviders": "Vedi di più", + "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.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Il crawler ha raggiunto il sito ma non ha trovato pagine indicizzabili. Prova un URL diverso o amplia le opzioni di scansione.", "newKnowledge.notFoundDescription": "Questa knowledge base potrebbe essere stata eliminata o potresti non avere accesso.", "newKnowledge.notFoundTitle": "Base di conoscenza non trovata", + "newKnowledge.notionNotConnected": "Notion non è connesso", + "newKnowledge.notionNotConnectedDescription": "Connetti Notion per scegliere le pagine e mantenerle sincronizzate.", "newKnowledge.onlineDocuments": "Documenti online", "newKnowledge.onlineDrive": "Unità in linea", "newKnowledge.pagesAppearDescription": "Esegui la scansione del sito per verificare quali pagine verranno aggiunte.", "newKnowledge.pagesAppearTitle": "Le pagine appariranno qui", "newKnowledge.pagesCrawled": "{{count}} pagine sottoposte a scansione su {{host}}", + "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.permission": "Autorizzazione", "newKnowledge.permissionAllMembers": "Area di lavoro · tutti i membri possono visualizzare e modificare", "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.providerConnected": "{{provider}} connesso", "newKnowledge.providerLabel": "Seleziona un fornitore", "newKnowledge.providerLoadFailed": "Impossibile caricare i provider", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Rimuovi la fonte", "newKnowledge.retryCrawl": "Riprova la scansione", "newKnowledge.retryProviderLoad": "Riprova", + "newKnowledge.retrySyncPolicy": "Riprova i criteri di sincronizzazione", "newKnowledge.rootUrl": "URL principale", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Questa sezione diventerà disponibile nella fase di configurazione successiva.", "newKnowledge.searchSources": "Cerca fonti", + "newKnowledge.selectAll": "Seleziona tutto", + "newKnowledge.selectAllSources": "Seleziona tutte le origini", + "newKnowledge.selectFilesAndFolders": "Seleziona file e cartelle da sincronizzare", + "newKnowledge.skippedFailed": "Ignorato (errore)", + "newKnowledge.skippedOffDomain": "Ignorato (fuori dominio)", "newKnowledge.sourceActions": "Azioni per {{name}}", "newKnowledge.sourceColumn": "Fonte", "newKnowledge.sourceFilterLabel": "Filtra le fonti per stato", "newKnowledge.sourceName": "Nome della fonte", "newKnowledge.sourceNamePlaceholder": "es. scansione della documentazione del prodotto", "newKnowledge.sourceNameRequired": "Inserisci un nome per la fonte.", + "newKnowledge.sourceSetupBackendDependency": "La configurazione dell’origine è mostrata per intero; modifica e anteprima richiedono il flusso backend dell’origine in bozza.", "newKnowledge.sourceStatus.active": "Attivo", "newKnowledge.sourceStatus.disabled": "Disabilitato", "newKnowledge.sourceStatus.error": "Errore", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Interrompi", "newKnowledge.stoppingCrawl": "Interruzione in corso…", "newKnowledge.syncNow": "Sincronizza ora", + "newKnowledge.syncPolicy": "Criteri di sincronizzazione", "newKnowledge.syncPolicyColumn": "Criterio di sincronizzazione", + "newKnowledge.syncPolicyCustom": "Intervallo personalizzato", + "newKnowledge.syncPolicyDaily": "Ogni 24 ore", + "newKnowledge.syncPolicyLoadFailed": "Impossibile caricare i criteri di sincronizzazione.", + "newKnowledge.syncPolicyManual": "Solo manuale", + "newKnowledge.syncPolicyProvider": "Usa la pianificazione del provider", "newKnowledge.tags": "Tag", "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.uploadCharactersUnavailable": "Caratteri: —", "newKnowledge.uploadFiles": "Carica file", "newKnowledge.uploadFilesDescription": "Aggiungi file dal computer: vengono indicizzati appena li crei.", + "newKnowledge.uploadingFiles": "Caricamento…", "newKnowledge.usingDefaults": "Impostazioni predefinite in uso", "newKnowledge.viewLabel": "Vista conoscenza", "newKnowledge.websiteCrawl": "Scansione del sito web", diff --git a/web/i18n/ja-JP/dataset.json b/web/i18n/ja-JP/dataset.json index 633343632f5..8dbfc1e10fc 100644 --- a/web/i18n/ja-JP/dataset.json +++ b/web/i18n/ja-JP/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "アクション", "newKnowledge.addSource": "ソースを追加", "newKnowledge.addSourceDescription": "ソースをこのナレッジ ベースに接続すると、自動的に同期され、最新の状態が保たれます。", + "newKnowledge.addSourceFailed": "このソースを追加できませんでした。設定を確認して、もう一度お試しください。", "newKnowledge.addSourceRequiresSelection": "追加前にクロール済みページを1つ以上選択してください。", "newKnowledge.adjustAndRecrawl": "調整して再クロール", "newKnowledge.allSources": "すべてのソース", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "キャンセル", "newKnowledge.cardType": "ナレッジスペース", "newKnowledge.configureProvider": "{{provider}} を構成する", + "newKnowledge.connectNotion": "Notion に接続", "newKnowledge.connectProvider": "{{provider}} を接続する", + "newKnowledge.connectProviderGeneric": "プロバイダーに接続", "newKnowledge.connectSource": "ソースを接続", "newKnowledge.connectSourceDescription": "コンテンツは Notion、Web サイト、その他のサービスと同期され続けます。", "newKnowledge.connectingProvider": "接続中…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "クローラーはこのプレビューを完了できませんでした。URLを確認し、クロールオプションを調整するか、もう一度お試しください。", "newKnowledge.crawlFailedProvider": "{{provider}}でエラーが報告されました。プロバイダー接続を確認して、もう一度お試しください。", "newKnowledge.crawlFailedTimeout": "クロールがタイムアウトしました。対象を絞ったURLを指定するか、最大ページ数を減らしてください。", + "newKnowledge.crawlNotStarted": "未開始", "newKnowledge.crawlOptions": "クロールオプション", + "newKnowledge.crawlPreview": "クロールのプレビュー", "newKnowledge.crawlProgress": "{{host}} のクロール進行状況", "newKnowledge.crawlSetupUnavailableDescription": "Web サイトのクロール設定は、クロール ワークフローが利用可能になるまで無効になります。", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl の準備ができました", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "クロール中…", "newKnowledge.crawlingPages": "{{host}} をクロール中… {{count}}ページが見つかりました", "newKnowledge.createFailed": "このナレッジ ベースを作成できませんでした。アクセスを確認して再試行してください。", + "newKnowledge.createIllustrationTitle": "コンテンツを一度接続すれば、すべてのアプリが最新のナレッジで回答します。", "newKnowledge.createTitle": "知識の創造", "newKnowledge.creators": "作成者", + "newKnowledge.customIntervalHours": "カスタム間隔(時間)", + "newKnowledge.customIntervalInvalid": "1~720 の整数を入力してください。", "newKnowledge.description": "説明", "newKnowledge.descriptionHelp": "明確な説明は、アプリがこのナレッジをいつ検索するかを決定するのに役立ちます。", "newKnowledge.descriptionPlaceholder": "例:返金ポリシー、配送に関する FAQ、エスカレーション手順 - カスタマー サポートの質問に使用します", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "下書きを破棄", "newKnowledge.discardDraftDescription": "このナレッジベースの下書きに加えた変更は失われます。", "newKnowledge.discardDraftTitle": "このナレッジベースの下書きを破棄しますか?", + "newKnowledge.discardSourceChanges": "ソースの変更を破棄しますか?", + "newKnowledge.discardSourceChangesConfirm": "変更を破棄", + "newKnowledge.discardSourceChangesDescription": "クロールのプレビュー、ページの選択、同期設定は失われます。", "newKnowledge.discardSourceDraftDescription": "ソース設定とクロールプレビューは削除されます。", "newKnowledge.discardSourceDraftFailed": "ソースの下書きを破棄できませんでした。もう一度お試しください。", "newKnowledge.discardSourceDraftTitle": "このソースの下書きを破棄しますか?", "newKnowledge.documents": "書類", "newKnowledge.documentsUnavailable": "— 件のドキュメント", + "newKnowledge.editSource": "編集", "newKnowledge.emptyDescription": "ドキュメントを追加すると、エージェントが直接読み取れます。インデックス設定は不要です。", "newKnowledge.emptyTitle": "ナレッジベースをよりスマートに構築", "newKnowledge.errorDescription": "新しいナレッジベースを読み込めませんでした。もう一度お試しください。", "newKnowledge.errorTitle": "問題が発生しました", "newKnowledge.filtersUnavailable": "KnowledgeFS がメタデータを提供すると、このフィルターを利用できます。", + "newKnowledge.firecrawlUnavailable": "Firecrawl は、このワークスペースのプロバイダー カタログでは使用できません。", "newKnowledge.gotIt": "了解", "newKnowledge.guideDescription": "ソースを接続すると、そのドキュメントは同期され、自動的にインデックス化されてすぐに利用できます。管理する設定はありません。従来のナレッジベースもこれまでどおり動作します。", "newKnowledge.guideTitle": "新しいナレッジベースへようこそ ✨", "newKnowledge.illustrationHeadline": "コンテンツを一度接続すれば、すべてのアプリが最新のナレッジで回答します。", "newKnowledge.includeSubpages": "サブページを含める", "newKnowledge.invalidRootUrl": "有効なhttp(s) URLを入力してください。", + "newKnowledge.keepEditing": "編集を続ける", "newKnowledge.lastSyncColumn": "最終同期", "newKnowledge.learnMore": "詳細を見る", "newKnowledge.leavePartialSetupConfirm": "セットアップを終了", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "未完了のセットアップを終了しますか?", "newKnowledge.legacy": "旧バージョン", "newKnowledge.loadMore": "さらに読み込む", + "newKnowledge.loadingSyncPolicy": "同期ポリシーを読み込み中", "newKnowledge.maxPages": "最大ページ数", - "newKnowledge.moreProviders": "もっと見る", + "newKnowledge.moreProviders": "その他のプロバイダー", "newKnowledge.name": "ナレッジ名", "newKnowledge.namePlaceholder": "例:カスタマーサポートハンドブック", "newKnowledge.nameRequired": "ナレッジベース名を入力します。", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "サイトには到達しましたが、インデックス可能なページが見つかりませんでした。別のURLを試すか、クロールオプションを広げてください。", "newKnowledge.notFoundDescription": "このナレッジ ベースは削除されているか、アクセスできない可能性があります。", "newKnowledge.notFoundTitle": "ナレッジベースが見つかりません", + "newKnowledge.notionNotConnected": "Notion に接続されていません", + "newKnowledge.notionNotConnectedDescription": "Notion に接続してページを選択し、同期を維持します。", "newKnowledge.onlineDocuments": "オンライン文書", "newKnowledge.onlineDrive": "オンラインドライブ", "newKnowledge.pagesAppearDescription": "サイトをクロールして、追加されるページを確認します。", "newKnowledge.pagesAppearTitle": "ページはここに表示されます", "newKnowledge.pagesCrawled": "{{host}} で{{count}}ページをクロールしました", + "newKnowledge.pagesCrawled_one": "{{host}} で{{count}}ページをクロールしました", + "newKnowledge.pagesCrawled_other": "{{host}} で{{count}}ページをクロールしました", + "newKnowledge.pagesSelected": "{{count}} 件選択済み", "newKnowledge.permission": "許可", "newKnowledge.permissionAllMembers": "ワークスペース · すべてのメンバーが表示および編集できます", "newKnowledge.permissionOnlyMe": "私だけ", "newKnowledge.permissionRestricted": "ワークスペースの権限では、この設定を変更することはできません。", "newKnowledge.permissionUpdateFailed": "ナレッジベースは作成されましたが、権限を確認できませんでした。再試行してセットアップを完了してください。", + "newKnowledge.preview": "プレビュー", + "newKnowledge.previewUnavailable": "バックエンドでこのファイルの処理が完了すると、プレビューを利用できます。", "newKnowledge.providerConnected": "{{provider}} が接続されました", "newKnowledge.providerLabel": "プロバイダーを選択する", "newKnowledge.providerLoadFailed": "プロバイダーを読み込めませんでした", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "ソースを削除", "newKnowledge.retryCrawl": "クロールを再試行", "newKnowledge.retryProviderLoad": "再試行", + "newKnowledge.retrySyncPolicy": "同期ポリシーを再読み込み", "newKnowledge.rootUrl": "ルートURL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "このセクションは、次のセットアップ手順で使用できるようになります。", "newKnowledge.searchSources": "ソースを検索", + "newKnowledge.selectAll": "すべて選択", + "newKnowledge.selectAllSources": "すべてのソースを選択", + "newKnowledge.selectFilesAndFolders": "同期するファイルとフォルダーを選択", + "newKnowledge.skippedFailed": "スキップ(失敗)", + "newKnowledge.skippedOffDomain": "スキップ(ドメイン外)", "newKnowledge.sourceActions": "{{name}} のアクション", "newKnowledge.sourceColumn": "ソース", "newKnowledge.sourceFilterLabel": "ソースをステータスでフィルタリングする", "newKnowledge.sourceName": "ソース名", "newKnowledge.sourceNamePlaceholder": "例:製品ドキュメントのクロール", "newKnowledge.sourceNameRequired": "ソース名を入力してください。", + "newKnowledge.sourceSetupBackendDependency": "ソース設定はすべて表示されています。編集とプレビューには、下書きソースのバックエンドワークフローが必要です。", "newKnowledge.sourceStatus.active": "アクティブ", "newKnowledge.sourceStatus.disabled": "無効", "newKnowledge.sourceStatus.error": "エラー", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "停止", "newKnowledge.stoppingCrawl": "停止中…", "newKnowledge.syncNow": "今すぐ同期", + "newKnowledge.syncPolicy": "同期ポリシー", "newKnowledge.syncPolicyColumn": "同期ポリシー", + "newKnowledge.syncPolicyCustom": "カスタム間隔", + "newKnowledge.syncPolicyDaily": "24時間ごと", + "newKnowledge.syncPolicyLoadFailed": "同期ポリシーを読み込めませんでした。", + "newKnowledge.syncPolicyManual": "手動のみ", + "newKnowledge.syncPolicyProvider": "プロバイダーのスケジュールを使用", "newKnowledge.tags": "タグ", "newKnowledge.unavailableDescription": "このワークスペースでは現在 KnowledgeFS を利用できません。", "newKnowledge.unavailableTitle": "新しいナレッジは利用できません", "newKnowledge.updated": "{{date}} に更新", + "newKnowledge.uploadCharactersUnavailable": "文字数:—", "newKnowledge.uploadFiles": "ファイルをアップロード", "newKnowledge.uploadFilesDescription": "コンピューターからファイルを追加すると、作成時にすぐインデックス化されます。", + "newKnowledge.uploadingFiles": "アップロード中…", "newKnowledge.usingDefaults": "デフォルト設定を使用中", "newKnowledge.viewLabel": "ナレッジ表示", "newKnowledge.websiteCrawl": "ウェブサイトのクロール", diff --git a/web/i18n/ko-KR/dataset.json b/web/i18n/ko-KR/dataset.json index b936189e2be..ccb7239968e 100644 --- a/web/i18n/ko-KR/dataset.json +++ b/web/i18n/ko-KR/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "작업", "newKnowledge.addSource": "소스 추가", "newKnowledge.addSourceDescription": "이 지식 베이스에 소스를 연결하세요. 자동으로 동기화되고 최신 상태로 유지됩니다.", + "newKnowledge.addSourceFailed": "이 소스를 추가하지 못했습니다. 설정을 검토한 후 다시 시도하세요.", "newKnowledge.addSourceRequiresSelection": "소스를 추가하기 전에 크롤링된 페이지를 하나 이상 선택하세요.", "newKnowledge.adjustAndRecrawl": "조정 후 다시 크롤링", "newKnowledge.allSources": "모든 소스", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "취소", "newKnowledge.cardType": "지식 공간", "newKnowledge.configureProvider": "{{provider}} 구성", + "newKnowledge.connectNotion": "Notion 연결", "newKnowledge.connectProvider": "{{provider}} 연결", + "newKnowledge.connectProviderGeneric": "공급자 연결", "newKnowledge.connectSource": "소스 연결", "newKnowledge.connectSourceDescription": "콘텐츠가 Notion, 웹사이트 및 기타 서비스와 계속 동기화됩니다.", "newKnowledge.connectingProvider": "연결 중…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "크롤러가 이 미리보기를 완료하지 못했습니다. URL을 확인하고 크롤링 옵션을 조정한 후 다시 시도하세요.", "newKnowledge.crawlFailedProvider": "{{provider}}에서 오류를 보고했습니다. 제공업체 연결을 확인하고 다시 시도하세요.", "newKnowledge.crawlFailedTimeout": "크롤링 시간이 초과되었습니다. 범위가 더 좁은 URL을 사용하거나 최대 페이지 수를 줄이세요.", + "newKnowledge.crawlNotStarted": "시작되지 않음", "newKnowledge.crawlOptions": "크롤링 옵션", + "newKnowledge.crawlPreview": "크롤링 미리보기", "newKnowledge.crawlProgress": "{{host}} 크롤링 진행률", "newKnowledge.crawlSetupUnavailableDescription": "크롤링 워크플로를 사용할 수 있을 때까지 웹사이트 크롤링 구성이 비활성화됩니다.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl이 준비되었습니다.", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "크롤링 중…", "newKnowledge.crawlingPages": "{{host}} 크롤링 중… {{count}}개 페이지 발견", "newKnowledge.createFailed": "이 기술 자료를 만들 수 없습니다. 액세스 권한을 확인하고 다시 시도하세요.", + "newKnowledge.createIllustrationTitle": "콘텐츠를 한 번 연결하면 모든 앱이 최신 지식으로 답변합니다.", "newKnowledge.createTitle": "지식 창조", "newKnowledge.creators": "생성자", + "newKnowledge.customIntervalHours": "사용자 지정 간격(시간)", + "newKnowledge.customIntervalInvalid": "1~720 사이의 정수를 입력하세요.", "newKnowledge.description": "설명", "newKnowledge.descriptionHelp": "명확한 설명은 앱이 이 지식을 검색할 시기를 결정하는 데 도움이 됩니다.", "newKnowledge.descriptionPlaceholder": "예를 들어 환불 정책, 배송 FAQ, 에스컬레이션 단계 - 고객 지원 질문에 사용", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "초안 삭제", "newKnowledge.discardDraftDescription": "이 지식 베이스 초안의 변경 사항이 삭제됩니다.", "newKnowledge.discardDraftTitle": "이 지식 베이스 초안을 삭제할까요?", + "newKnowledge.discardSourceChanges": "소스 변경 사항을 삭제할까요?", + "newKnowledge.discardSourceChangesConfirm": "변경 사항 삭제", + "newKnowledge.discardSourceChangesDescription": "크롤링 미리보기, 페이지 선택 및 동기화 설정이 사라집니다.", "newKnowledge.discardSourceDraftDescription": "소스 구성과 크롤링 미리보기가 삭제됩니다.", "newKnowledge.discardSourceDraftFailed": "소스 초안을 삭제할 수 없습니다. 다시 시도하세요.", "newKnowledge.discardSourceDraftTitle": "이 소스 초안을 삭제할까요?", "newKnowledge.documents": "문서", "newKnowledge.documentsUnavailable": "문서 —개", + "newKnowledge.editSource": "편집", "newKnowledge.emptyDescription": "문서를 추가하면 에이전트가 직접 읽을 수 있습니다. 인덱싱 설정은 필요하지 않습니다.", "newKnowledge.emptyTitle": "지식 베이스를 구축하는 더 스마트한 방법", "newKnowledge.errorDescription": "새 지식 베이스를 불러오지 못했습니다. 다시 시도하세요.", "newKnowledge.errorTitle": "문제가 발생했습니다", "newKnowledge.filtersUnavailable": "KnowledgeFS에서 메타데이터를 제공하면 이 필터를 사용할 수 있습니다.", + "newKnowledge.firecrawlUnavailable": "이 작업공간의 공급자 카탈로그에서는 Firecrawl을 사용할 수 없습니다.", "newKnowledge.gotIt": "확인", "newKnowledge.guideDescription": "소스를 연결하면 문서가 계속 동기화되고 자동으로 인덱싱되어 바로 사용할 수 있습니다. 관리할 설정은 없습니다. 기존 지식 베이스도 그대로 작동합니다.", "newKnowledge.guideTitle": "새로운 지식 베이스를 만나보세요 ✨", "newKnowledge.illustrationHeadline": "콘텐츠를 한 번 연결하면 모든 앱이 최신 지식으로 답변합니다.", "newKnowledge.includeSubpages": "하위 페이지 포함", "newKnowledge.invalidRootUrl": "유효한 http(s) URL을 입력하세요.", + "newKnowledge.keepEditing": "계속 편집", "newKnowledge.lastSyncColumn": "마지막 동기화", "newKnowledge.learnMore": "자세히 알아보기", "newKnowledge.leavePartialSetupConfirm": "설정 나가기", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "완료되지 않은 설정을 나갈까요?", "newKnowledge.legacy": "레거시", "newKnowledge.loadMore": "더 보기", + "newKnowledge.loadingSyncPolicy": "동기화 정책 불러오는 중", "newKnowledge.maxPages": "최대 페이지 수", - "newKnowledge.moreProviders": "더보기", + "newKnowledge.moreProviders": "더 많은 공급자", "newKnowledge.name": "지식 이름", "newKnowledge.namePlaceholder": "예를 들어 고객 지원 핸드북", "newKnowledge.nameRequired": "기술 자료 이름을 입력하세요.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "사이트에 접속했지만 색인 가능한 페이지를 찾지 못했습니다. 다른 URL을 사용하거나 크롤링 옵션을 확장하세요.", "newKnowledge.notFoundDescription": "이 기술 자료가 삭제되었거나 액세스 권한이 없을 수 있습니다.", "newKnowledge.notFoundTitle": "기술 자료를 찾을 수 없습니다.", + "newKnowledge.notionNotConnected": "Notion이 연결되지 않음", + "newKnowledge.notionNotConnectedDescription": "페이지를 선택하고 동기화 상태를 유지하려면 Notion을 연결하세요.", "newKnowledge.onlineDocuments": "온라인 문서", "newKnowledge.onlineDrive": "온라인 드라이브", "newKnowledge.pagesAppearDescription": "사이트를 크롤링하여 추가될 페이지를 확인하세요.", "newKnowledge.pagesAppearTitle": "페이지가 여기에 표시됩니다", "newKnowledge.pagesCrawled": "{{host}}에서 {{count}}개 페이지를 크롤링했습니다", + "newKnowledge.pagesCrawled_one": "{{host}}에서 {{count}}개 페이지를 크롤링했습니다", + "newKnowledge.pagesCrawled_other": "{{host}}에서 {{count}}개 페이지를 크롤링했습니다", + "newKnowledge.pagesSelected": "{{count}}개 선택됨", "newKnowledge.permission": "허가", "newKnowledge.permissionAllMembers": "작업공간 · 모든 구성원이 보고 편집할 수 있습니다.", "newKnowledge.permissionOnlyMe": "나만", "newKnowledge.permissionRestricted": "작업공간 권한으로 인해 이 설정을 변경할 수 없습니다.", "newKnowledge.permissionUpdateFailed": "지식 베이스가 생성되었지만 권한을 확인하지 못했습니다. 다시 시도하여 설정을 완료하세요.", + "newKnowledge.preview": "미리보기", + "newKnowledge.previewUnavailable": "백엔드에서 이 파일 처리를 완료하면 미리보기를 사용할 수 있습니다.", "newKnowledge.providerConnected": "{{provider}} 연결됨", "newKnowledge.providerLabel": "제공자 선택", "newKnowledge.providerLoadFailed": "제공자를 로드할 수 없습니다.", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "소스 제거", "newKnowledge.retryCrawl": "크롤링 다시 시도", "newKnowledge.retryProviderLoad": "재시도", + "newKnowledge.retrySyncPolicy": "동기화 정책 다시 시도", "newKnowledge.rootUrl": "루트 URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "이 섹션은 다음 설정 단계에서 사용할 수 있습니다.", "newKnowledge.searchSources": "소스 검색", + "newKnowledge.selectAll": "모두 선택", + "newKnowledge.selectAllSources": "모든 소스 선택", + "newKnowledge.selectFilesAndFolders": "동기화할 파일과 폴더 선택", + "newKnowledge.skippedFailed": "건너뜀(실패)", + "newKnowledge.skippedOffDomain": "건너뜀(도메인 외부)", "newKnowledge.sourceActions": "{{name}}에 대한 작업", "newKnowledge.sourceColumn": "출처", "newKnowledge.sourceFilterLabel": "상태별로 소스 필터링", "newKnowledge.sourceName": "소스 이름", "newKnowledge.sourceNamePlaceholder": "예: 제품 문서 크롤링", "newKnowledge.sourceNameRequired": "소스 이름을 입력하세요.", + "newKnowledge.sourceSetupBackendDependency": "소스 설정은 전체가 표시되며 편집과 미리보기에는 초안 소스 백엔드 워크플로가 필요합니다.", "newKnowledge.sourceStatus.active": "활성", "newKnowledge.sourceStatus.disabled": "비활성화됨", "newKnowledge.sourceStatus.error": "오류", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "중지", "newKnowledge.stoppingCrawl": "중지 중…", "newKnowledge.syncNow": "지금 동기화", + "newKnowledge.syncPolicy": "동기화 정책", "newKnowledge.syncPolicyColumn": "동기화 정책", + "newKnowledge.syncPolicyCustom": "사용자 지정 간격", + "newKnowledge.syncPolicyDaily": "24시간마다", + "newKnowledge.syncPolicyLoadFailed": "동기화 정책을 불러오지 못했습니다.", + "newKnowledge.syncPolicyManual": "수동 전용", + "newKnowledge.syncPolicyProvider": "제공업체 일정 사용", "newKnowledge.tags": "태그", "newKnowledge.unavailableDescription": "현재 이 워크스페이스에서 KnowledgeFS를 사용할 수 없습니다.", "newKnowledge.unavailableTitle": "새 지식을 사용할 수 없음", "newKnowledge.updated": "{{date}} 업데이트", + "newKnowledge.uploadCharactersUnavailable": "문자 수: —", "newKnowledge.uploadFiles": "파일 업로드", "newKnowledge.uploadFilesDescription": "컴퓨터에서 파일을 추가하면 생성 즉시 인덱싱됩니다.", + "newKnowledge.uploadingFiles": "업로드 중…", "newKnowledge.usingDefaults": "기본값 사용 중", "newKnowledge.viewLabel": "지식 보기", "newKnowledge.websiteCrawl": "웹사이트 크롤링", diff --git a/web/i18n/nl-NL/dataset.json b/web/i18n/nl-NL/dataset.json index 3c0bd1fca16..a13f71037cb 100644 --- a/web/i18n/nl-NL/dataset.json +++ b/web/i18n/nl-NL/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Acties", "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.allSources": "Alle bronnen", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Annuleren", "newKnowledge.cardType": "Kennisruimte", "newKnowledge.configureProvider": "{{provider}} configureren", + "newKnowledge.connectNotion": "Notion verbinden", "newKnowledge.connectProvider": "{{provider}} verbinden", + "newKnowledge.connectProviderGeneric": "Provider verbinden", "newKnowledge.connectSource": "Bron verbinden", "newKnowledge.connectSourceDescription": "Inhoud blijft gesynchroniseerd met Notion, websites en andere diensten.", "newKnowledge.connectingProvider": "Verbinden…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "De crawler kon dit voorbeeld niet voltooien. Controleer de URL, pas de crawlopties aan of probeer het opnieuw.", "newKnowledge.crawlFailedProvider": "{{provider}} heeft een fout gemeld. Controleer de providerverbinding en probeer het opnieuw.", "newKnowledge.crawlFailedTimeout": "De crawl heeft de tijdslimiet overschreden. Probeer een specifiekere URL of verlaag het maximale aantal pagina’s.", + "newKnowledge.crawlNotStarted": "Niet gestart", "newKnowledge.crawlOptions": "Crawlopties", + "newKnowledge.crawlPreview": "Crawlvoorbeeld", "newKnowledge.crawlProgress": "Crawlvoortgang voor {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "De configuratie van de websitecrawl is uitgeschakeld totdat de crawlworkflow beschikbaar is.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl is klaar", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Bezig met crawlen…", "newKnowledge.crawlingPages": "{{host}} wordt gecrawld… {{count}} pagina’s gevonden", "newKnowledge.createFailed": "We konden deze kennisbank niet creëren. Controleer uw toegang en probeer het opnieuw.", + "newKnowledge.createIllustrationTitle": "Koppel je inhoud één keer — elke app antwoordt met de meest actuele kennis.", "newKnowledge.createTitle": "Creëer kennis", "newKnowledge.creators": "Makers", + "newKnowledge.customIntervalHours": "Aangepast interval (uren)", + "newKnowledge.customIntervalInvalid": "Voer een geheel getal van 1 tot 720 uur in.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Concept verwijderen", "newKnowledge.discardDraftDescription": "Uw wijzigingen aan dit kennisbankconcept gaan verloren.", "newKnowledge.discardDraftTitle": "Dit kennisbankconcept verwijderen?", + "newKnowledge.discardSourceChanges": "Wijzigingen aan de bron negeren?", + "newKnowledge.discardSourceChangesConfirm": "Wijzigingen negeren", + "newKnowledge.discardSourceChangesDescription": "De crawlvoorvertoning, paginaselectie en synchronisatie-instellingen gaan verloren.", "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.documents": "Documenten", "newKnowledge.documentsUnavailable": "— documenten", + "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", "newKnowledge.errorDescription": "We konden je nieuwe kennisbanken niet laden. Probeer het opnieuw.", "newKnowledge.errorTitle": "Er is iets misgegaan", "newKnowledge.filtersUnavailable": "Dit filter wordt beschikbaar zodra KnowledgeFS metadata aanbiedt.", + "newKnowledge.firecrawlUnavailable": "Firecrawl is niet beschikbaar in de providercatalogus voor deze werkruimte.", "newKnowledge.gotIt": "Begrepen", "newKnowledge.guideDescription": "Verbind een bron en de documenten blijven gesynchroniseerd, automatisch geïndexeerd en direct klaar voor gebruik, zonder beheer. Je bestaande kennisbanken blijven ongewijzigd werken.", "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.invalidRootUrl": "Voer een geldige http(s)-URL in.", + "newKnowledge.keepEditing": "Doorgaan met bewerken", "newKnowledge.lastSyncColumn": "Laatste synchronisatie", "newKnowledge.learnMore": "Meer informatie", "newKnowledge.leavePartialSetupConfirm": "Configuratie verlaten", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Onvoltooide configuratie verlaten?", "newKnowledge.legacy": "Oud", "newKnowledge.loadMore": "Meer laden", + "newKnowledge.loadingSyncPolicy": "Synchronisatiebeleid laden", "newKnowledge.maxPages": "Maximumaantal pagina’s", - "newKnowledge.moreProviders": "View more", + "newKnowledge.moreProviders": "Meer providers", "newKnowledge.name": "Kennis naam", "newKnowledge.namePlaceholder": "bijv. Handboek voor klantenondersteuning", "newKnowledge.nameRequired": "Voer een kennisbanknaam in.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "De crawler bereikte de site, maar vond geen indexeerbare pagina’s. Probeer een andere URL of breid de crawlopties uit.", "newKnowledge.notFoundDescription": "Deze kennisbank is mogelijk verwijderd of u heeft mogelijk geen toegang.", "newKnowledge.notFoundTitle": "Kennisbank niet gevonden", + "newKnowledge.notionNotConnected": "Notion is niet verbonden", + "newKnowledge.notionNotConnectedDescription": "Verbind Notion om pagina’s te kiezen en gesynchroniseerd te houden.", "newKnowledge.onlineDocuments": "Onlinedocumenten", "newKnowledge.onlineDrive": "Online rijden", "newKnowledge.pagesAppearDescription": "Crawl de site om te bekijken welke pagina’s worden toegevoegd.", "newKnowledge.pagesAppearTitle": "Pagina’s verschijnen hier", "newKnowledge.pagesCrawled": "{{count}} pagina’s gecrawld op {{host}}", + "newKnowledge.pagesCrawled_one": "{{count}} pagina’s gecrawld op {{host}}", + "newKnowledge.pagesCrawled_other": "{{count}} pagina’s gecrawld op {{host}}", + "newKnowledge.pagesSelected": "{{count}} geselecteerd", "newKnowledge.permission": "Toestemming", "newKnowledge.permissionAllMembers": "Werkruimte · alle leden kunnen bekijken en bewerken", "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.providerConnected": "{{provider}} aangesloten", "newKnowledge.providerLabel": "Select a provider", "newKnowledge.providerLoadFailed": "Providers konden niet laden", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Bron verwijderen", "newKnowledge.retryCrawl": "Crawl opnieuw proberen", "newKnowledge.retryProviderLoad": "Probeer het opnieuw", + "newKnowledge.retrySyncPolicy": "Synchronisatiebeleid opnieuw proberen", "newKnowledge.rootUrl": "Hoofd-URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Deze sectie komt beschikbaar in de volgende installatiestap.", "newKnowledge.searchSources": "Zoek bronnen", + "newKnowledge.selectAll": "Alles selecteren", + "newKnowledge.selectAllSources": "Alle bronnen selecteren", + "newKnowledge.selectFilesAndFolders": "Selecteer bestanden en mappen om te synchroniseren", + "newKnowledge.skippedFailed": "Overgeslagen (mislukt)", + "newKnowledge.skippedOffDomain": "Overgeslagen (buiten domein)", "newKnowledge.sourceActions": "Acties voor {{name}}", "newKnowledge.sourceColumn": "Bron", "newKnowledge.sourceFilterLabel": "Filter bronnen op status", "newKnowledge.sourceName": "Bronnaam", "newKnowledge.sourceNamePlaceholder": "bijv. crawl van productdocumentatie", "newKnowledge.sourceNameRequired": "Voer een bronnaam in.", + "newKnowledge.sourceSetupBackendDependency": "De bronconfiguratie wordt volledig getoond; bewerken en bekijken vereisen de backendworkflow voor conceptbronnen.", "newKnowledge.sourceStatus.active": "Actief", "newKnowledge.sourceStatus.disabled": "Uitgeschakeld", "newKnowledge.sourceStatus.error": "Fout", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Stoppen", "newKnowledge.stoppingCrawl": "Bezig met stoppen…", "newKnowledge.syncNow": "Synchroniseer nu", + "newKnowledge.syncPolicy": "Synchronisatiebeleid", "newKnowledge.syncPolicyColumn": "Synchronisatiebeleid", + "newKnowledge.syncPolicyCustom": "Aangepast interval", + "newKnowledge.syncPolicyDaily": "Elke 24 uur", + "newKnowledge.syncPolicyLoadFailed": "Het synchronisatiebeleid kon niet worden geladen.", + "newKnowledge.syncPolicyManual": "Alleen handmatig", + "newKnowledge.syncPolicyProvider": "Planning van provider gebruiken", "newKnowledge.tags": "Tags", "newKnowledge.unavailableDescription": "KnowledgeFS is momenteel niet beschikbaar voor deze werkruimte.", "newKnowledge.unavailableTitle": "Nieuwe kennis is niet beschikbaar", "newKnowledge.updated": "Bijgewerkt op {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Tekens: —", "newKnowledge.uploadFiles": "Bestanden uploaden", "newKnowledge.uploadFilesDescription": "Voeg bestanden van je computer toe; ze worden geïndexeerd zodra je ze aanmaakt.", + "newKnowledge.uploadingFiles": "Uploaden…", "newKnowledge.usingDefaults": "Standaardinstellingen gebruikt", "newKnowledge.viewLabel": "Kennisweergave", "newKnowledge.websiteCrawl": "Websitecrawl", diff --git a/web/i18n/pl-PL/dataset.json b/web/i18n/pl-PL/dataset.json index c914a8a23f2..353348d56c5 100644 --- a/web/i18n/pl-PL/dataset.json +++ b/web/i18n/pl-PL/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Działania", "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.allSources": "Wszystkie źródła", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Anuluj", "newKnowledge.cardType": "Przestrzeń wiedzy", "newKnowledge.configureProvider": "Skonfiguruj {{provider}}", + "newKnowledge.connectNotion": "Połącz z Notion", "newKnowledge.connectProvider": "Połącz {{provider}}", + "newKnowledge.connectProviderGeneric": "Połącz dostawcę", "newKnowledge.connectSource": "Połącz źródło", "newKnowledge.connectSourceDescription": "Treści pozostają zsynchronizowane z Notion, witrynami internetowymi i innymi usługami.", "newKnowledge.connectingProvider": "Łączenie…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Robot nie mógł ukończyć tego podglądu. Sprawdź adres URL, dostosuj opcje skanowania lub spróbuj ponownie.", "newKnowledge.crawlFailedProvider": "{{provider}} zgłosił błąd. Sprawdź połączenie z dostawcą i spróbuj ponownie.", "newKnowledge.crawlFailedTimeout": "Przekroczono limit czasu skanowania. Użyj bardziej szczegółowego adresu URL lub zmniejsz maksymalną liczbę stron.", + "newKnowledge.crawlNotStarted": "Nie rozpoczęto", "newKnowledge.crawlOptions": "Opcje skanowania", + "newKnowledge.crawlPreview": "Podgląd indeksowania", "newKnowledge.crawlProgress": "Postęp skanowania {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Konfiguracja indeksowania witryny jest wyłączona do czasu udostępnienia przepływu pracy indeksowania.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl jest gotowy", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Skanowanie…", "newKnowledge.crawlingPages": "Skanowanie {{host}}… znaleziono {{count}} stron", "newKnowledge.createFailed": "Nie mogliśmy utworzyć tej bazy wiedzy. Sprawdź swój dostęp i spróbuj ponownie.", + "newKnowledge.createIllustrationTitle": "Połącz treści raz — każda aplikacja odpowie, korzystając z najnowszej wiedzy.", "newKnowledge.createTitle": "Twórz wiedzę", "newKnowledge.creators": "Twórcy", + "newKnowledge.customIntervalHours": "Niestandardowy interwał (godziny)", + "newKnowledge.customIntervalInvalid": "Wprowadź liczbę całkowitą od 1 do 720 godzin.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Odrzuć wersję roboczą", "newKnowledge.discardDraftDescription": "Zmiany w tej wersji roboczej bazy wiedzy zostaną utracone.", "newKnowledge.discardDraftTitle": "Odrzucić tę wersję roboczą bazy wiedzy?", + "newKnowledge.discardSourceChanges": "Odrzucić zmiany źródła?", + "newKnowledge.discardSourceChangesConfirm": "Odrzuć zmiany", + "newKnowledge.discardSourceChangesDescription": "Podgląd indeksowania, wybór stron i ustawienia synchronizacji zostaną utracone.", "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.documents": "Dokumenty", "newKnowledge.documentsUnavailable": "— dokumentów", + "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", "newKnowledge.errorDescription": "Nie udało się wczytać nowych baz wiedzy. Spróbuj ponownie.", "newKnowledge.errorTitle": "Coś poszło nie tak", "newKnowledge.filtersUnavailable": "Filtr będzie dostępny, gdy KnowledgeFS udostępni metadane.", + "newKnowledge.firecrawlUnavailable": "Firecrawl nie jest dostępny w katalogu dostawców dla tego obszaru roboczego.", "newKnowledge.gotIt": "Rozumiem", "newKnowledge.guideDescription": "Połącz źródło, a jego dokumenty pozostaną zsynchronizowane, automatycznie zindeksowane i gotowe do użycia, bez konieczności zarządzania konfiguracją. Dotychczasowe bazy wiedzy będą nadal działać bez zmian.", "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.invalidRootUrl": "Wprowadź prawidłowy adres URL http(s).", + "newKnowledge.keepEditing": "Kontynuuj edycję", "newKnowledge.lastSyncColumn": "Ostatnia synchronizacja", "newKnowledge.learnMore": "Dowiedz się więcej", "newKnowledge.leavePartialSetupConfirm": "Opuść konfigurację", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Opuścić nieukończoną konfigurację?", "newKnowledge.legacy": "Starsza wersja", "newKnowledge.loadMore": "Załaduj więcej", + "newKnowledge.loadingSyncPolicy": "Ładowanie zasad synchronizacji", "newKnowledge.maxPages": "Maksymalna liczba stron", - "newKnowledge.moreProviders": "Zobacz więcej", + "newKnowledge.moreProviders": "Więcej dostawców", "newKnowledge.name": "Nazwa wiedzy", "newKnowledge.namePlaceholder": "np. Podręcznik obsługi klienta", "newKnowledge.nameRequired": "Wprowadź nazwę bazy wiedzy.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Robot dotarł do witryny, ale nie znalazł stron możliwych do zindeksowania. Użyj innego adresu URL lub rozszerz opcje skanowania.", "newKnowledge.notFoundDescription": "Ta baza wiedzy mogła zostać usunięta lub możesz nie mieć do niej dostępu.", "newKnowledge.notFoundTitle": "Nie znaleziono bazy wiedzy", + "newKnowledge.notionNotConnected": "Notion nie jest połączony", + "newKnowledge.notionNotConnectedDescription": "Połącz z Notion, aby wybrać strony i utrzymywać ich synchronizację.", "newKnowledge.onlineDocuments": "Dokumenty online", "newKnowledge.onlineDrive": "Dysk online", "newKnowledge.pagesAppearDescription": "Przeskanuj witrynę, aby sprawdzić, które strony zostaną dodane.", "newKnowledge.pagesAppearTitle": "Strony pojawią się tutaj", "newKnowledge.pagesCrawled": "Przeskanowano {{count}} stron w {{host}}", + "newKnowledge.pagesCrawled_one": "Przeskanowano {{count}} stron w {{host}}", + "newKnowledge.pagesCrawled_other": "Przeskanowano {{count}} stron w {{host}}", + "newKnowledge.pagesSelected": "Wybrano: {{count}}", "newKnowledge.permission": "Pozwolenie", "newKnowledge.permissionAllMembers": "Obszar roboczy · wszyscy członkowie mogą przeglądać i edytować", "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.providerConnected": "{{provider}} podłączony", "newKnowledge.providerLabel": "Wybierz dostawcę", "newKnowledge.providerLoadFailed": "Dostawcy nie mogli załadować", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Usuń źródło", "newKnowledge.retryCrawl": "Ponów skanowanie", "newKnowledge.retryProviderLoad": "Spróbuj ponownie", + "newKnowledge.retrySyncPolicy": "Ponów ładowanie zasad synchronizacji", "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.searchSources": "Wyszukaj źródła", + "newKnowledge.selectAll": "Zaznacz wszystko", + "newKnowledge.selectAllSources": "Wybierz wszystkie źródła", + "newKnowledge.selectFilesAndFolders": "Wybierz pliki i foldery do synchronizacji", + "newKnowledge.skippedFailed": "Pominięto (błąd)", + "newKnowledge.skippedOffDomain": "Pominięto (poza domeną)", "newKnowledge.sourceActions": "Działania dla {{name}}", "newKnowledge.sourceColumn": "Źródło", "newKnowledge.sourceFilterLabel": "Filtruj źródła według stanu", "newKnowledge.sourceName": "Nazwa źródła", "newKnowledge.sourceNamePlaceholder": "np. skanowanie dokumentacji produktu", "newKnowledge.sourceNameRequired": "Wprowadź nazwę źródła.", + "newKnowledge.sourceSetupBackendDependency": "Konfiguracja źródła jest pokazana w całości; edycja i podgląd wymagają przepływu backendowego źródła roboczego.", "newKnowledge.sourceStatus.active": "Aktywny", "newKnowledge.sourceStatus.disabled": "Wyłączone", "newKnowledge.sourceStatus.error": "Błąd", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Zatrzymaj", "newKnowledge.stoppingCrawl": "Zatrzymywanie…", "newKnowledge.syncNow": "Synchronizuj teraz", + "newKnowledge.syncPolicy": "Zasady synchronizacji", "newKnowledge.syncPolicyColumn": "Polityka synchronizacji", + "newKnowledge.syncPolicyCustom": "Niestandardowy interwał", + "newKnowledge.syncPolicyDaily": "Co 24 godziny", + "newKnowledge.syncPolicyLoadFailed": "Nie udało się załadować zasad synchronizacji.", + "newKnowledge.syncPolicyManual": "Tylko ręcznie", + "newKnowledge.syncPolicyProvider": "Użyj harmonogramu dostawcy", "newKnowledge.tags": "Tagi", "newKnowledge.unavailableDescription": "KnowledgeFS nie jest obecnie dostępny dla tego obszaru roboczego.", "newKnowledge.unavailableTitle": "Nowa wiedza jest niedostępna", "newKnowledge.updated": "Zaktualizowano {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Znaki: —", "newKnowledge.uploadFiles": "Prześlij pliki", "newKnowledge.uploadFilesDescription": "Dodaj pliki z komputera — zostaną zindeksowane od razu po utworzeniu.", + "newKnowledge.uploadingFiles": "Przesyłanie…", "newKnowledge.usingDefaults": "Używane są ustawienia domyślne", "newKnowledge.viewLabel": "Widok wiedzy", "newKnowledge.websiteCrawl": "Indeksowanie witryny", diff --git a/web/i18n/pt-BR/dataset.json b/web/i18n/pt-BR/dataset.json index 7615a118188..2dc97abbd11 100644 --- a/web/i18n/pt-BR/dataset.json +++ b/web/i18n/pt-BR/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Ações", "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.allSources": "Todas as fontes", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Cancelar", "newKnowledge.cardType": "Espaço de conhecimento", "newKnowledge.configureProvider": "Configurar {{provider}}", + "newKnowledge.connectNotion": "Conectar o Notion", "newKnowledge.connectProvider": "Conectar {{provider}}", + "newKnowledge.connectProviderGeneric": "Conectar provedor", "newKnowledge.connectSource": "Conectar uma fonte", "newKnowledge.connectSourceDescription": "O conteúdo permanece sincronizado com Notion, sites e outros serviços.", "newKnowledge.connectingProvider": "Conectando…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "O rastreador não conseguiu concluir esta visualização. Verifique a URL, ajuste as opções de rastreamento ou tente novamente.", "newKnowledge.crawlFailedProvider": "O {{provider}} informou um erro. Verifique a conexão do provedor e tente novamente.", "newKnowledge.crawlFailedTimeout": "O tempo limite do rastreamento expirou. Tente uma URL mais específica ou reduza o número máximo de páginas.", + "newKnowledge.crawlNotStarted": "Não iniciado", "newKnowledge.crawlOptions": "Opções de rastreamento", + "newKnowledge.crawlPreview": "Prévia do rastreamento", "newKnowledge.crawlProgress": "Progresso do rastreamento de {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "A configuração de rastreamento de sites será desativada até que o fluxo de trabalho de rastreamento esteja disponível.", "newKnowledge.crawlSetupUnavailableTitle": "O Firecrawl está pronto", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Rastreando…", "newKnowledge.crawlingPages": "Rastreando {{host}}… {{count}} páginas encontradas", "newKnowledge.createFailed": "Não foi possível criar esta base de conhecimento. Verifique seu acesso e tente novamente.", + "newKnowledge.createIllustrationTitle": "Conecte seu conteúdo uma vez — cada aplicativo responde com o conhecimento mais atualizado.", "newKnowledge.createTitle": "Crie Conhecimento", "newKnowledge.creators": "Criadores", + "newKnowledge.customIntervalHours": "Intervalo personalizado (horas)", + "newKnowledge.customIntervalInvalid": "Digite um número inteiro de 1 a 720 horas.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Descartar rascunho", "newKnowledge.discardDraftDescription": "Suas alterações neste rascunho da base de conhecimento serão perdidas.", "newKnowledge.discardDraftTitle": "Descartar este rascunho da base de conhecimento?", + "newKnowledge.discardSourceChanges": "Descartar alterações da fonte?", + "newKnowledge.discardSourceChangesConfirm": "Descartar alterações", + "newKnowledge.discardSourceChangesDescription": "A prévia do rastreamento, a seleção de páginas e as configurações de sincronização serão perdidas.", "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.documents": "Documentos", "newKnowledge.documentsUnavailable": "— documentos", + "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", "newKnowledge.errorDescription": "Não foi possível carregar suas novas bases de conhecimento. Tente novamente.", "newKnowledge.errorTitle": "Algo deu errado", "newKnowledge.filtersUnavailable": "Este filtro ficará disponível quando o KnowledgeFS expuser seus metadados.", + "newKnowledge.firecrawlUnavailable": "O Firecrawl não está disponível no catálogo de provedores deste espaço de trabalho.", "newKnowledge.gotIt": "Entendi", "newKnowledge.guideDescription": "Conecte uma fonte e os documentos permanecerão sincronizados, indexados automaticamente e prontos para uso, sem nada para gerenciar. Suas bases de conhecimento anteriores continuarão funcionando normalmente.", "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.invalidRootUrl": "Insira uma URL http(s) válida.", + "newKnowledge.keepEditing": "Continuar editando", "newKnowledge.lastSyncColumn": "Última sincronização", "newKnowledge.learnMore": "Saiba mais", "newKnowledge.leavePartialSetupConfirm": "Sair da configuração", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Sair da configuração incompleta?", "newKnowledge.legacy": "Legado", "newKnowledge.loadMore": "Carregar mais", + "newKnowledge.loadingSyncPolicy": "Carregando política de sincronização", "newKnowledge.maxPages": "Máximo de páginas", - "newKnowledge.moreProviders": "Ver mais", + "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.", @@ -217,16 +232,23 @@ "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.", "newKnowledge.notFoundDescription": "Esta base de conhecimento pode ter sido excluída ou você pode não ter acesso.", "newKnowledge.notFoundTitle": "Base de conhecimento não encontrada", + "newKnowledge.notionNotConnected": "O Notion não está conectado", + "newKnowledge.notionNotConnectedDescription": "Conecte o Notion para escolher páginas e mantê-las sincronizadas.", "newKnowledge.onlineDocuments": "Documentos on-line", "newKnowledge.onlineDrive": "Unidade on-line", "newKnowledge.pagesAppearDescription": "Rastreie o site para revisar quais páginas serão adicionadas.", "newKnowledge.pagesAppearTitle": "As páginas aparecerão aqui", "newKnowledge.pagesCrawled": "{{count}} páginas rastreadas em {{host}}", + "newKnowledge.pagesCrawled_one": "{{count}} páginas rastreadas em {{host}}", + "newKnowledge.pagesCrawled_other": "{{count}} páginas rastreadas em {{host}}", + "newKnowledge.pagesSelected": "{{count}} selecionado(s)", "newKnowledge.permission": "Permissão", "newKnowledge.permissionAllMembers": "Espaço de trabalho · todos os membros podem visualizar e editar", "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.providerConnected": "{{provider}} conectado", "newKnowledge.providerLabel": "Selecione um provedor", "newKnowledge.providerLoadFailed": "Os provedores não conseguiram carregar", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Remover fonte", "newKnowledge.retryCrawl": "Tentar rastreamento novamente", "newKnowledge.retryProviderLoad": "Tentar novamente", + "newKnowledge.retrySyncPolicy": "Tentar novamente a política de sincronização", "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.searchSources": "Fontes de pesquisa", + "newKnowledge.selectAll": "Selecionar tudo", + "newKnowledge.selectAllSources": "Selecionar todas as origens", + "newKnowledge.selectFilesAndFolders": "Selecione arquivos e pastas para sincronizar", + "newKnowledge.skippedFailed": "Ignorado (falha)", + "newKnowledge.skippedOffDomain": "Ignorado (fora do domínio)", "newKnowledge.sourceActions": "Ações para {{name}}", "newKnowledge.sourceColumn": "Fonte", "newKnowledge.sourceFilterLabel": "Filtrar fontes por status", "newKnowledge.sourceName": "Nome da fonte", "newKnowledge.sourceNamePlaceholder": "ex.: rastreamento da documentação do produto", "newKnowledge.sourceNameRequired": "Insira um nome para a fonte.", + "newKnowledge.sourceSetupBackendDependency": "A configuração da origem é exibida por completo; a edição e a prévia exigem o fluxo de backend da origem em rascunho.", "newKnowledge.sourceStatus.active": "Ativo", "newKnowledge.sourceStatus.disabled": "Desativado", "newKnowledge.sourceStatus.error": "Erro", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Parar", "newKnowledge.stoppingCrawl": "Parando…", "newKnowledge.syncNow": "Sincronize agora", + "newKnowledge.syncPolicy": "Política de sincronização", "newKnowledge.syncPolicyColumn": "Política de sincronização", + "newKnowledge.syncPolicyCustom": "Intervalo personalizado", + "newKnowledge.syncPolicyDaily": "A cada 24 horas", + "newKnowledge.syncPolicyLoadFailed": "Não foi possível carregar a política de sincronização.", + "newKnowledge.syncPolicyManual": "Somente manual", + "newKnowledge.syncPolicyProvider": "Usar agenda do provedor", "newKnowledge.tags": "Tags", "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.uploadCharactersUnavailable": "Caracteres: —", "newKnowledge.uploadFiles": "Enviar arquivos", "newKnowledge.uploadFilesDescription": "Adicione arquivos do seu computador; eles são indexados assim que você os cria.", + "newKnowledge.uploadingFiles": "Enviando…", "newKnowledge.usingDefaults": "Usando valores padrão", "newKnowledge.viewLabel": "Visualização de conhecimento", "newKnowledge.websiteCrawl": "Rastreamento de site", diff --git a/web/i18n/ro-RO/dataset.json b/web/i18n/ro-RO/dataset.json index a3e79d4a1d9..e7a915ca3c2 100644 --- a/web/i18n/ro-RO/dataset.json +++ b/web/i18n/ro-RO/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Acțiuni", "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.allSources": "Toate sursele", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Anula", "newKnowledge.cardType": "Spațiu de cunoștințe", "newKnowledge.configureProvider": "Configurați {{provider}}", + "newKnowledge.connectNotion": "Conectează Notion", "newKnowledge.connectProvider": "Conectați {{provider}}", + "newKnowledge.connectProviderGeneric": "Conectează furnizorul", "newKnowledge.connectSource": "Conectează o sursă", "newKnowledge.connectSourceDescription": "Conținutul rămâne sincronizat cu Notion, site-uri web și alte servicii.", "newKnowledge.connectingProvider": "Se conectează...", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Crawlerul nu a putut finaliza această previzualizare. Verifică adresa URL, ajustează opțiunile de explorare sau încearcă din nou.", "newKnowledge.crawlFailedProvider": "{{provider}} a raportat o eroare. Verifică conexiunea furnizorului și încearcă din nou.", "newKnowledge.crawlFailedTimeout": "Explorarea a depășit timpul limită. Încearcă o adresă URL mai specifică sau redu numărul maxim de pagini.", + "newKnowledge.crawlNotStarted": "Nepornit", "newKnowledge.crawlOptions": "Opțiuni de explorare", + "newKnowledge.crawlPreview": "Previzualizare accesare", "newKnowledge.crawlProgress": "Progresul explorării pentru {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Configurarea accesării cu crawlere a site-ului este dezactivată până când fluxul de lucru cu crawlere este disponibil.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl este gata", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Se explorează…", "newKnowledge.crawlingPages": "Se explorează {{host}}… {{count}} pagini găsite", "newKnowledge.createFailed": "Nu am putut crea această bază de cunoștințe. Verificați accesul și încercați din nou.", + "newKnowledge.createIllustrationTitle": "Conectează conținutul o singură dată — fiecare aplicație răspunde cu cele mai noi cunoștințe.", "newKnowledge.createTitle": "Creați cunoștințe", "newKnowledge.creators": "Creatori", + "newKnowledge.customIntervalHours": "Interval personalizat (ore)", + "newKnowledge.customIntervalInvalid": "Introdu un număr întreg între 1 și 720 de ore.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Renunță la schiță", "newKnowledge.discardDraftDescription": "Modificările aduse acestei schițe a bazei de cunoștințe se vor pierde.", "newKnowledge.discardDraftTitle": "Renunțați la această schiță a bazei de cunoștințe?", + "newKnowledge.discardSourceChanges": "Renunți la modificările sursei?", + "newKnowledge.discardSourceChangesConfirm": "Renunță la modificări", + "newKnowledge.discardSourceChangesDescription": "Previzualizarea accesării cu crawlere, selectarea paginilor și setările de sincronizare se vor pierde.", "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.documents": "Documente", "newKnowledge.documentsUnavailable": "— documente", + "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", "newKnowledge.errorDescription": "Nu am putut încărca noile baze de cunoștințe. Încearcă din nou.", "newKnowledge.errorTitle": "Ceva nu a funcționat", "newKnowledge.filtersUnavailable": "Acest filtru va fi disponibil când KnowledgeFS va furniza metadatele.", + "newKnowledge.firecrawlUnavailable": "Firecrawl nu este disponibil în catalogul furnizorilor pentru acest spațiu de lucru.", "newKnowledge.gotIt": "Am înțeles", "newKnowledge.guideDescription": "Conectează o sursă, iar documentele ei rămân sincronizate, indexate automat și gata de utilizare, fără nimic de administrat. Bazele de cunoștințe existente continuă să funcționeze ca înainte.", "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.invalidRootUrl": "Introdu o adresă URL http(s) validă.", + "newKnowledge.keepEditing": "Continuă editarea", "newKnowledge.lastSyncColumn": "Ultima sincronizare", "newKnowledge.learnMore": "Află mai multe", "newKnowledge.leavePartialSetupConfirm": "Ieși din configurare", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Ieșiți din configurarea incompletă?", "newKnowledge.legacy": "Versiune veche", "newKnowledge.loadMore": "Încarcă mai multe", + "newKnowledge.loadingSyncPolicy": "Se încarcă politica de sincronizare", "newKnowledge.maxPages": "Număr maxim de pagini", - "newKnowledge.moreProviders": "Vezi mai mult", + "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.", @@ -217,16 +232,23 @@ "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.", "newKnowledge.notFoundDescription": "Este posibil ca această bază de cunoștințe să fi fost ștearsă sau este posibil să nu aveți acces.", "newKnowledge.notFoundTitle": "Baza de cunoștințe nu a fost găsită", + "newKnowledge.notionNotConnected": "Notion nu este conectat", + "newKnowledge.notionNotConnectedDescription": "Conectează Notion pentru a alege pagini și a le menține sincronizate.", "newKnowledge.onlineDocuments": "Documente online", "newKnowledge.onlineDrive": "Conducere online", "newKnowledge.pagesAppearDescription": "Explorează site-ul pentru a verifica paginile care vor fi adăugate.", "newKnowledge.pagesAppearTitle": "Paginile vor apărea aici", "newKnowledge.pagesCrawled": "{{count}} pagini explorate la {{host}}", + "newKnowledge.pagesCrawled_one": "{{count}} pagini explorate la {{host}}", + "newKnowledge.pagesCrawled_other": "{{count}} pagini explorate la {{host}}", + "newKnowledge.pagesSelected": "{{count}} selectate", "newKnowledge.permission": "Permisiune", "newKnowledge.permissionAllMembers": "Spațiu de lucru · toți membrii pot vizualiza și edita", "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.providerConnected": "{{provider}} conectat", "newKnowledge.providerLabel": "Selectați un furnizor", "newKnowledge.providerLoadFailed": "Furnizorii nu s-au putut încărca", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Eliminați sursa", "newKnowledge.retryCrawl": "Reîncearcă explorarea", "newKnowledge.retryProviderLoad": "Reîncercați", + "newKnowledge.retrySyncPolicy": "Reîncearcă politica de sincronizare", "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.searchSources": "Căutați surse", + "newKnowledge.selectAll": "Selectează tot", + "newKnowledge.selectAllSources": "Selectează toate sursele", + "newKnowledge.selectFilesAndFolders": "Selectează fișierele și folderele de sincronizat", + "newKnowledge.skippedFailed": "Omis (eșuat)", + "newKnowledge.skippedOffDomain": "Omis (în afara domeniului)", "newKnowledge.sourceActions": "Acțiuni pentru {{name}}", "newKnowledge.sourceColumn": "Sursă", "newKnowledge.sourceFilterLabel": "Filtrați sursele după stare", "newKnowledge.sourceName": "Numele sursei", "newKnowledge.sourceNamePlaceholder": "de ex., explorarea documentației produsului", "newKnowledge.sourceNameRequired": "Introdu un nume pentru sursă.", + "newKnowledge.sourceSetupBackendDependency": "Configurarea sursei este afișată integral; editarea și previzualizarea necesită fluxul backend pentru sursa ciornă.", "newKnowledge.sourceStatus.active": "Activ", "newKnowledge.sourceStatus.disabled": "Dezactivat", "newKnowledge.sourceStatus.error": "Eroare", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Oprește", "newKnowledge.stoppingCrawl": "Se oprește…", "newKnowledge.syncNow": "Sincronizează acum", + "newKnowledge.syncPolicy": "Politică de sincronizare", "newKnowledge.syncPolicyColumn": "Politica de sincronizare", + "newKnowledge.syncPolicyCustom": "Interval personalizat", + "newKnowledge.syncPolicyDaily": "La fiecare 24 de ore", + "newKnowledge.syncPolicyLoadFailed": "Politica de sincronizare nu a putut fi încărcată.", + "newKnowledge.syncPolicyManual": "Doar manual", + "newKnowledge.syncPolicyProvider": "Folosește programul furnizorului", "newKnowledge.tags": "Etichete", "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.uploadCharactersUnavailable": "Caractere: —", "newKnowledge.uploadFiles": "Încarcă fișiere", "newKnowledge.uploadFilesDescription": "Adaugă fișiere de pe computer — sunt indexate imediat ce le creezi.", + "newKnowledge.uploadingFiles": "Se încarcă…", "newKnowledge.usingDefaults": "Se folosesc valorile implicite", "newKnowledge.viewLabel": "Vizualizare cunoștințe", "newKnowledge.websiteCrawl": "Accesare cu crawlere a site-ului", diff --git a/web/i18n/ru-RU/dataset.json b/web/i18n/ru-RU/dataset.json index 053de91b664..14e0edf85fc 100644 --- a/web/i18n/ru-RU/dataset.json +++ b/web/i18n/ru-RU/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Действия", "newKnowledge.addSource": "Добавить источник", "newKnowledge.addSourceDescription": "Подключите источник к этой базе знаний — она синхронизируется автоматически и остается актуальной.", + "newKnowledge.addSourceFailed": "Не удалось добавить этот источник. Проверьте настройки и повторите попытку.", "newKnowledge.addSourceRequiresSelection": "Перед добавлением источника выберите хотя бы одну просканированную страницу.", "newKnowledge.adjustAndRecrawl": "Настроить и просканировать снова", "newKnowledge.allSources": "Все источники", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Отмена", "newKnowledge.cardType": "Пространство знаний", "newKnowledge.configureProvider": "Настроить {{provider}}", + "newKnowledge.connectNotion": "Подключить Notion", "newKnowledge.connectProvider": "Подключите {{provider}}", + "newKnowledge.connectProviderGeneric": "Подключить провайдера", "newKnowledge.connectSource": "Подключить источник", "newKnowledge.connectSourceDescription": "Контент остаётся синхронизированным с Notion, веб-сайтами и другими сервисами.", "newKnowledge.connectingProvider": "Подключение…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Не удалось завершить предварительное сканирование. Проверьте URL, измените параметры сканирования или повторите попытку.", "newKnowledge.crawlFailedProvider": "{{provider}} сообщил об ошибке. Проверьте подключение к провайдеру и повторите попытку.", "newKnowledge.crawlFailedTimeout": "Истекло время ожидания сканирования. Укажите более точный URL или уменьшите максимальное число страниц.", + "newKnowledge.crawlNotStarted": "Не запущено", "newKnowledge.crawlOptions": "Параметры сканирования", + "newKnowledge.crawlPreview": "Предпросмотр обхода", "newKnowledge.crawlProgress": "Ход сканирования {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Конфигурация сканирования веб-сайта отключена до тех пор, пока не будет доступен рабочий процесс сканирования.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl готов", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Сканирование…", "newKnowledge.crawlingPages": "Сканирование {{host}}… найдено страниц: {{count}}", "newKnowledge.createFailed": "Мы не смогли создать эту базу знаний. Проверьте доступ и повторите попытку.", + "newKnowledge.createIllustrationTitle": "Подключите контент один раз — каждое приложение будет отвечать на основе самых свежих знаний.", "newKnowledge.createTitle": "Создавайте знания", "newKnowledge.creators": "Создатели", + "newKnowledge.customIntervalHours": "Пользовательский интервал (часы)", + "newKnowledge.customIntervalInvalid": "Введите целое число от 1 до 720 часов.", "newKnowledge.description": "Описание", "newKnowledge.descriptionHelp": "Четкое описание помогает приложениям решить, когда следует искать эти знания.", "newKnowledge.descriptionPlaceholder": "например Политика возврата, часто задаваемые вопросы о доставке и шаги эскалации — используйте для вопросов поддержки клиентов.", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Удалить черновик", "newKnowledge.discardDraftDescription": "Изменения в этом черновике базы знаний будут потеряны.", "newKnowledge.discardDraftTitle": "Удалить этот черновик базы знаний?", + "newKnowledge.discardSourceChanges": "Отменить изменения источника?", + "newKnowledge.discardSourceChangesConfirm": "Отменить изменения", + "newKnowledge.discardSourceChangesDescription": "Предпросмотр сканирования, выбор страниц и настройки синхронизации будут потеряны.", "newKnowledge.discardSourceDraftDescription": "Конфигурация источника и предварительное сканирование будут удалены.", "newKnowledge.discardSourceDraftFailed": "Не удалось удалить черновик источника. Повторите попытку.", "newKnowledge.discardSourceDraftTitle": "Удалить этот черновик источника?", "newKnowledge.documents": "Документы", "newKnowledge.documentsUnavailable": "— документов", + "newKnowledge.editSource": "Править", "newKnowledge.emptyDescription": "Добавьте документы, и агенты смогут читать их напрямую — без настройки индексации.", "newKnowledge.emptyTitle": "Более умный способ создавать базы знаний", "newKnowledge.errorDescription": "Не удалось загрузить новые базы знаний. Повторите попытку.", "newKnowledge.errorTitle": "Что-то пошло не так", "newKnowledge.filtersUnavailable": "Фильтр станет доступен, когда KnowledgeFS предоставит метаданные.", + "newKnowledge.firecrawlUnavailable": "Firecrawl недоступен в каталоге поставщиков для этой рабочей области.", "newKnowledge.gotIt": "Понятно", "newKnowledge.guideDescription": "Подключите источник, и его документы будут синхронизироваться, автоматически индексироваться и сразу станут готовы к работе — без дополнительной настройки. Ваши прежние базы знаний продолжат работать как раньше.", "newKnowledge.guideTitle": "Познакомьтесь с новой базой знаний ✨", "newKnowledge.illustrationHeadline": "Подключите контент один раз — и каждое приложение будет отвечать на основе самых свежих знаний.", "newKnowledge.includeSubpages": "Включать вложенные страницы", "newKnowledge.invalidRootUrl": "Введите корректный URL с http(s).", + "newKnowledge.keepEditing": "Продолжить редактирование", "newKnowledge.lastSyncColumn": "Последняя синхронизация", "newKnowledge.learnMore": "Подробнее", "newKnowledge.leavePartialSetupConfirm": "Выйти из настройки", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Выйти из незавершённой настройки?", "newKnowledge.legacy": "Предыдущая версия", "newKnowledge.loadMore": "Загрузить ещё", + "newKnowledge.loadingSyncPolicy": "Загрузка политики синхронизации", "newKnowledge.maxPages": "Максимум страниц", - "newKnowledge.moreProviders": "Подробнее", + "newKnowledge.moreProviders": "Другие провайдеры", "newKnowledge.name": "Название знания", "newKnowledge.namePlaceholder": "например Руководство по поддержке клиентов", "newKnowledge.nameRequired": "Введите название базы знаний.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Робот получил доступ к сайту, но не нашёл страниц для индексирования. Попробуйте другой URL или расширьте параметры сканирования.", "newKnowledge.notFoundDescription": "Возможно, эта база знаний была удалена, или у вас нет доступа.", "newKnowledge.notFoundTitle": "База знаний не найдена", + "newKnowledge.notionNotConnected": "Notion не подключён", + "newKnowledge.notionNotConnectedDescription": "Подключите Notion, чтобы выбирать страницы и поддерживать их синхронизацию.", "newKnowledge.onlineDocuments": "Онлайн-документы", "newKnowledge.onlineDrive": "Онлайн-диск", "newKnowledge.pagesAppearDescription": "Просканируйте сайт, чтобы проверить, какие страницы будут добавлены.", "newKnowledge.pagesAppearTitle": "Здесь появятся страницы", "newKnowledge.pagesCrawled": "Просканировано страниц на {{host}}: {{count}}", + "newKnowledge.pagesCrawled_one": "Просканировано страниц на {{host}}: {{count}}", + "newKnowledge.pagesCrawled_other": "Просканировано страниц на {{host}}: {{count}}", + "newKnowledge.pagesSelected": "Выбрано: {{count}}", "newKnowledge.permission": "Разрешение", "newKnowledge.permissionAllMembers": "Рабочая область · все участники могут просматривать и редактировать", "newKnowledge.permissionOnlyMe": "Только я", "newKnowledge.permissionRestricted": "Разрешения вашей рабочей области не позволяют изменить этот параметр.", "newKnowledge.permissionUpdateFailed": "База знаний создана, но подтвердить её разрешения не удалось. Повторите попытку, чтобы завершить настройку.", + "newKnowledge.preview": "Предпросмотр", + "newKnowledge.previewUnavailable": "Предпросмотр станет доступен после завершения обработки файла сервером.", "newKnowledge.providerConnected": "{{provider}} подключен", "newKnowledge.providerLabel": "Выберите провайдера", "newKnowledge.providerLoadFailed": "Поставщикам не удалось загрузить", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Удалить источник", "newKnowledge.retryCrawl": "Повторить сканирование", "newKnowledge.retryProviderLoad": "Повторить попытку", + "newKnowledge.retrySyncPolicy": "Повторить загрузку политики синхронизации", "newKnowledge.rootUrl": "Корневой URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Этот раздел станет доступен на следующем этапе настройки.", "newKnowledge.searchSources": "Поиск источников", + "newKnowledge.selectAll": "Выбрать все", + "newKnowledge.selectAllSources": "Выбрать все источники", + "newKnowledge.selectFilesAndFolders": "Выберите файлы и папки для синхронизации", + "newKnowledge.skippedFailed": "Пропущено (ошибка)", + "newKnowledge.skippedOffDomain": "Пропущено (вне домена)", "newKnowledge.sourceActions": "Действия для {{name}}", "newKnowledge.sourceColumn": "Источник", "newKnowledge.sourceFilterLabel": "Фильтровать источники по статусу", "newKnowledge.sourceName": "Название источника", "newKnowledge.sourceNamePlaceholder": "например, сканирование документации продукта", "newKnowledge.sourceNameRequired": "Введите название источника.", + "newKnowledge.sourceSetupBackendDependency": "Настройка источника показана полностью; для редактирования и предпросмотра требуется серверный процесс черновика источника.", "newKnowledge.sourceStatus.active": "Активен", "newKnowledge.sourceStatus.disabled": "Отключено", "newKnowledge.sourceStatus.error": "Ошибка", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Остановить", "newKnowledge.stoppingCrawl": "Остановка…", "newKnowledge.syncNow": "Синхронизировать сейчас", + "newKnowledge.syncPolicy": "Политика синхронизации", "newKnowledge.syncPolicyColumn": "Политика синхронизации", + "newKnowledge.syncPolicyCustom": "Пользовательский интервал", + "newKnowledge.syncPolicyDaily": "Каждые 24 часа", + "newKnowledge.syncPolicyLoadFailed": "Не удалось загрузить политику синхронизации.", + "newKnowledge.syncPolicyManual": "Только вручную", + "newKnowledge.syncPolicyProvider": "Использовать расписание поставщика", "newKnowledge.tags": "Теги", "newKnowledge.unavailableDescription": "KnowledgeFS сейчас недоступен для этого рабочего пространства.", "newKnowledge.unavailableTitle": "Новая база знаний недоступна", "newKnowledge.updated": "Обновлено {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Символы: —", "newKnowledge.uploadFiles": "Загрузить файлы", "newKnowledge.uploadFilesDescription": "Добавьте файлы с компьютера — они будут проиндексированы сразу после создания.", + "newKnowledge.uploadingFiles": "Загрузка…", "newKnowledge.usingDefaults": "Используются настройки по умолчанию", "newKnowledge.viewLabel": "Вид базы знаний", "newKnowledge.websiteCrawl": "Сканирование веб-сайта", diff --git a/web/i18n/sl-SI/dataset.json b/web/i18n/sl-SI/dataset.json index c22ba6237e5..2df760fc004 100644 --- a/web/i18n/sl-SI/dataset.json +++ b/web/i18n/sl-SI/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Dejanja", "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.allSources": "Vsi viri", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Prekliči", "newKnowledge.cardType": "Prostor znanja", "newKnowledge.configureProvider": "Konfigurirajte {{provider}}", + "newKnowledge.connectNotion": "Poveži Notion", "newKnowledge.connectProvider": "Povežite {{provider}}", + "newKnowledge.connectProviderGeneric": "Poveži ponudnika", "newKnowledge.connectSource": "Poveži vir", "newKnowledge.connectSourceDescription": "Vsebina ostaja sinhronizirana z Notionom, spletnimi mesti in drugimi storitvami.", "newKnowledge.connectingProvider": "Povezovanje ...", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Pajek ni mogel dokončati tega predogleda. Preverite URL, prilagodite možnosti preiskovanja ali poskusite znova.", "newKnowledge.crawlFailedProvider": "{{provider}} je sporočil napako. Preverite povezavo s ponudnikom in poskusite znova.", "newKnowledge.crawlFailedTimeout": "Čas za preiskovanje je potekel. Poskusite z natančnejšim URL-jem ali zmanjšajte največje število strani.", + "newKnowledge.crawlNotStarted": "Ni začeto", "newKnowledge.crawlOptions": "Možnosti preiskovanja", + "newKnowledge.crawlPreview": "Predogled iskanja", "newKnowledge.crawlProgress": "Napredek preiskovanja za {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Konfiguracija iskanja po vsebini spletnega mesta je onemogočena, dokler potek dela po vsebini ni na voljo.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl je pripravljen", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Preiskovanje…", "newKnowledge.crawlingPages": "Preiskovanje {{host}}… najdenih strani: {{count}}", "newKnowledge.createFailed": "Te baze znanja nismo mogli ustvariti. Preverite dostop in poskusite znova.", + "newKnowledge.createIllustrationTitle": "Vsebino povežite enkrat — vsaka aplikacija odgovarja z najnovejšim znanjem.", "newKnowledge.createTitle": "Ustvarite znanje", "newKnowledge.creators": "Ustvarjalci", + "newKnowledge.customIntervalHours": "Interval po meri (ure)", + "newKnowledge.customIntervalInvalid": "Vnesite celo število od 1 do 720 ur.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Zavrzi osnutek", "newKnowledge.discardDraftDescription": "Spremembe tega osnutka zbirke znanja bodo izgubljene.", "newKnowledge.discardDraftTitle": "Želite zavreči ta osnutek zbirke znanja?", + "newKnowledge.discardSourceChanges": "Zavreči spremembe vira?", + "newKnowledge.discardSourceChangesConfirm": "Zavrzi spremembe", + "newKnowledge.discardSourceChangesDescription": "Predogled iskanja po vsebini, izbira strani in nastavitve sinhronizacije bodo izgubljeni.", "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.documents": "Dokumenti", "newKnowledge.documentsUnavailable": "— dokumentov", + "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", "newKnowledge.errorDescription": "Novih baz znanja ni bilo mogoče naložiti. Poskusite znova.", "newKnowledge.errorTitle": "Prišlo je do napake", "newKnowledge.filtersUnavailable": "Ta filter bo na voljo, ko bo KnowledgeFS razkril metapodatke.", + "newKnowledge.firecrawlUnavailable": "Firecrawl ni na voljo v katalogu ponudnika za ta delovni prostor.", "newKnowledge.gotIt": "Razumem", "newKnowledge.guideDescription": "Povežite vir in njegovi dokumenti bodo ostali sinhronizirani, samodejno indeksirani in pripravljeni za uporabo brez dodatnega upravljanja. Vaše obstoječe baze znanja bodo še naprej delovale kot doslej.", "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.invalidRootUrl": "Vnesite veljaven URL http(s).", + "newKnowledge.keepEditing": "Nadaljuj urejanje", "newKnowledge.lastSyncColumn": "Zadnja sinhronizacija", "newKnowledge.learnMore": "Več informacij", "newKnowledge.leavePartialSetupConfirm": "Zapusti nastavitev", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Želite zapustiti nedokončano nastavitev?", "newKnowledge.legacy": "Staro", "newKnowledge.loadMore": "Naloži več", + "newKnowledge.loadingSyncPolicy": "Nalaganje pravilnika sinhronizacije", "newKnowledge.maxPages": "Največje število strani", - "newKnowledge.moreProviders": "Oglejte si več", + "newKnowledge.moreProviders": "Več ponudnikov", "newKnowledge.name": "Ime znanja", "newKnowledge.namePlaceholder": "npr. Priročnik za podporo strankam", "newKnowledge.nameRequired": "Vnesite ime baze znanja.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Pajek je dosegel spletno mesto, vendar ni našel strani za indeksiranje. Poskusite drug URL ali razširite možnosti preiskovanja.", "newKnowledge.notFoundDescription": "Ta baza znanja je bila morda izbrisana ali pa nimate dostopa.", "newKnowledge.notFoundTitle": "Baza znanja ni bila najdena", + "newKnowledge.notionNotConnected": "Notion ni povezan", + "newKnowledge.notionNotConnectedDescription": "Povežite Notion, da izberete strani in jih ohranite sinhronizirane.", "newKnowledge.onlineDocuments": "Spletni dokumenti", "newKnowledge.onlineDrive": "Spletna vožnja", "newKnowledge.pagesAppearDescription": "Preiščite spletno mesto in preverite, katere strani bodo dodane.", "newKnowledge.pagesAppearTitle": "Strani bodo prikazane tukaj", "newKnowledge.pagesCrawled": "Preiskanih strani na {{host}}: {{count}}", + "newKnowledge.pagesCrawled_one": "Preiskanih strani na {{host}}: {{count}}", + "newKnowledge.pagesCrawled_other": "Preiskanih strani na {{host}}: {{count}}", + "newKnowledge.pagesSelected": "Izbrano: {{count}}", "newKnowledge.permission": "Dovoljenje", "newKnowledge.permissionAllMembers": "Delovni prostor · vsi člani si lahko ogledujejo in urejajo", "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.providerConnected": "{{provider}} je povezan", "newKnowledge.providerLabel": "Izberi ponudnika", "newKnowledge.providerLoadFailed": "Ponudnikov ni bilo mogoče naložiti", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Odstrani vir", "newKnowledge.retryCrawl": "Ponovi preiskovanje", "newKnowledge.retryProviderLoad": "Poskusi znova", + "newKnowledge.retrySyncPolicy": "Znova poskusi naložiti pravilnik sinhronizacije", "newKnowledge.rootUrl": "Korenski URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Ta razdelek bo na voljo v naslednjem koraku nastavitve.", "newKnowledge.searchSources": "Iskanje virov", + "newKnowledge.selectAll": "Izberi vse", + "newKnowledge.selectAllSources": "Izberi vse vire", + "newKnowledge.selectFilesAndFolders": "Izberite datoteke in mape za sinhronizacijo", + "newKnowledge.skippedFailed": "Preskočeno (neuspešno)", + "newKnowledge.skippedOffDomain": "Preskočeno (zunaj domene)", "newKnowledge.sourceActions": "Dejanja za {{name}}", "newKnowledge.sourceColumn": "Vir", "newKnowledge.sourceFilterLabel": "Filtrirajte vire po statusu", "newKnowledge.sourceName": "Ime vira", "newKnowledge.sourceNamePlaceholder": "npr. preiskovanje dokumentacije izdelka", "newKnowledge.sourceNameRequired": "Vnesite ime vira.", + "newKnowledge.sourceSetupBackendDependency": "Nastavitev vira je prikazana v celoti; urejanje in predogled zahtevata zaledni potek za osnutek vira.", "newKnowledge.sourceStatus.active": "Aktivno", "newKnowledge.sourceStatus.disabled": "Onemogočeno", "newKnowledge.sourceStatus.error": "Napaka", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Ustavi", "newKnowledge.stoppingCrawl": "Ustavljanje…", "newKnowledge.syncNow": "Sinhroniziraj zdaj", + "newKnowledge.syncPolicy": "Pravilnik sinhronizacije", "newKnowledge.syncPolicyColumn": "Pravilnik o sinhronizaciji", + "newKnowledge.syncPolicyCustom": "Interval po meri", + "newKnowledge.syncPolicyDaily": "Vsakih 24 ur", + "newKnowledge.syncPolicyLoadFailed": "Pravilnika sinhronizacije ni bilo mogoče naložiti.", + "newKnowledge.syncPolicyManual": "Samo ročno", + "newKnowledge.syncPolicyProvider": "Uporabi urnik ponudnika", "newKnowledge.tags": "Oznake", "newKnowledge.unavailableDescription": "KnowledgeFS trenutno ni na voljo za ta delovni prostor.", "newKnowledge.unavailableTitle": "Novo znanje ni na voljo", "newKnowledge.updated": "Posodobljeno {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Znaki: —", "newKnowledge.uploadFiles": "Naloži datoteke", "newKnowledge.uploadFilesDescription": "Dodajte datoteke iz računalnika — indeksirajo se takoj ob ustvarjanju.", + "newKnowledge.uploadingFiles": "Nalaganje…", "newKnowledge.usingDefaults": "Uporabljene so privzete nastavitve", "newKnowledge.viewLabel": "Prikaz znanja", "newKnowledge.websiteCrawl": "Iskanje po vsebini spletne strani", diff --git a/web/i18n/th-TH/dataset.json b/web/i18n/th-TH/dataset.json index d074fa526c5..9d0b197eaf6 100644 --- a/web/i18n/th-TH/dataset.json +++ b/web/i18n/th-TH/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "การดำเนินการ", "newKnowledge.addSource": "เพิ่มแหล่งที่มา", "newKnowledge.addSourceDescription": "เชื่อมต่อแหล่งข้อมูลกับฐานความรู้นี้ โดยจะซิงค์โดยอัตโนมัติและมีความสดใหม่อยู่เสมอ", + "newKnowledge.addSourceFailed": "ไม่สามารถเพิ่มแหล่งข้อมูลนี้ได้ โปรดตรวจสอบการตั้งค่าแล้วลองอีกครั้ง", "newKnowledge.addSourceRequiresSelection": "เลือกหน้าที่รวบรวมข้อมูลแล้วอย่างน้อยหนึ่งหน้าก่อนเพิ่มแหล่งข้อมูล", "newKnowledge.adjustAndRecrawl": "ปรับและรวบรวมข้อมูลอีกครั้ง", "newKnowledge.allSources": "แหล่งที่มาทั้งหมด", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "ยกเลิก", "newKnowledge.cardType": "พื้นที่ความรู้", "newKnowledge.configureProvider": "กำหนดค่า {{provider}}", + "newKnowledge.connectNotion": "เชื่อมต่อ Notion", "newKnowledge.connectProvider": "เชื่อมต่อ {{provider}}", + "newKnowledge.connectProviderGeneric": "เชื่อมต่อผู้ให้บริการ", "newKnowledge.connectSource": "เชื่อมต่อแหล่งข้อมูล", "newKnowledge.connectSourceDescription": "เนื้อหาจะซิงค์กับ Notion เว็บไซต์ และบริการอื่น ๆ อยู่เสมอ", "newKnowledge.connectingProvider": "กำลังเชื่อมต่อ...", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "โปรแกรมรวบรวมข้อมูลไม่สามารถทำตัวอย่างนี้ให้เสร็จได้ ตรวจสอบ URL ปรับตัวเลือกการรวบรวมข้อมูล หรือลองอีกครั้ง", "newKnowledge.crawlFailedProvider": "{{provider}} รายงานข้อผิดพลาด ตรวจสอบการเชื่อมต่อผู้ให้บริการแล้วลองอีกครั้ง", "newKnowledge.crawlFailedTimeout": "หมดเวลาการรวบรวมข้อมูล ลองใช้ URL ที่เจาะจงขึ้นหรือลดจำนวนหน้าสูงสุด", + "newKnowledge.crawlNotStarted": "ยังไม่เริ่ม", "newKnowledge.crawlOptions": "ตัวเลือกการรวบรวมข้อมูล", + "newKnowledge.crawlPreview": "ตัวอย่างการเก็บข้อมูล", "newKnowledge.crawlProgress": "ความคืบหน้าการรวบรวมข้อมูลของ {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "การกำหนดค่าการรวบรวมข้อมูลเว็บไซต์ถูกปิดใช้งานจนกว่าเวิร์กโฟลว์การรวบรวมข้อมูลจะพร้อมใช้งาน", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl พร้อมแล้ว", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "กำลังรวบรวมข้อมูล…", "newKnowledge.crawlingPages": "กำลังรวบรวมข้อมูล {{host}}… พบ {{count}} หน้า", "newKnowledge.createFailed": "เราไม่สามารถสร้างฐานความรู้นี้ได้ ตรวจสอบการเข้าถึงของคุณแล้วลองอีกครั้ง", + "newKnowledge.createIllustrationTitle": "เชื่อมต่อเนื้อหาเพียงครั้งเดียว — ทุกแอปจะตอบด้วยความรู้ล่าสุด", "newKnowledge.createTitle": "สร้างองค์ความรู้", "newKnowledge.creators": "ผู้สร้าง", + "newKnowledge.customIntervalHours": "ช่วงเวลาแบบกำหนดเอง (ชั่วโมง)", + "newKnowledge.customIntervalInvalid": "ป้อนจำนวนเต็มตั้งแต่ 1 ถึง 720 ชั่วโมง", "newKnowledge.description": "คำอธิบาย", "newKnowledge.descriptionHelp": "คำอธิบายที่ชัดเจนช่วยให้แอปตัดสินใจว่าจะค้นหาความรู้นี้เมื่อใด", "newKnowledge.descriptionPlaceholder": "เช่น นโยบายการคืนเงิน คำถามที่พบบ่อยเกี่ยวกับการจัดส่ง และขั้นตอนการยกระดับ — ใช้สำหรับคำถามด้านการสนับสนุนลูกค้า", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "ละทิ้งฉบับร่าง", "newKnowledge.discardDraftDescription": "การเปลี่ยนแปลงในฉบับร่างฐานความรู้นี้จะสูญหาย", "newKnowledge.discardDraftTitle": "ละทิ้งฉบับร่างฐานความรู้นี้หรือไม่?", + "newKnowledge.discardSourceChanges": "ละทิ้งการเปลี่ยนแปลงแหล่งข้อมูลหรือไม่", + "newKnowledge.discardSourceChangesConfirm": "ละทิ้งการเปลี่ยนแปลง", + "newKnowledge.discardSourceChangesDescription": "ตัวอย่างการรวบรวมข้อมูล การเลือกหน้า และการตั้งค่าการซิงค์จะสูญหาย", "newKnowledge.discardSourceDraftDescription": "การกำหนดค่าแหล่งข้อมูลและตัวอย่างการรวบรวมข้อมูลจะถูกลบ", "newKnowledge.discardSourceDraftFailed": "ไม่สามารถละทิ้งฉบับร่างแหล่งข้อมูลได้ ลองอีกครั้ง", "newKnowledge.discardSourceDraftTitle": "ละทิ้งฉบับร่างแหล่งข้อมูลนี้หรือไม่", "newKnowledge.documents": "เอกสาร", "newKnowledge.documentsUnavailable": "— เอกสาร", + "newKnowledge.editSource": "แก้ไข", "newKnowledge.emptyDescription": "เพิ่มเอกสารของคุณ แล้วเอเจนต์จะอ่านได้โดยตรง โดยไม่ต้องตั้งค่าการทำดัชนี", "newKnowledge.emptyTitle": "วิธีที่ชาญฉลาดกว่าในการสร้างฐานความรู้", "newKnowledge.errorDescription": "ไม่สามารถโหลดฐานความรู้ใหม่ได้ โปรดลองอีกครั้ง", "newKnowledge.errorTitle": "เกิดข้อผิดพลาด", "newKnowledge.filtersUnavailable": "ตัวกรองนี้จะพร้อมใช้งานเมื่อ KnowledgeFS เปิดเผยข้อมูลเมตา", + "newKnowledge.firecrawlUnavailable": "Firecrawl ไม่พร้อมใช้งานในแค็ตตาล็อกผู้ให้บริการสำหรับพื้นที่ทำงานนี้", "newKnowledge.gotIt": "เข้าใจแล้ว", "newKnowledge.guideDescription": "เชื่อมต่อแหล่งข้อมูล แล้วเอกสารจะซิงค์ ทำดัชนีอัตโนมัติ และพร้อมใช้งานโดยไม่ต้องจัดการการตั้งค่า ฐานความรู้เดิมของคุณยังคงทำงานได้ตามปกติ", "newKnowledge.guideTitle": "พบกับฐานความรู้แบบใหม่ ✨", "newKnowledge.illustrationHeadline": "เชื่อมต่อเนื้อหาครั้งเดียว ทุกแอปจะตอบด้วยความรู้ล่าสุด", "newKnowledge.includeSubpages": "รวมหน้าย่อย", "newKnowledge.invalidRootUrl": "ป้อน URL http(s) ที่ถูกต้อง", + "newKnowledge.keepEditing": "แก้ไขต่อ", "newKnowledge.lastSyncColumn": "ซิงค์ครั้งล่าสุด", "newKnowledge.learnMore": "เรียนรู้เพิ่มเติม", "newKnowledge.leavePartialSetupConfirm": "ออกจากการตั้งค่า", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "ออกจากการตั้งค่าที่ยังไม่เสร็จหรือไม่?", "newKnowledge.legacy": "แบบเดิม", "newKnowledge.loadMore": "โหลดเพิ่มเติม", + "newKnowledge.loadingSyncPolicy": "กำลังโหลดนโยบายการซิงค์", "newKnowledge.maxPages": "จำนวนหน้าสูงสุด", - "newKnowledge.moreProviders": "ดูเพิ่มเติม", + "newKnowledge.moreProviders": "ผู้ให้บริการเพิ่มเติม", "newKnowledge.name": "ชื่อความรู้", "newKnowledge.namePlaceholder": "เช่น คู่มือการสนับสนุนลูกค้า", "newKnowledge.nameRequired": "ป้อนชื่อฐานความรู้", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "โปรแกรมรวบรวมข้อมูลเข้าถึงเว็บไซต์แล้วแต่ไม่พบหน้าที่สร้างดัชนีได้ ลองใช้ URL อื่นหรือขยายตัวเลือกการรวบรวมข้อมูล", "newKnowledge.notFoundDescription": "ฐานความรู้นี้อาจถูกลบไปแล้วหรือคุณอาจไม่สามารถเข้าถึงได้", "newKnowledge.notFoundTitle": "ไม่พบฐานความรู้", + "newKnowledge.notionNotConnected": "ยังไม่ได้เชื่อมต่อ Notion", + "newKnowledge.notionNotConnectedDescription": "เชื่อมต่อ Notion เพื่อเลือกหน้าและคงการซิงค์ไว้", "newKnowledge.onlineDocuments": "เอกสารออนไลน์", "newKnowledge.onlineDrive": "ไดรฟ์ออนไลน์", "newKnowledge.pagesAppearDescription": "รวบรวมข้อมูลเว็บไซต์เพื่อตรวจสอบหน้าที่จะเพิ่ม", "newKnowledge.pagesAppearTitle": "หน้าจะแสดงที่นี่", "newKnowledge.pagesCrawled": "รวบรวมข้อมูล {{count}} หน้าที่ {{host}}", + "newKnowledge.pagesCrawled_one": "รวบรวมข้อมูล {{count}} หน้าที่ {{host}}", + "newKnowledge.pagesCrawled_other": "รวบรวมข้อมูล {{count}} หน้าที่ {{host}}", + "newKnowledge.pagesSelected": "เลือกแล้ว {{count}} รายการ", "newKnowledge.permission": "การอนุญาต", "newKnowledge.permissionAllMembers": "พื้นที่ทำงาน · สมาชิกทุกคนสามารถดูและแก้ไขได้", "newKnowledge.permissionOnlyMe": "มีเพียงฉันเท่านั้น", "newKnowledge.permissionRestricted": "สิทธิ์พื้นที่ทำงานของคุณไม่อนุญาตให้เปลี่ยนการตั้งค่านี้", "newKnowledge.permissionUpdateFailed": "สร้างฐานความรู้แล้ว แต่ไม่สามารถยืนยันสิทธิ์ได้ โปรดลองอีกครั้งเพื่อดำเนินการตั้งค่าให้เสร็จ", + "newKnowledge.preview": "ตัวอย่าง", + "newKnowledge.previewUnavailable": "ตัวอย่างจะพร้อมใช้งานหลังจากแบ็กเอนด์ประมวลผลไฟล์นี้เสร็จสิ้น", "newKnowledge.providerConnected": "เชื่อมต่อ {{provider}} แล้ว", "newKnowledge.providerLabel": "เลือกผู้ให้บริการ", "newKnowledge.providerLoadFailed": "ผู้ให้บริการไม่สามารถโหลดได้", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "ลบแหล่งสัญญาณ", "newKnowledge.retryCrawl": "ลองรวบรวมข้อมูลอีกครั้ง", "newKnowledge.retryProviderLoad": "ลองอีกครั้ง", + "newKnowledge.retrySyncPolicy": "ลองโหลดนโยบายการซิงค์อีกครั้ง", "newKnowledge.rootUrl": "URL หลัก", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "ส่วนนี้จะพร้อมใช้งานในขั้นตอนการตั้งค่าถัดไป", "newKnowledge.searchSources": "แหล่งค้นหา", + "newKnowledge.selectAll": "เลือกทั้งหมด", + "newKnowledge.selectAllSources": "เลือกแหล่งที่มาทั้งหมด", + "newKnowledge.selectFilesAndFolders": "เลือกไฟล์และโฟลเดอร์ที่จะซิงค์", + "newKnowledge.skippedFailed": "ข้าม (ล้มเหลว)", + "newKnowledge.skippedOffDomain": "ข้าม (นอกโดเมน)", "newKnowledge.sourceActions": "การดำเนินการสำหรับ {{name}}", "newKnowledge.sourceColumn": "ที่มา", "newKnowledge.sourceFilterLabel": "กรองแหล่งที่มาตามสถานะ", "newKnowledge.sourceName": "ชื่อแหล่งข้อมูล", "newKnowledge.sourceNamePlaceholder": "เช่น การรวบรวมข้อมูลเอกสารผลิตภัณฑ์", "newKnowledge.sourceNameRequired": "ป้อนชื่อแหล่งข้อมูล", + "newKnowledge.sourceSetupBackendDependency": "การตั้งค่าแหล่งที่มาแสดงอย่างครบถ้วน การแก้ไขและดูตัวอย่างต้องใช้เวิร์กโฟลว์แบ็กเอนด์ของแหล่งที่มาฉบับร่าง", "newKnowledge.sourceStatus.active": "แอคทีฟ", "newKnowledge.sourceStatus.disabled": "ปิดใช้งาน", "newKnowledge.sourceStatus.error": "เกิดข้อผิดพลาด", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "หยุด", "newKnowledge.stoppingCrawl": "กำลังหยุด…", "newKnowledge.syncNow": "ซิงค์เลย", + "newKnowledge.syncPolicy": "นโยบายการซิงค์", "newKnowledge.syncPolicyColumn": "นโยบายการซิงค์", + "newKnowledge.syncPolicyCustom": "ช่วงเวลาแบบกำหนดเอง", + "newKnowledge.syncPolicyDaily": "ทุก 24 ชั่วโมง", + "newKnowledge.syncPolicyLoadFailed": "ไม่สามารถโหลดนโยบายการซิงค์ได้", + "newKnowledge.syncPolicyManual": "ด้วยตนเองเท่านั้น", + "newKnowledge.syncPolicyProvider": "ใช้กำหนดการของผู้ให้บริการ", "newKnowledge.tags": "แท็ก", "newKnowledge.unavailableDescription": "ขณะนี้ KnowledgeFS ไม่พร้อมใช้งานสำหรับพื้นที่ทำงานนี้", "newKnowledge.unavailableTitle": "ความรู้แบบใหม่ไม่พร้อมใช้งาน", "newKnowledge.updated": "อัปเดตเมื่อ {{date}}", + "newKnowledge.uploadCharactersUnavailable": "อักขระ: —", "newKnowledge.uploadFiles": "อัปโหลดไฟล์", "newKnowledge.uploadFilesDescription": "เพิ่มไฟล์จากคอมพิวเตอร์ ไฟล์จะถูกทำดัชนีทันทีที่สร้าง", + "newKnowledge.uploadingFiles": "กำลังอัปโหลด…", "newKnowledge.usingDefaults": "ใช้ค่าเริ่มต้น", "newKnowledge.viewLabel": "มุมมองความรู้", "newKnowledge.websiteCrawl": "การรวบรวมข้อมูลเว็บไซต์", diff --git a/web/i18n/tr-TR/dataset.json b/web/i18n/tr-TR/dataset.json index 662888b7a72..8aab4dd2c43 100644 --- a/web/i18n/tr-TR/dataset.json +++ b/web/i18n/tr-TR/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Eylemler", "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.allSources": "Tüm kaynaklar", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "İptal et", "newKnowledge.cardType": "Bilgi alanı", "newKnowledge.configureProvider": "{{provider}}'ı Yapılandır", + "newKnowledge.connectNotion": "Notion’a bağlan", "newKnowledge.connectProvider": "{{provider}}'a bağlanın", + "newKnowledge.connectProviderGeneric": "Sağlayıcıya bağlan", "newKnowledge.connectSource": "Kaynak bağla", "newKnowledge.connectSourceDescription": "İçerik Notion, web siteleri ve diğer hizmetlerle senkronize kalır.", "newKnowledge.connectingProvider": "Bağlanıyor…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Tarayıcı bu önizlemeyi tamamlayamadı. URL’yi kontrol edin, tarama seçeneklerini ayarlayın veya yeniden deneyin.", "newKnowledge.crawlFailedProvider": "{{provider}} bir hata bildirdi. Sağlayıcı bağlantısını kontrol edip yeniden deneyin.", "newKnowledge.crawlFailedTimeout": "Tarama zaman aşımına uğradı. Daha dar kapsamlı bir URL deneyin veya en fazla sayfa sayısını azaltın.", + "newKnowledge.crawlNotStarted": "Başlatılmadı", "newKnowledge.crawlOptions": "Tarama seçenekleri", + "newKnowledge.crawlPreview": "Tarama önizlemesi", "newKnowledge.crawlProgress": "{{host}} için tarama ilerlemesi", "newKnowledge.crawlSetupUnavailableDescription": "Web sitesi tarama yapılandırması, tarama iş akışı kullanılabilir olana kadar devre dışı bırakılır.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl hazır", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Taranıyor…", "newKnowledge.crawlingPages": "{{host}} taranıyor… {{count}} sayfa bulundu", "newKnowledge.createFailed": "Bu bilgi tabanını oluşturamadık. Erişiminizi kontrol edip tekrar deneyin.", + "newKnowledge.createIllustrationTitle": "İçeriğinizi bir kez bağlayın — her uygulama en güncel bilgilerle yanıt versin.", "newKnowledge.createTitle": "Bilgi Yaratın", "newKnowledge.creators": "Oluşturanlar", + "newKnowledge.customIntervalHours": "Özel aralık (saat)", + "newKnowledge.customIntervalInvalid": "1 ile 720 saat arasında bir tam sayı girin.", "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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Taslağı sil", "newKnowledge.discardDraftDescription": "Bu bilgi tabanı taslağındaki değişiklikleriniz kaybolacak.", "newKnowledge.discardDraftTitle": "Bu bilgi tabanı taslağı silinsin mi?", + "newKnowledge.discardSourceChanges": "Kaynak değişiklikleri silinsin mi?", + "newKnowledge.discardSourceChangesConfirm": "Değişiklikleri sil", + "newKnowledge.discardSourceChangesDescription": "Tarama önizlemesi, sayfa seçimi ve eşitleme ayarları kaybolacak.", "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.documents": "Belgeler", "newKnowledge.documentsUnavailable": "— belge", + "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", "newKnowledge.errorDescription": "Yeni bilgi tabanlarınız yüklenemedi. Tekrar deneyin.", "newKnowledge.errorTitle": "Bir şeyler yanlış gitti", "newKnowledge.filtersUnavailable": "KnowledgeFS meta verilerini sunduğunda bu filtre kullanılabilir olacak.", + "newKnowledge.firecrawlUnavailable": "Firecrawl bu çalışma alanının sağlayıcı kataloğunda mevcut değil.", "newKnowledge.gotIt": "Anladım", "newKnowledge.guideDescription": "Bir kaynak bağladığınızda belgeleri senkronize kalır, otomatik olarak indekslenir ve yönetilecek bir kurulum olmadan kullanıma hazır olur. Mevcut bilgi tabanlarınız olduğu gibi çalışmaya devam eder.", "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.invalidRootUrl": "Geçerli bir http(s) URL’si girin.", + "newKnowledge.keepEditing": "Düzenlemeye devam et", "newKnowledge.lastSyncColumn": "Son senkronizasyon", "newKnowledge.learnMore": "Daha fazla bilgi", "newKnowledge.leavePartialSetupConfirm": "Kurulumdan çık", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Tamamlanmamış kurulumdan çıkılsın mı?", "newKnowledge.legacy": "Eski", "newKnowledge.loadMore": "Daha fazla yükle", + "newKnowledge.loadingSyncPolicy": "Eşitleme politikası yükleniyor", "newKnowledge.maxPages": "En fazla sayfa", - "newKnowledge.moreProviders": "Daha fazla göster", + "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.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Tarayıcı siteye ulaştı ancak dizine eklenebilir sayfa bulamadı. Farklı bir URL deneyin veya tarama seçeneklerini genişletin.", "newKnowledge.notFoundDescription": "Bu bilgi tabanı silinmiş olabilir veya erişiminiz olmayabilir.", "newKnowledge.notFoundTitle": "Bilgi tabanı bulunamadı", + "newKnowledge.notionNotConnected": "Notion bağlı değil", + "newKnowledge.notionNotConnectedDescription": "Sayfaları seçmek ve eşitlenmiş tutmak için Notion’a bağlanın.", "newKnowledge.onlineDocuments": "Çevrimiçi belgeler", "newKnowledge.onlineDrive": "Çevrimiçi sürücü", "newKnowledge.pagesAppearDescription": "Hangi sayfaların ekleneceğini görmek için siteyi tarayın.", "newKnowledge.pagesAppearTitle": "Sayfalar burada görünecek", "newKnowledge.pagesCrawled": "{{host}} üzerinde {{count}} sayfa tarandı", + "newKnowledge.pagesCrawled_one": "{{host}} üzerinde {{count}} sayfa tarandı", + "newKnowledge.pagesCrawled_other": "{{host}} üzerinde {{count}} sayfa tarandı", + "newKnowledge.pagesSelected": "{{count}} seçildi", "newKnowledge.permission": "İzin", "newKnowledge.permissionAllMembers": "Çalışma alanı · tüm üyeler görüntüleyebilir ve düzenleyebilir", "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.providerConnected": "{{provider}} bağlandı", "newKnowledge.providerLabel": "Bir sağlayıcı seçin", "newKnowledge.providerLoadFailed": "Sağlayıcılar yüklenemedi", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Kaynağı kaldır", "newKnowledge.retryCrawl": "Taramayı yeniden dene", "newKnowledge.retryProviderLoad": "Tekrar dene", + "newKnowledge.retrySyncPolicy": "Eşitleme politikasını 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.searchSources": "Kaynakları ara", + "newKnowledge.selectAll": "Tümünü seç", + "newKnowledge.selectAllSources": "Tüm kaynakları seç", + "newKnowledge.selectFilesAndFolders": "Senkronize edilecek dosya ve klasörleri seçin", + "newKnowledge.skippedFailed": "Atlandı (başarısız)", + "newKnowledge.skippedOffDomain": "Atlandı (alan adı dışında)", "newKnowledge.sourceActions": "{{name}} için eylemler", "newKnowledge.sourceColumn": "Kaynak", "newKnowledge.sourceFilterLabel": "Kaynakları duruma göre filtrele", "newKnowledge.sourceName": "Kaynak adı", "newKnowledge.sourceNamePlaceholder": "ör. ürün belgeleri taraması", "newKnowledge.sourceNameRequired": "Bir kaynak adı girin.", + "newKnowledge.sourceSetupBackendDependency": "Kaynak kurulumu eksiksiz gösterilir; düzenleme ve önizleme işlemleri taslak kaynak arka uç akışını gerektirir.", "newKnowledge.sourceStatus.active": "Aktif", "newKnowledge.sourceStatus.disabled": "Devre Dışı", "newKnowledge.sourceStatus.error": "Hata", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Durdur", "newKnowledge.stoppingCrawl": "Durduruluyor…", "newKnowledge.syncNow": "Şimdi senkronize et", + "newKnowledge.syncPolicy": "Eşitleme politikası", "newKnowledge.syncPolicyColumn": "Senkronizasyon politikası", + "newKnowledge.syncPolicyCustom": "Özel aralık", + "newKnowledge.syncPolicyDaily": "Her 24 saatte bir", + "newKnowledge.syncPolicyLoadFailed": "Eşitleme politikası yüklenemedi.", + "newKnowledge.syncPolicyManual": "Yalnızca manuel", + "newKnowledge.syncPolicyProvider": "Sağlayıcı zamanlamasını kullan", "newKnowledge.tags": "Etiketler", "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.uploadCharactersUnavailable": "Karakterler: —", "newKnowledge.uploadFiles": "Dosya yükle", "newKnowledge.uploadFilesDescription": "Bilgisayarınızdan dosya ekleyin; oluşturduğunuz anda indekslenirler.", + "newKnowledge.uploadingFiles": "Yükleniyor…", "newKnowledge.usingDefaults": "Varsayılanlar kullanılıyor", "newKnowledge.viewLabel": "Bilgi görünümü", "newKnowledge.websiteCrawl": "Web sitesi taraması", diff --git a/web/i18n/uk-UA/dataset.json b/web/i18n/uk-UA/dataset.json index 70c40da1ce8..0af9b2f1618 100644 --- a/web/i18n/uk-UA/dataset.json +++ b/web/i18n/uk-UA/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Дії", "newKnowledge.addSource": "Додати джерело", "newKnowledge.addSourceDescription": "Підключіть джерело до цієї бази знань — вона автоматично синхронізується та залишається актуальною.", + "newKnowledge.addSourceFailed": "Не вдалося додати це джерело. Перевірте налаштування та повторіть спробу.", "newKnowledge.addSourceRequiresSelection": "Перед додаванням джерела виберіть принаймні одну проскановану сторінку.", "newKnowledge.adjustAndRecrawl": "Налаштувати й просканувати знову", "newKnowledge.allSources": "Усі джерела", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Скасувати", "newKnowledge.cardType": "Простір знань", "newKnowledge.configureProvider": "Налаштувати {{provider}}", + "newKnowledge.connectNotion": "Підключити Notion", "newKnowledge.connectProvider": "Підключіть {{provider}}", + "newKnowledge.connectProviderGeneric": "Підключити провайдера", "newKnowledge.connectSource": "Підключити джерело", "newKnowledge.connectSourceDescription": "Вміст залишається синхронізованим із Notion, вебсайтами та іншими сервісами.", "newKnowledge.connectingProvider": "Підключення…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Робот не зміг завершити попередній перегляд. Перевірте URL-адресу, змініть параметри сканування або повторіть спробу.", "newKnowledge.crawlFailedProvider": "{{provider}} повідомив про помилку. Перевірте підключення до постачальника й повторіть спробу.", "newKnowledge.crawlFailedTimeout": "Час очікування сканування минув. Спробуйте точнішу URL-адресу або зменште максимальну кількість сторінок.", + "newKnowledge.crawlNotStarted": "Не розпочато", "newKnowledge.crawlOptions": "Параметри сканування", + "newKnowledge.crawlPreview": "Попередній перегляд обходу", "newKnowledge.crawlProgress": "Хід сканування {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Конфігурацію сканування веб-сайту вимкнено, доки робочий процес сканування не стане доступним.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl готовий", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Сканування…", "newKnowledge.crawlingPages": "Сканування {{host}}… знайдено сторінок: {{count}}", "newKnowledge.createFailed": "Ми не змогли створити цю базу знань. Перевірте свій доступ і повторіть спробу.", + "newKnowledge.createIllustrationTitle": "Підключіть вміст один раз — кожен застосунок відповідатиме на основі найсвіжіших знань.", "newKnowledge.createTitle": "Створюйте знання", "newKnowledge.creators": "Автори", + "newKnowledge.customIntervalHours": "Власний інтервал (години)", + "newKnowledge.customIntervalInvalid": "Введіть ціле число від 1 до 720 годин.", "newKnowledge.description": "опис", "newKnowledge.descriptionHelp": "Чіткий опис допомагає програмам вирішити, коли шукати ці знання.", "newKnowledge.descriptionPlaceholder": "напр. Політика відшкодування, поширені запитання щодо доставки та етапи ескалації — використовуйте для запитань служби підтримки клієнтів", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Відхилити чернетку", "newKnowledge.discardDraftDescription": "Зміни в цій чернетці бази знань буде втрачено.", "newKnowledge.discardDraftTitle": "Відхилити цю чернетку бази знань?", + "newKnowledge.discardSourceChanges": "Відхилити зміни джерела?", + "newKnowledge.discardSourceChangesConfirm": "Відхилити зміни", + "newKnowledge.discardSourceChangesDescription": "Попередній перегляд сканування, вибір сторінок і налаштування синхронізації буде втрачено.", "newKnowledge.discardSourceDraftDescription": "Конфігурацію джерела й попередній перегляд сканування буде видалено.", "newKnowledge.discardSourceDraftFailed": "Не вдалося відхилити чернетку джерела. Повторіть спробу.", "newKnowledge.discardSourceDraftTitle": "Відхилити цю чернетку джерела?", "newKnowledge.documents": "Документи", "newKnowledge.documentsUnavailable": "— документів", + "newKnowledge.editSource": "Редагувати", "newKnowledge.emptyDescription": "Додайте документи, і агенти зможуть читати їх безпосередньо — без налаштування індексації.", "newKnowledge.emptyTitle": "Розумніший спосіб створювати бази знань", "newKnowledge.errorDescription": "Не вдалося завантажити нові бази знань. Спробуйте ще раз.", "newKnowledge.errorTitle": "Щось пішло не так", "newKnowledge.filtersUnavailable": "Фільтр стане доступним, коли KnowledgeFS надасть метадані.", + "newKnowledge.firecrawlUnavailable": "Firecrawl недоступний у каталозі постачальників для цієї робочої області.", "newKnowledge.gotIt": "Зрозуміло", "newKnowledge.guideDescription": "Підключіть джерело, і його документи залишатимуться синхронізованими, автоматично індексуватимуться та будуть готові до використання без додаткового налаштування. Ваші попередні бази знань працюватимуть як і раніше.", "newKnowledge.guideTitle": "Познайомтеся з новою базою знань ✨", "newKnowledge.illustrationHeadline": "Підключіть вміст один раз — і кожна програма відповідатиме на основі найсвіжіших знань.", "newKnowledge.includeSubpages": "Включати підсторінки", "newKnowledge.invalidRootUrl": "Введіть дійсну URL-адресу http(s).", + "newKnowledge.keepEditing": "Продовжити редагування", "newKnowledge.lastSyncColumn": "Остання синхронізація", "newKnowledge.learnMore": "Докладніше", "newKnowledge.leavePartialSetupConfirm": "Вийти з налаштування", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Вийти з незавершеного налаштування?", "newKnowledge.legacy": "Попередня версія", "newKnowledge.loadMore": "Завантажити ще", + "newKnowledge.loadingSyncPolicy": "Завантаження політики синхронізації", "newKnowledge.maxPages": "Максимум сторінок", - "newKnowledge.moreProviders": "Дивитись більше", + "newKnowledge.moreProviders": "Інші провайдери", "newKnowledge.name": "Назва знання", "newKnowledge.namePlaceholder": "напр. Посібник із підтримки клієнтів", "newKnowledge.nameRequired": "Введіть назву бази знань.", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "Робот отримав доступ до сайту, але не знайшов сторінок для індексування. Спробуйте іншу URL-адресу або розширте параметри сканування.", "newKnowledge.notFoundDescription": "Можливо, цю базу знань було видалено або ви не маєте доступу.", "newKnowledge.notFoundTitle": "База знань не знайдена", + "newKnowledge.notionNotConnected": "Notion не підключено", + "newKnowledge.notionNotConnectedDescription": "Підключіть Notion, щоб вибирати сторінки й підтримувати їх синхронізацію.", "newKnowledge.onlineDocuments": "Документи онлайн", "newKnowledge.onlineDrive": "Онлайн драйв", "newKnowledge.pagesAppearDescription": "Проскануйте сайт, щоб переглянути сторінки, які буде додано.", "newKnowledge.pagesAppearTitle": "Тут з’являться сторінки", "newKnowledge.pagesCrawled": "Проскановано сторінок на {{host}}: {{count}}", + "newKnowledge.pagesCrawled_one": "Проскановано сторінок на {{host}}: {{count}}", + "newKnowledge.pagesCrawled_other": "Проскановано сторінок на {{host}}: {{count}}", + "newKnowledge.pagesSelected": "Вибрано: {{count}}", "newKnowledge.permission": "Дозвіл", "newKnowledge.permissionAllMembers": "Робоча область · усі учасники можуть переглядати та редагувати", "newKnowledge.permissionOnlyMe": "Тільки я", "newKnowledge.permissionRestricted": "Дозволи вашої робочої області не дозволяють змінити це налаштування.", "newKnowledge.permissionUpdateFailed": "Базу знань створено, але не вдалося підтвердити її дозвіл. Повторіть спробу, щоб завершити налаштування.", + "newKnowledge.preview": "Попередній перегляд", + "newKnowledge.previewUnavailable": "Попередній перегляд стане доступним після завершення обробки цього файлу сервером.", "newKnowledge.providerConnected": "{{provider}} підключено", "newKnowledge.providerLabel": "Оберіть провайдера", "newKnowledge.providerLoadFailed": "Не вдалося завантажити постачальників", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "Видалити джерело", "newKnowledge.retryCrawl": "Повторити сканування", "newKnowledge.retryProviderLoad": "Спробуйте ще раз", + "newKnowledge.retrySyncPolicy": "Повторити завантаження політики синхронізації", "newKnowledge.rootUrl": "Коренева URL-адреса", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "Цей розділ стане доступним на наступному кроці налаштування.", "newKnowledge.searchSources": "Пошук джерел", + "newKnowledge.selectAll": "Вибрати все", + "newKnowledge.selectAllSources": "Вибрати всі джерела", + "newKnowledge.selectFilesAndFolders": "Виберіть файли й папки для синхронізації", + "newKnowledge.skippedFailed": "Пропущено (помилка)", + "newKnowledge.skippedOffDomain": "Пропущено (поза доменом)", "newKnowledge.sourceActions": "Дії для {{name}}", "newKnowledge.sourceColumn": "Джерело", "newKnowledge.sourceFilterLabel": "Фільтрувати джерела за статусом", "newKnowledge.sourceName": "Назва джерела", "newKnowledge.sourceNamePlaceholder": "наприклад, сканування документації продукту", "newKnowledge.sourceNameRequired": "Введіть назву джерела.", + "newKnowledge.sourceSetupBackendDependency": "Налаштування джерела показано повністю; для редагування й перегляду потрібен серверний процес чернетки джерела.", "newKnowledge.sourceStatus.active": "Активний", "newKnowledge.sourceStatus.disabled": "Вимкнено", "newKnowledge.sourceStatus.error": "Помилка", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Зупинити", "newKnowledge.stoppingCrawl": "Зупинення…", "newKnowledge.syncNow": "Синхронізувати зараз", + "newKnowledge.syncPolicy": "Політика синхронізації", "newKnowledge.syncPolicyColumn": "Політика синхронізації", + "newKnowledge.syncPolicyCustom": "Власний інтервал", + "newKnowledge.syncPolicyDaily": "Кожні 24 години", + "newKnowledge.syncPolicyLoadFailed": "Не вдалося завантажити політику синхронізації.", + "newKnowledge.syncPolicyManual": "Лише вручну", + "newKnowledge.syncPolicyProvider": "Використовувати розклад постачальника", "newKnowledge.tags": "Теги", "newKnowledge.unavailableDescription": "KnowledgeFS зараз недоступний для цього робочого простору.", "newKnowledge.unavailableTitle": "Нова база знань недоступна", "newKnowledge.updated": "Оновлено {{date}}", + "newKnowledge.uploadCharactersUnavailable": "Символи: —", "newKnowledge.uploadFiles": "Завантажити файли", "newKnowledge.uploadFilesDescription": "Додайте файли з комп’ютера — вони індексуються одразу після створення.", + "newKnowledge.uploadingFiles": "Завантаження…", "newKnowledge.usingDefaults": "Використовуються стандартні налаштування", "newKnowledge.viewLabel": "Вигляд бази знань", "newKnowledge.websiteCrawl": "Сканування веб-сайту", diff --git a/web/i18n/vi-VN/dataset.json b/web/i18n/vi-VN/dataset.json index 696e82c1c0c..81637c9e4fd 100644 --- a/web/i18n/vi-VN/dataset.json +++ b/web/i18n/vi-VN/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "Hành động", "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.allSources": "Tất cả các nguồn", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "Hủy", "newKnowledge.cardType": "Không gian kiến thức", "newKnowledge.configureProvider": "Định cấu hình {{provider}}", + "newKnowledge.connectNotion": "Kết nối Notion", "newKnowledge.connectProvider": "Kết nối {{provider}}", + "newKnowledge.connectProviderGeneric": "Kết nối nhà cung cấp", "newKnowledge.connectSource": "Kết nối nguồn", "newKnowledge.connectSourceDescription": "Nội dung luôn được đồng bộ với Notion, trang web và các dịch vụ khác.", "newKnowledge.connectingProvider": "Đang kết nối…", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "Trình thu thập không thể hoàn tất bản xem trước này. Kiểm tra URL, điều chỉnh tùy chọn thu thập hoặc thử lại.", "newKnowledge.crawlFailedProvider": "{{provider}} đã báo lỗi. Kiểm tra kết nối nhà cung cấp rồi thử lại.", "newKnowledge.crawlFailedTimeout": "Quá thời gian thu thập. Hãy thử URL cụ thể hơn hoặc giảm số trang tối đa.", + "newKnowledge.crawlNotStarted": "Chưa bắt đầu", "newKnowledge.crawlOptions": "Tùy chọn thu thập", + "newKnowledge.crawlPreview": "Xem trước thu thập", "newKnowledge.crawlProgress": "Tiến trình thu thập của {{host}}", "newKnowledge.crawlSetupUnavailableDescription": "Cấu hình thu thập thông tin trang web bị tắt cho đến khi có quy trình thu thập thông tin.", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl đã sẵn sàng", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "Đang thu thập…", "newKnowledge.crawlingPages": "Đang thu thập {{host}}… đã tìm thấy {{count}} trang", "newKnowledge.createFailed": "Chúng tôi không thể tạo ra nền tảng kiến thức này. Hãy kiểm tra quyền truy cập của bạn và thử lại.", + "newKnowledge.createIllustrationTitle": "Kết nối nội dung một lần — mọi ứng dụng đều trả lời bằng kiến thức mới nhất.", "newKnowledge.createTitle": "Tạo kiến thức", "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.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", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "Hủy bản nháp", "newKnowledge.discardDraftDescription": "Các thay đổi đối với bản nháp cơ sở kiến thức này sẽ bị mất.", "newKnowledge.discardDraftTitle": "Hủy bản nháp cơ sở kiến thức này?", + "newKnowledge.discardSourceChanges": "Hủy thay đổi nguồn?", + "newKnowledge.discardSourceChangesConfirm": "Hủy thay đổi", + "newKnowledge.discardSourceChangesDescription": "Bản xem trước thu thập dữ liệu, lựa chọn trang và cài đặt đồng bộ sẽ bị mất.", "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.documents": "Tài liệu", "newKnowledge.documentsUnavailable": "— tài liệu", + "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", "newKnowledge.errorDescription": "Không thể tải các cơ sở kiến thức mới. Hãy thử lại.", "newKnowledge.errorTitle": "Đã xảy ra lỗi", "newKnowledge.filtersUnavailable": "Bộ lọc này sẽ khả dụng khi KnowledgeFS cung cấp siêu dữ liệu.", + "newKnowledge.firecrawlUnavailable": "Firecrawl không có sẵn trong danh mục nhà cung cấp cho không gian làm việc này.", "newKnowledge.gotIt": "Đã hiểu", "newKnowledge.guideDescription": "Kết nối một nguồn và tài liệu của nguồn đó sẽ luôn được đồng bộ, tự động lập chỉ mục và sẵn sàng sử dụng mà không cần quản lý thiết lập. Các cơ sở kiến thức cũ vẫn hoạt động như trước.", "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.invalidRootUrl": "Nhập URL http(s) hợp lệ.", + "newKnowledge.keepEditing": "Tiếp tục chỉnh sửa", "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", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "Thoát khỏi thiết lập chưa hoàn tất?", "newKnowledge.legacy": "Cũ", "newKnowledge.loadMore": "Tải thêm", + "newKnowledge.loadingSyncPolicy": "Đang tải chính sách đồng bộ", "newKnowledge.maxPages": "Số trang tối đa", - "newKnowledge.moreProviders": "Xem thêm", + "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.", @@ -217,16 +232,23 @@ "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.", "newKnowledge.notFoundDescription": "Cơ sở kiến thức này có thể đã bị xóa hoặc bạn không có quyền truy cập.", "newKnowledge.notFoundTitle": "Không tìm thấy cơ sở kiến thức", + "newKnowledge.notionNotConnected": "Notion chưa được kết nối", + "newKnowledge.notionNotConnectedDescription": "Kết nối Notion để chọn trang và duy trì đồng bộ.", "newKnowledge.onlineDocuments": "Tài liệu trực tuyến", "newKnowledge.onlineDrive": "Ổ đĩa trực tuyến", "newKnowledge.pagesAppearDescription": "Thu thập trang web để xem những trang nào sẽ được thêm.", "newKnowledge.pagesAppearTitle": "Các trang sẽ xuất hiện ở đây", "newKnowledge.pagesCrawled": "Đã thu thập {{count}} trang tại {{host}}", + "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.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.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.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", @@ -240,16 +262,23 @@ "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.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.searchSources": "Nguồn tìm kiếm", + "newKnowledge.selectAll": "Chọn tất cả", + "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)", + "newKnowledge.skippedOffDomain": "Đã bỏ qua (ngoài miền)", "newKnowledge.sourceActions": "Hành động dành cho {{name}}", "newKnowledge.sourceColumn": "Nguồn", "newKnowledge.sourceFilterLabel": "Lọc nguồn theo trạng thái", "newKnowledge.sourceName": "Tên nguồn", "newKnowledge.sourceNamePlaceholder": "ví dụ: thu thập tài liệu sản phẩm", "newKnowledge.sourceNameRequired": "Nhập tên nguồn.", + "newKnowledge.sourceSetupBackendDependency": "Thiết lập nguồn được hiển thị đầy đủ; thao tác chỉnh sửa và xem trước cần quy trình backend cho nguồn nháp.", "newKnowledge.sourceStatus.active": "Đang hoạt động", "newKnowledge.sourceStatus.disabled": "Đã tắt", "newKnowledge.sourceStatus.error": "Lỗi", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "Dừng", "newKnowledge.stoppingCrawl": "Đang dừng…", "newKnowledge.syncNow": "Đồng bộ hóa ngay", + "newKnowledge.syncPolicy": "Chính sách đồng bộ", "newKnowledge.syncPolicyColumn": "Chính sách đồng bộ hóa", + "newKnowledge.syncPolicyCustom": "Khoảng thời gian tùy chỉnh", + "newKnowledge.syncPolicyDaily": "Mỗi 24 giờ", + "newKnowledge.syncPolicyLoadFailed": "Không thể tải chính sách đồng bộ.", + "newKnowledge.syncPolicyManual": "Chỉ thủ công", + "newKnowledge.syncPolicyProvider": "Dùng lịch của nhà cung cấp", "newKnowledge.tags": "Thẻ", "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.uploadCharactersUnavailable": "Ký tự: —", "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…", "newKnowledge.usingDefaults": "Đang dùng giá trị mặc định", "newKnowledge.viewLabel": "Chế độ xem kiến thức", "newKnowledge.websiteCrawl": "Thu thập thông tin trang web", diff --git a/web/i18n/zh-Hans/dataset.json b/web/i18n/zh-Hans/dataset.json index 8f693a79a32..4eec8a5ee87 100644 --- a/web/i18n/zh-Hans/dataset.json +++ b/web/i18n/zh-Hans/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "行动", "newKnowledge.addSource": "添加来源", "newKnowledge.addSourceDescription": "将来源连接到该知识库 - 它会自动同步并保持最新。", + "newKnowledge.addSourceFailed": "无法添加此来源。请检查设置后重试。", "newKnowledge.addSourceRequiresSelection": "添加来源前,请至少选择一个已抓取的页面。", "newKnowledge.adjustAndRecrawl": "调整并重新抓取", "newKnowledge.allSources": "所有来源", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "取消", "newKnowledge.cardType": "知识空间", "newKnowledge.configureProvider": "配置{{provider}}", + "newKnowledge.connectNotion": "连接 Notion", "newKnowledge.connectProvider": "连接{{provider}}", + "newKnowledge.connectProviderGeneric": "连接提供商", "newKnowledge.connectSource": "连接数据源", "newKnowledge.connectSourceDescription": "内容会与 Notion、网站及其他服务保持同步。", "newKnowledge.connectingProvider": "正在连接...", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "抓取工具无法完成此预览。请检查 URL、调整抓取选项,或重试。", "newKnowledge.crawlFailedProvider": "{{provider}} 报告了错误。请检查提供商连接并重试。", "newKnowledge.crawlFailedTimeout": "抓取超时。请尝试范围更小的 URL,或减少最大页面数。", + "newKnowledge.crawlNotStarted": "尚未开始", "newKnowledge.crawlOptions": "抓取选项", + "newKnowledge.crawlPreview": "抓取预览", "newKnowledge.crawlProgress": "{{host}} 的抓取进度", "newKnowledge.crawlSetupUnavailableDescription": "在爬网工作流程可用之前,网站爬网配置将被禁用。", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl 已准备就绪", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "正在抓取…", "newKnowledge.crawlingPages": "正在抓取 {{host}}… 已找到 {{count}} 个页面", "newKnowledge.createFailed": "无法创建此知识库,请检查权限后重试。", + "newKnowledge.createIllustrationTitle": "只需连接一次内容——每个应用都能用最新知识作答。", "newKnowledge.createTitle": "创建知识库", "newKnowledge.creators": "创建者", + "newKnowledge.customIntervalHours": "自定义间隔(小时)", + "newKnowledge.customIntervalInvalid": "请输入 1 到 720 之间的整数小时数。", "newKnowledge.description": "描述", "newKnowledge.descriptionHelp": "清晰的描述有助于应用判断何时搜索此知识库。", "newKnowledge.descriptionPlaceholder": "例如:退款政策、配送常见问题和升级处理步骤——用于客户支持问题", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "放弃草稿", "newKnowledge.discardDraftDescription": "对此知识库草稿的更改将会丢失。", "newKnowledge.discardDraftTitle": "放弃此知识库草稿?", + "newKnowledge.discardSourceChanges": "放弃来源更改?", + "newKnowledge.discardSourceChangesConfirm": "放弃更改", + "newKnowledge.discardSourceChangesDescription": "抓取预览、页面选择和同步设置将会丢失。", "newKnowledge.discardSourceDraftDescription": "来源配置和抓取预览将被移除。", "newKnowledge.discardSourceDraftFailed": "无法放弃来源草稿。请重试。", "newKnowledge.discardSourceDraftTitle": "放弃此来源草稿?", "newKnowledge.documents": "文档", "newKnowledge.documentsUnavailable": "— 篇文档", + "newKnowledge.editSource": "编辑", "newKnowledge.emptyDescription": "添加文档后,智能体即可直接读取,无需配置索引。", "newKnowledge.emptyTitle": "更智能的知识库构建方式", "newKnowledge.errorDescription": "无法加载新知识库,请重试。", "newKnowledge.errorTitle": "出现错误", "newKnowledge.filtersUnavailable": "KnowledgeFS 提供元数据后即可使用此筛选器。", + "newKnowledge.firecrawlUnavailable": "Firecrawl 在此工作区的提供程序目录中不可用。", "newKnowledge.gotIt": "知道了", "newKnowledge.guideDescription": "连接数据源后,其文档会保持同步、自动建立索引并可立即使用,无需管理任何配置。旧版知识库仍会照常运行。", "newKnowledge.guideTitle": "认识全新的知识库 ✨", "newKnowledge.illustrationHeadline": "只需连接一次内容,每个应用都能基于最新知识回答。", "newKnowledge.includeSubpages": "包含子页面", "newKnowledge.invalidRootUrl": "请输入有效的 http(s) URL。", + "newKnowledge.keepEditing": "继续编辑", "newKnowledge.lastSyncColumn": "上次同步", "newKnowledge.learnMore": "了解更多", "newKnowledge.leavePartialSetupConfirm": "离开设置", @@ -205,6 +219,7 @@ "newKnowledge.leavePartialSetupTitle": "离开未完成的设置?", "newKnowledge.legacy": "旧版", "newKnowledge.loadMore": "加载更多", + "newKnowledge.loadingSyncPolicy": "正在加载同步策略", "newKnowledge.maxPages": "最大页面数", "newKnowledge.moreProviders": "更多提供商", "newKnowledge.name": "知识库名称", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "抓取工具已访问网站,但未找到可建立索引的页面。请尝试其他 URL,或扩大抓取范围。", "newKnowledge.notFoundDescription": "此知识库可能已被删除,或你没有访问权限。", "newKnowledge.notFoundTitle": "未找到知识库", + "newKnowledge.notionNotConnected": "Notion 尚未连接", + "newKnowledge.notionNotConnectedDescription": "连接 Notion 以选择页面并保持同步。", "newKnowledge.onlineDocuments": "在线文档", "newKnowledge.onlineDrive": "在线驱动器", "newKnowledge.pagesAppearDescription": "抓取网站以查看将添加哪些页面。", "newKnowledge.pagesAppearTitle": "页面将显示在此处", "newKnowledge.pagesCrawled": "已抓取 {{host}} 的 {{count}} 个页面", + "newKnowledge.pagesCrawled_one": "已抓取 {{host}} 的 {{count}} 个页面", + "newKnowledge.pagesCrawled_other": "已抓取 {{host}} 的 {{count}} 个页面", + "newKnowledge.pagesSelected": "已选择 {{count}} 项", "newKnowledge.permission": "权限", "newKnowledge.permissionAllMembers": "工作区 · 所有成员均可查看和编辑", "newKnowledge.permissionOnlyMe": "仅自己", "newKnowledge.permissionRestricted": "你的工作区权限不允许更改此设置。", "newKnowledge.permissionUpdateFailed": "知识库已创建,但无法确认其权限状态。请重试以完成设置。", + "newKnowledge.preview": "预览", + "newKnowledge.previewUnavailable": "后端完成此文件的处理后即可预览。", "newKnowledge.providerConnected": "{{provider}}已连接", "newKnowledge.providerLabel": "提供商", "newKnowledge.providerLoadFailed": "提供商无法加载", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "删除来源", "newKnowledge.retryCrawl": "重试抓取", "newKnowledge.retryProviderLoad": "重试", + "newKnowledge.retrySyncPolicy": "重试同步策略", "newKnowledge.rootUrl": "根 URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "此部分将在下一设置步骤中开放。", "newKnowledge.searchSources": "搜索来源", + "newKnowledge.selectAll": "全选", + "newKnowledge.selectAllSources": "选择所有来源", + "newKnowledge.selectFilesAndFolders": "选择要同步的文件和文件夹", + "newKnowledge.skippedFailed": "已跳过(失败)", + "newKnowledge.skippedOffDomain": "已跳过(域外)", "newKnowledge.sourceActions": "{{name}} 的操作", "newKnowledge.sourceColumn": "来源", "newKnowledge.sourceFilterLabel": "按状态过滤源", "newKnowledge.sourceName": "来源名称", "newKnowledge.sourceNamePlaceholder": "例如:产品文档抓取", "newKnowledge.sourceNameRequired": "请输入来源名称。", + "newKnowledge.sourceSetupBackendDependency": "来源设置已完整展示;编辑和预览操作需要草稿来源后端流程。", "newKnowledge.sourceStatus.active": "活跃", "newKnowledge.sourceStatus.disabled": "已禁用", "newKnowledge.sourceStatus.error": "错误", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "停止", "newKnowledge.stoppingCrawl": "正在停止…", "newKnowledge.syncNow": "立即同步", + "newKnowledge.syncPolicy": "同步策略", "newKnowledge.syncPolicyColumn": "同步策略", + "newKnowledge.syncPolicyCustom": "自定义间隔", + "newKnowledge.syncPolicyDaily": "每 24 小时", + "newKnowledge.syncPolicyLoadFailed": "无法加载同步策略。", + "newKnowledge.syncPolicyManual": "仅手动", + "newKnowledge.syncPolicyProvider": "使用提供商计划", "newKnowledge.tags": "标签", "newKnowledge.unavailableDescription": "KnowledgeFS 当前在此工作区不可用。", "newKnowledge.unavailableTitle": "新版知识库不可用", "newKnowledge.updated": "更新于 {{date}}", + "newKnowledge.uploadCharactersUnavailable": "字符数:—", "newKnowledge.uploadFiles": "上传文件", "newKnowledge.uploadFilesDescription": "从电脑添加文件,创建后会立即建立索引。", + "newKnowledge.uploadingFiles": "正在上传…", "newKnowledge.usingDefaults": "使用默认设置", "newKnowledge.viewLabel": "知识库视图", "newKnowledge.websiteCrawl": "网站抓取", diff --git a/web/i18n/zh-Hant/dataset.json b/web/i18n/zh-Hant/dataset.json index 692dd1aa3c7..c70ca2fb615 100644 --- a/web/i18n/zh-Hant/dataset.json +++ b/web/i18n/zh-Hant/dataset.json @@ -134,6 +134,7 @@ "newKnowledge.actionsColumn": "行動", "newKnowledge.addSource": "新增來源", "newKnowledge.addSourceDescription": "將來源連接到該知識庫 - 它會自動同步並保持最新。", + "newKnowledge.addSourceFailed": "無法新增此來源。請檢查設定後再試一次。", "newKnowledge.addSourceRequiresSelection": "新增來源前,請至少選取一個已抓取的頁面。", "newKnowledge.adjustAndRecrawl": "調整並重新抓取", "newKnowledge.allSources": "所有來源", @@ -147,7 +148,9 @@ "newKnowledge.cancelAddSource": "取消", "newKnowledge.cardType": "知識空間", "newKnowledge.configureProvider": "配置{{provider}}", + "newKnowledge.connectNotion": "連接 Notion", "newKnowledge.connectProvider": "連接{{provider}}", + "newKnowledge.connectProviderGeneric": "連接提供者", "newKnowledge.connectSource": "連接資料來源", "newKnowledge.connectSourceDescription": "內容會與 Notion、網站及其他服務保持同步。", "newKnowledge.connectingProvider": "正在連接...", @@ -162,7 +165,9 @@ "newKnowledge.crawlFailedDescription": "抓取工具無法完成此預覽。請檢查 URL、調整抓取選項,或重試。", "newKnowledge.crawlFailedProvider": "{{provider}} 回報錯誤。請檢查供應商連線並重試。", "newKnowledge.crawlFailedTimeout": "抓取逾時。請嘗試範圍較小的 URL,或減少頁面數上限。", + "newKnowledge.crawlNotStarted": "尚未開始", "newKnowledge.crawlOptions": "抓取選項", + "newKnowledge.crawlPreview": "抓取預覽", "newKnowledge.crawlProgress": "{{host}} 的抓取進度", "newKnowledge.crawlSetupUnavailableDescription": "在爬網工作流程可用之前,網站爬網設定將會被停用。", "newKnowledge.crawlSetupUnavailableTitle": "Firecrawl 已準備就緒", @@ -171,8 +176,11 @@ "newKnowledge.crawling": "正在抓取…", "newKnowledge.crawlingPages": "正在抓取 {{host}}… 已找到 {{count}} 個頁面", "newKnowledge.createFailed": "無法建立此知識庫,請檢查權限後再試一次。", + "newKnowledge.createIllustrationTitle": "只需連接一次內容——每個應用都能以最新知識作答。", "newKnowledge.createTitle": "建立知識庫", "newKnowledge.creators": "建立者", + "newKnowledge.customIntervalHours": "自訂間隔(小時)", + "newKnowledge.customIntervalInvalid": "請輸入 1 到 720 之間的整數小時數。", "newKnowledge.description": "描述", "newKnowledge.descriptionHelp": "清楚的描述有助於應用程式判斷何時搜尋此知識庫。", "newKnowledge.descriptionPlaceholder": "例如:退款政策、配送常見問題和升級處理步驟——用於客戶支援問題", @@ -182,22 +190,28 @@ "newKnowledge.discardDraftConfirm": "放棄草稿", "newKnowledge.discardDraftDescription": "對此知識庫草稿的變更將會遺失。", "newKnowledge.discardDraftTitle": "放棄此知識庫草稿?", + "newKnowledge.discardSourceChanges": "放棄來源變更?", + "newKnowledge.discardSourceChangesConfirm": "放棄變更", + "newKnowledge.discardSourceChangesDescription": "檢索預覽、頁面選取和同步設定將會遺失。", "newKnowledge.discardSourceDraftDescription": "來源設定和抓取預覽將被移除。", "newKnowledge.discardSourceDraftFailed": "無法捨棄來源草稿。請重試。", "newKnowledge.discardSourceDraftTitle": "捨棄此來源草稿?", "newKnowledge.documents": "文件", "newKnowledge.documentsUnavailable": "— 份文件", + "newKnowledge.editSource": "編輯", "newKnowledge.emptyDescription": "加入文件後,智慧代理即可直接讀取,無需設定索引。", "newKnowledge.emptyTitle": "更聰明的知識庫建立方式", "newKnowledge.errorDescription": "無法載入新知識庫,請再試一次。", "newKnowledge.errorTitle": "發生錯誤", "newKnowledge.filtersUnavailable": "KnowledgeFS 提供中繼資料後即可使用此篩選器。", + "newKnowledge.firecrawlUnavailable": "Firecrawl 在此工作區的提供者目錄中不可用。", "newKnowledge.gotIt": "知道了", "newKnowledge.guideDescription": "連接資料來源後,其文件會保持同步、自動建立索引並可立即使用,無需管理任何設定。舊版知識庫仍會照常運作。", "newKnowledge.guideTitle": "認識全新的知識庫 ✨", "newKnowledge.illustrationHeadline": "只需連接一次內容,每個應用程式都能以最新知識回答。", "newKnowledge.includeSubpages": "包含子頁面", "newKnowledge.invalidRootUrl": "請輸入有效的 http(s) URL。", + "newKnowledge.keepEditing": "繼續編輯", "newKnowledge.lastSyncColumn": "上次同步", "newKnowledge.learnMore": "瞭解更多", "newKnowledge.leavePartialSetupConfirm": "離開設定", @@ -205,8 +219,9 @@ "newKnowledge.leavePartialSetupTitle": "離開未完成的設定?", "newKnowledge.legacy": "舊版", "newKnowledge.loadMore": "載入更多", + "newKnowledge.loadingSyncPolicy": "正在載入同步政策", "newKnowledge.maxPages": "頁面數上限", - "newKnowledge.moreProviders": "更多供應商", + "newKnowledge.moreProviders": "更多提供者", "newKnowledge.name": "知識庫名稱", "newKnowledge.namePlaceholder": "例如:客戶支援手冊", "newKnowledge.nameRequired": "請輸入知識庫名稱。", @@ -217,16 +232,23 @@ "newKnowledge.noPagesFoundDescription": "抓取工具已連線至網站,但找不到可建立索引的頁面。請嘗試其他 URL,或擴大抓取範圍。", "newKnowledge.notFoundDescription": "此知識庫可能已被刪除,或你沒有存取權限。", "newKnowledge.notFoundTitle": "找不到知識庫", + "newKnowledge.notionNotConnected": "Notion 尚未連接", + "newKnowledge.notionNotConnectedDescription": "連接 Notion 以選擇頁面並保持同步。", "newKnowledge.onlineDocuments": "線上文檔", "newKnowledge.onlineDrive": "線上驅動器", "newKnowledge.pagesAppearDescription": "抓取網站以查看將新增哪些頁面。", "newKnowledge.pagesAppearTitle": "頁面將顯示在此處", "newKnowledge.pagesCrawled": "已抓取 {{host}} 的 {{count}} 個頁面", + "newKnowledge.pagesCrawled_one": "已抓取 {{host}} 的 {{count}} 個頁面", + "newKnowledge.pagesCrawled_other": "已抓取 {{host}} 的 {{count}} 個頁面", + "newKnowledge.pagesSelected": "已選取 {{count}} 項", "newKnowledge.permission": "權限", "newKnowledge.permissionAllMembers": "工作區 · 所有成員皆可檢視和編輯", "newKnowledge.permissionOnlyMe": "僅自己", "newKnowledge.permissionRestricted": "你的工作區權限不允許變更此設定。", "newKnowledge.permissionUpdateFailed": "知識庫已建立,但無法確認其權限狀態。請重試以完成設定。", + "newKnowledge.preview": "預覽", + "newKnowledge.previewUnavailable": "後端完成此檔案的處理後即可預覽。", "newKnowledge.providerConnected": "{{provider}}已連接", "newKnowledge.providerLabel": "供應商", "newKnowledge.providerLoadFailed": "提供者無法載入", @@ -240,16 +262,23 @@ "newKnowledge.removeSource": "刪除來源", "newKnowledge.retryCrawl": "重試抓取", "newKnowledge.retryProviderLoad": "重試", + "newKnowledge.retrySyncPolicy": "重試同步政策", "newKnowledge.rootUrl": "根 URL", "newKnowledge.rootUrlPlaceholder": "https://docs.example.com", "newKnowledge.routeUnavailable": "此區段將在下一個設定步驟中開放。", "newKnowledge.searchSources": "搜尋來源", + "newKnowledge.selectAll": "全選", + "newKnowledge.selectAllSources": "選擇所有來源", + "newKnowledge.selectFilesAndFolders": "選擇要同步的檔案與資料夾", + "newKnowledge.skippedFailed": "已略過(失敗)", + "newKnowledge.skippedOffDomain": "已略過(網域外)", "newKnowledge.sourceActions": "{{name}} 的操作", "newKnowledge.sourceColumn": "來源", "newKnowledge.sourceFilterLabel": "按狀態過濾來源", "newKnowledge.sourceName": "來源名稱", "newKnowledge.sourceNamePlaceholder": "例如:產品文件抓取", "newKnowledge.sourceNameRequired": "請輸入來源名稱。", + "newKnowledge.sourceSetupBackendDependency": "來源設定已完整顯示;編輯與預覽操作需要草稿來源後端流程。", "newKnowledge.sourceStatus.active": "活躍", "newKnowledge.sourceStatus.disabled": "已停用", "newKnowledge.sourceStatus.error": "錯誤", @@ -274,13 +303,21 @@ "newKnowledge.stopCrawl": "停止", "newKnowledge.stoppingCrawl": "正在停止…", "newKnowledge.syncNow": "立即同步", + "newKnowledge.syncPolicy": "同步政策", "newKnowledge.syncPolicyColumn": "同步策略", + "newKnowledge.syncPolicyCustom": "自訂間隔", + "newKnowledge.syncPolicyDaily": "每 24 小時", + "newKnowledge.syncPolicyLoadFailed": "無法載入同步政策。", + "newKnowledge.syncPolicyManual": "僅限手動", + "newKnowledge.syncPolicyProvider": "使用供應商排程", "newKnowledge.tags": "標籤", "newKnowledge.unavailableDescription": "KnowledgeFS 目前在此工作區不可用。", "newKnowledge.unavailableTitle": "新版知識庫不可用", "newKnowledge.updated": "更新於 {{date}}", + "newKnowledge.uploadCharactersUnavailable": "字元數:—", "newKnowledge.uploadFiles": "上傳檔案", "newKnowledge.uploadFilesDescription": "從電腦加入檔案,建立後會立即建立索引。", + "newKnowledge.uploadingFiles": "正在上傳…", "newKnowledge.usingDefaults": "使用預設設定", "newKnowledge.viewLabel": "知識庫檢視", "newKnowledge.websiteCrawl": "網站抓取",