diff --git a/api/controllers/console/knowledge_fs_proxy.py b/api/controllers/console/knowledge_fs_proxy.py index f930c92a971..59ec6dafc62 100644 --- a/api/controllers/console/knowledge_fs_proxy.py +++ b/api/controllers/console/knowledge_fs_proxy.py @@ -287,7 +287,24 @@ def _proxy_knowledge_fs_non_get( @bp.route( "/knowledge-fs/", - methods=["GET", "OPTIONS"], + methods=["OPTIONS"], + provide_automatic_options=False, +) +@_console_api_errors +@_knowledge_fs_enabled +def proxy_knowledge_fs_options(upstream_path: str) -> ResponseReturnValue: + """Complete a CORS preflight only for an enabled Console operation.""" + requested_method = cast(KnowledgeFSMethod, request.headers.get("Access-Control-Request-Method", "").upper()) + try: + get_knowledge_fs_operation(requested_method, upstream_path) + except KnowledgeFSRouteNotAllowedError as exc: + raise NotFound() from exc + return Response(status=HTTPStatus.NO_CONTENT) + + +@bp.route( + "/knowledge-fs/", + methods=["GET"], provide_automatic_options=False, ) @_console_api_errors diff --git a/api/dev/generate_knowledge_fs_contract.py b/api/dev/generate_knowledge_fs_contract.py index cd4e7f9aef1..a753a2c4ba7 100644 --- a/api/dev/generate_knowledge_fs_contract.py +++ b/api/dev/generate_knowledge_fs_contract.py @@ -70,6 +70,7 @@ def main() -> None: mode.add_argument("--check", action="store_true") mode.add_argument("--update-lock", action="store_true") parser.add_argument("--repository", type=Path, default=DEFAULT_REPOSITORY) + parser.add_argument("--output-openapi", type=Path) args = parser.parse_args() repository = args.repository.resolve() @@ -101,7 +102,12 @@ def main() -> None: ) document: dict[str, Any] = json.loads(openapi_content) - validate_declarations(document, console_contract_declarations()) + declarations = console_contract_declarations() + validate_declarations(document, declarations) + + if args.output_openapi: + args.output_openapi.parent.mkdir(parents=True, exist_ok=True) + args.output_openapi.write_text(json.dumps(filter_openapi_document(document, declarations), indent=2) + "\n") if args.update_lock: LOCK_PATH.write_text( @@ -168,6 +174,62 @@ def validate_declarations(document: dict[str, Any], declarations: tuple[Contract ) +def filter_openapi_document( + document: dict[str, Any], + declarations: tuple[ContractDeclaration, ...], +) -> dict[str, Any]: + """Return a code-generation document containing only Console-allowlisted operations.""" + filtered_document: dict[str, Any] = { + key: value for key, value in document.items() if key not in {"components", "paths"} + } + source_paths = document.get("paths", {}) + filtered_paths: dict[str, Any] = {} + + for declaration in declarations: + path = f"/{declaration['path']}" + method = declaration["method"].lower() + source_path_item = source_paths[path] + path_metadata = {key: value for key, value in source_path_item.items() if key not in OPENAPI_METHODS} + filtered_path_item = filtered_paths.setdefault(path, path_metadata) + filtered_path_item[method] = source_path_item[method] + + filtered_document["paths"] = filtered_paths + + source_components = document.get("components", {}) + filtered_components = {key: value for key, value in source_components.items() if key != "schemas"} + source_schemas = source_components.get("schemas", {}) + schema_names = _referenced_schema_names(filtered_paths, source_schemas) + filtered_components["schemas"] = {name: schema for name, schema in source_schemas.items() if name in schema_names} + filtered_document["components"] = filtered_components + return filtered_document + + +def _referenced_schema_names(value: Any, schemas: dict[str, Any]) -> set[str]: + reference_prefix = "#/components/schemas/" + selected: set[str] = set() + pending: list[Any] = [value] + + while pending: + current = pending.pop() + if isinstance(current, list): + pending.extend(current) + continue + if not isinstance(current, dict): + continue + + reference = current.get("$ref") + if isinstance(reference, str) and reference.startswith(reference_prefix): + name = reference.removeprefix(reference_prefix) + if name not in selected: + if name not in schemas: + raise ValueError(f"KnowledgeFS OpenAPI references missing schema: {name}") + selected.add(name) + pending.append(schemas[name]) + pending.extend(current.values()) + + return selected + + def console_contract_declarations() -> tuple[ContractDeclaration, ...]: """Return transport declarations from the runtime Console operation registry.""" from services.knowledge_fs_proxy import KNOWLEDGE_FS_CONSOLE_OPERATIONS diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index a5bb29e1917..02a20e95504 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -22036,6 +22036,7 @@ Model class for provider system configuration response. | is_allow_create_workspace | boolean | | Yes | | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | +| knowledge_fs_enabled | boolean | | Yes | | license | [LicenseModel](#licensemodel) | | Yes | | max_plugin_package_size | integer,
**Default:** 15728640 | | Yes | | plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes | diff --git a/api/openapi/markdown/web-openapi.md b/api/openapi/markdown/web-openapi.md index 44296235014..e196aa2eb81 100644 --- a/api/openapi/markdown/web-openapi.md +++ b/api/openapi/markdown/web-openapi.md @@ -1582,6 +1582,7 @@ Default configuration for form inputs. | is_allow_create_workspace | boolean | | Yes | | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | +| knowledge_fs_enabled | boolean | | Yes | | license | [LicenseModel](#licensemodel) | | Yes | | max_plugin_package_size | integer,
**Default:** 15728640 | | Yes | | plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes | diff --git a/api/services/feature_service.py b/api/services/feature_service.py index de72792f4d5..b23638bba20 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -186,6 +186,7 @@ class SystemFeatureModel(FeatureResponseModel): enable_learn_app: bool = True enable_step_by_step_tour: bool = False rbac_enabled: bool = False + knowledge_fs_enabled: bool = False class FeatureService: @@ -289,6 +290,7 @@ class FeatureService: system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED system_features.enable_step_by_step_tour = dify_config.ENABLE_STEP_BY_STEP_TOUR + system_features.knowledge_fs_enabled = dify_config.KNOWLEDGE_FS_ENABLED @classmethod def _fulfill_trial_models_from_env(cls) -> list[str]: diff --git a/api/tests/unit_tests/controllers/console/test_knowledge_fs_proxy.py b/api/tests/unit_tests/controllers/console/test_knowledge_fs_proxy.py index 8edcc033593..41b97e2f028 100644 --- a/api/tests/unit_tests/controllers/console/test_knowledge_fs_proxy.py +++ b/api/tests/unit_tests/controllers/console/test_knowledge_fs_proxy.py @@ -22,6 +22,7 @@ from controllers.console.knowledge_fs_proxy import ( _proxy_request, _proxy_response, proxy_knowledge_fs_get, + proxy_knowledge_fs_options, proxy_knowledge_fs_write, ) from controllers.console.wraps import RBACPermission @@ -118,10 +119,61 @@ def test_console_blueprint_registers_generic_knowledge_fs_routes() -> None: "/console/api/knowledge-fs/knowledge-spaces", method="OPTIONS", ) - assert options_endpoint.endswith("proxy_knowledge_fs_get") + assert options_endpoint.endswith("proxy_knowledge_fs_options") assert options_values == {"upstream_path": "knowledge-spaces"} +def test_proxy_options_does_not_require_an_authenticated_account(app: Flask) -> None: + with app.test_request_context( + "/console/api/knowledge-fs/knowledge-spaces", + method="OPTIONS", + headers={"Access-Control-Request-Method": "GET"}, + ): + response = app.make_response(proxy_knowledge_fs_options("knowledge-spaces")) + + assert response.status_code == 204 + + +def test_proxy_options_is_hidden_when_knowledge_fs_is_disabled( + app: Flask, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("controllers.console.knowledge_fs_proxy.dify_config.KNOWLEDGE_FS_ENABLED", False) + + with app.test_request_context( + "/console/api/knowledge-fs/knowledge-spaces", + method="OPTIONS", + headers={"Access-Control-Request-Method": "GET"}, + ): + response = app.make_response(proxy_knowledge_fs_options("knowledge-spaces")) + + assert response.status_code == 404 + + +@pytest.mark.parametrize( + ("upstream_path", "requested_method"), + [ + ("unregistered", "GET"), + ("knowledge-spaces", "DELETE"), + ("knowledge-spaces", ""), + ], +) +def test_proxy_options_hides_unregistered_operations( + app: Flask, + upstream_path: str, + requested_method: str, +) -> None: + headers = {"Access-Control-Request-Method": requested_method} if requested_method else None + with app.test_request_context( + f"/console/api/knowledge-fs/{upstream_path}", + method="OPTIONS", + headers=headers, + ): + response = app.make_response(proxy_knowledge_fs_options(upstream_path)) + + assert response.status_code == 404 + + def test_proxy_is_hidden_when_knowledge_fs_is_disabled(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("controllers.console.knowledge_fs_proxy.dify_config.KNOWLEDGE_FS_ENABLED", False) diff --git a/api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py b/api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py index 10dfabbd90a..007e88eb672 100644 --- a/api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py +++ b/api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py @@ -10,7 +10,11 @@ from typing import cast import pytest from dev import generate_knowledge_fs_contract as contract_validator -from dev.generate_knowledge_fs_contract import ContractDeclaration, validate_declarations +from dev.generate_knowledge_fs_contract import ( + ContractDeclaration, + filter_openapi_document, + validate_declarations, +) from services.knowledge_fs_proxy import KNOWLEDGE_FS_CONSOLE_OPERATIONS, KnowledgeFSOperation @@ -55,7 +59,8 @@ def test_contract_cli_updates_checks_and_detects_openapi_drift(tmp_path: Path, m ) ) monkeypatch.setattr(contract_validator, "LOCK_PATH", lock_path) - monkeypatch.setenv("PATH", f"{executable_directory}{os.pathsep}{os.environ['PATH']}") + current_path = os.environ.get("PATH", os.defpath) + monkeypatch.setenv("PATH", f"{executable_directory}{os.pathsep}{current_path}") monkeypatch.setattr( sys, @@ -131,6 +136,52 @@ def test_validate_declarations_accepts_matching_contract() -> None: ) +def test_filter_openapi_document_keeps_only_declared_operations_and_referenced_schemas() -> None: + list_route = operation("knowledge-spaces:read", "listKnowledgeSpaces") + list_route["responses"] = { + "200": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/KnowledgeSpaceList"}, + } + } + } + } + document = { + "openapi": "3.1.0", + "paths": { + "/knowledge-spaces": { + "get": list_route, + "post": operation("knowledge-spaces:write", "createKnowledgeSpace"), + }, + "/health": {"get": operation(None, "getHealth", security=[])}, + }, + "components": { + "schemas": { + "KnowledgeSpaceList": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": {"$ref": "#/components/schemas/KnowledgeSpace"}, + } + }, + }, + "KnowledgeSpace": {"type": "object"}, + "Unused": {"type": "object"}, + }, + "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}, + }, + } + + filtered = filter_openapi_document(document, (declaration(),)) + + assert set(filtered["paths"]) == {"/knowledge-spaces"} + assert set(filtered["paths"]["/knowledge-spaces"]) == {"get"} + assert set(filtered["components"]["schemas"]) == {"KnowledgeSpaceList", "KnowledgeSpace"} + assert filtered["components"]["securitySchemes"] == document["components"]["securitySchemes"] + + def test_console_operation_registry_matches_contract() -> None: list_route = operation("knowledge-spaces:read", "listKnowledgeSpaces") create_route = operation("knowledge-spaces:write", "createKnowledgeSpace") diff --git a/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py b/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py new file mode 100644 index 00000000000..63bfc739659 --- /dev/null +++ b/api/tests/unit_tests/services/test_feature_service_knowledge_fs.py @@ -0,0 +1,19 @@ +import pytest + +from services.feature_service import FeatureService, SystemFeatureModel + + +def test_system_feature_model_disables_knowledge_fs_by_default() -> None: + assert SystemFeatureModel().knowledge_fs_enabled is False + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_get_system_features_reads_knowledge_fs_flag( + monkeypatch: pytest.MonkeyPatch, + enabled: bool, +) -> None: + monkeypatch.setattr("services.feature_service.dify_config.KNOWLEDGE_FS_ENABLED", enabled) + + result = FeatureService.get_system_features() + + assert result.knowledge_fs_enabled is enabled diff --git a/packages/contracts/console.ts b/packages/contracts/console.ts new file mode 100644 index 00000000000..af4c1f89f3e --- /dev/null +++ b/packages/contracts/console.ts @@ -0,0 +1,7 @@ +import { consoleRouterContract as generatedConsoleRouterContract } from './generated/api/console/router.gen' +import { contract as knowledgeFsContract } from './generated/knowledge-fs/orpc.gen' + +export const consoleRouterContract = { + ...generatedConsoleRouterContract, + knowledgeFs: knowledgeFsContract, +} diff --git a/packages/contracts/generated/api/console/system-features/types.gen.ts b/packages/contracts/generated/api/console/system-features/types.gen.ts index 0417727f5ba..65652a60498 100644 --- a/packages/contracts/generated/api/console/system-features/types.gen.ts +++ b/packages/contracts/generated/api/console/system-features/types.gen.ts @@ -21,6 +21,7 @@ export type SystemFeatureModel = { is_allow_create_workspace: boolean is_allow_register: boolean is_email_setup: boolean + knowledge_fs_enabled: boolean license: LicenseModel max_plugin_package_size: number plugin_installation_permission: PluginInstallationPermissionModel diff --git a/packages/contracts/generated/api/console/system-features/zod.gen.ts b/packages/contracts/generated/api/console/system-features/zod.gen.ts index 3ce9d68841e..8d2aa34ee22 100644 --- a/packages/contracts/generated/api/console/system-features/zod.gen.ts +++ b/packages/contracts/generated/api/console/system-features/zod.gen.ts @@ -119,6 +119,7 @@ export const zSystemFeatureModel = z.object({ is_allow_create_workspace: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), + knowledge_fs_enabled: z.boolean().default(false), license: zLicenseModel.default({ expired_at: '', seats: { diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index 607386d387d..f98ac9a775d 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -525,6 +525,7 @@ export type SystemFeatureModel = { is_allow_create_workspace: boolean is_allow_register: boolean is_email_setup: boolean + knowledge_fs_enabled: boolean license: LicenseModel max_plugin_package_size: number plugin_installation_permission: PluginInstallationPermissionModel diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index 4dec23a2fe5..36ed9501a97 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -797,6 +797,7 @@ export const zSystemFeatureModel = z.object({ is_allow_create_workspace: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), + knowledge_fs_enabled: z.boolean().default(false), license: zLicenseModel.default({ expired_at: '', seats: { diff --git a/packages/contracts/generated/knowledge-fs/orpc.gen.ts b/packages/contracts/generated/knowledge-fs/orpc.gen.ts new file mode 100644 index 00000000000..ac9a0c47f88 --- /dev/null +++ b/packages/contracts/generated/knowledge-fs/orpc.gen.ts @@ -0,0 +1,47 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { oc } from '@orpc/contract' +import * as z from 'zod' +import { + zCreateKnowledgeSpaceBody, + zCreateKnowledgeSpaceHeaders, + zCreateKnowledgeSpaceResponse, + zListKnowledgeSpacesHeaders, + zListKnowledgeSpacesQuery, + zListKnowledgeSpacesResponse, +} from './zod.gen' + +export const listKnowledgeSpaces = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'listKnowledgeSpaces', + path: '/knowledge-fs/knowledge-spaces', + tags: ['Knowledge Spaces'], + }) + .input( + z.object({ + headers: zListKnowledgeSpacesHeaders.optional(), + query: zListKnowledgeSpacesQuery.optional(), + }), + ) + .output(zListKnowledgeSpacesResponse) + +export const createKnowledgeSpace = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'createKnowledgeSpace', + path: '/knowledge-fs/knowledge-spaces', + successStatus: 201, + tags: ['Knowledge Spaces'], + }) + .input( + z.object({ body: zCreateKnowledgeSpaceBody, headers: zCreateKnowledgeSpaceHeaders.optional() }), + ) + .output(zCreateKnowledgeSpaceResponse) + +export const contract = { + listKnowledgeSpaces, + createKnowledgeSpace, +} diff --git a/packages/contracts/generated/knowledge-fs/types.gen.ts b/packages/contracts/generated/knowledge-fs/types.gen.ts new file mode 100644 index 00000000000..d0b1886e100 --- /dev/null +++ b/packages/contracts/generated/knowledge-fs/types.gen.ts @@ -0,0 +1,138 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}) +} + +export type KnowledgeSpaceCreationResponse = { + createdAt: string + description?: string + iconRef?: string + id: string + name: string + revision: number + slug: string + tenantId: string + updatedAt: string + configurationStatus: 'pending-validation' | 'ready' | 'setup-required' | 'validation-failed' +} + +export type ErrorResponse = { + code?: string + error: string +} + +export type CreateKnowledgeSpace = { + description?: string + embeddingProfile?: { + model: string + pluginId: string + provider: string + } + iconRef?: string + idempotencyKey?: string + name: string + retrievalProfile?: { + defaultMode: 'fast' | 'research' | 'deep' + reasoningModel: { + model: string + pluginId: string + provider: string + } + rerank: { + enabled: boolean + model?: { + model: string + pluginId: string + provider: string + } + } + scoreThreshold: { + enabled: boolean + stage: 'mode-final' | 'rerank' + value?: number + } + topK: number + } + slug?: string +} + +export type KnowledgeSpace = { + createdAt: string + description?: string + iconRef?: string + id: string + name: string + revision: number + slug: string + tenantId: string + updatedAt: string +} + +export type KnowledgeSpaceList = { + items: Array + nextCursor?: string +} + +export type ListKnowledgeSpacesData = { + body?: never + headers?: { + 'x-trace-id'?: string + } + path?: never + query?: { + cursor?: string + limit?: number + } + url: '/knowledge-fs/knowledge-spaces' +} + +export type ListKnowledgeSpacesErrors = { + 400: ErrorResponse + 401: ErrorResponse + 403: ErrorResponse +} + +export type ListKnowledgeSpacesError = ListKnowledgeSpacesErrors[keyof ListKnowledgeSpacesErrors] + +export type ListKnowledgeSpacesResponses = { + 200: KnowledgeSpaceList +} + +export type ListKnowledgeSpacesResponse = + ListKnowledgeSpacesResponses[keyof ListKnowledgeSpacesResponses] + +export type CreateKnowledgeSpaceData = { + body: CreateKnowledgeSpace + headers?: { + 'x-trace-id'?: string + } + path?: never + query?: never + url: '/knowledge-fs/knowledge-spaces' +} + +export type CreateKnowledgeSpaceErrors = { + 400: + | { + code: 'RETRIEVAL_PROFILE_SCORE_THRESHOLD_REQUIRES_RERANK' + error: 'Fast/Deep mode-final score threshold requires the knowledge-space reranker to be enabled' + mode: 'fast' | 'research' | 'deep' + } + | ErrorResponse + 401: ErrorResponse + 403: ErrorResponse + 409: ErrorResponse + 422: ErrorResponse + 429: ErrorResponse + 503: ErrorResponse +} + +export type CreateKnowledgeSpaceError = CreateKnowledgeSpaceErrors[keyof CreateKnowledgeSpaceErrors] + +export type CreateKnowledgeSpaceResponses = { + 201: KnowledgeSpaceCreationResponse +} + +export type CreateKnowledgeSpaceResponse = + CreateKnowledgeSpaceResponses[keyof CreateKnowledgeSpaceResponses] diff --git a/packages/contracts/generated/knowledge-fs/zod.gen.ts b/packages/contracts/generated/knowledge-fs/zod.gen.ts new file mode 100644 index 00000000000..776479ad6e9 --- /dev/null +++ b/packages/contracts/generated/knowledge-fs/zod.gen.ts @@ -0,0 +1,131 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod' + +export const zKnowledgeSpaceCreationResponse = z.object({ + createdAt: z.iso.datetime(), + description: z.string().max(2000).optional(), + iconRef: z + .string() + .max(72) + .regex(/^builtin:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/) + .optional(), + id: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), + name: z.string().min(1).max(160), + revision: z.int().gt(0), + slug: z + .string() + .max(160) + .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), + tenantId: z.string().min(1).max(255), + updatedAt: z.iso.datetime(), + configurationStatus: z.enum([ + 'pending-validation', + 'ready', + 'setup-required', + 'validation-failed', + ]), +}) + +export const zErrorResponse = z.object({ + code: z.string().optional(), + error: z.string(), +}) + +export const zCreateKnowledgeSpace = z.object({ + description: z.string().max(2000).optional(), + embeddingProfile: z + .object({ + model: z.string().min(1).max(256), + pluginId: z.string().min(1).max(256), + provider: z.string().min(1).max(256), + }) + .optional(), + iconRef: z + .string() + .max(72) + .regex(/^builtin:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/) + .optional(), + idempotencyKey: z.string().min(1).max(255).optional(), + name: z.string().min(1).max(160), + retrievalProfile: z + .object({ + defaultMode: z.enum(['fast', 'research', 'deep']), + reasoningModel: z.object({ + model: z.string().min(1).max(256), + pluginId: z.string().min(1).max(256), + provider: z.string().min(1).max(256), + }), + rerank: z.object({ + enabled: z.boolean(), + model: z + .object({ + model: z.string().min(1).max(256), + pluginId: z.string().min(1).max(256), + provider: z.string().min(1).max(256), + }) + .optional(), + }), + scoreThreshold: z.object({ + enabled: z.boolean(), + stage: z.enum(['mode-final', 'rerank']), + value: z.number().gte(0).lte(1).optional(), + }), + topK: z.int().gte(1).lte(100), + }) + .optional(), + slug: z + .string() + .max(160) + .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) + .optional(), +}) + +export const zKnowledgeSpace = z.object({ + createdAt: z.iso.datetime(), + description: z.string().max(2000).optional(), + iconRef: z + .string() + .max(72) + .regex(/^builtin:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/) + .optional(), + id: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), + name: z.string().min(1).max(160), + revision: z.int().gt(0), + slug: z + .string() + .max(160) + .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), + tenantId: z.string().min(1).max(255), + updatedAt: z.iso.datetime(), +}) + +export const zKnowledgeSpaceList = z.object({ + items: z.array(zKnowledgeSpace), + nextCursor: z.string().optional(), +}) + +export const zListKnowledgeSpacesHeaders = z.object({ + 'x-trace-id': z.string().optional(), +}) + +export const zListKnowledgeSpacesQuery = z.object({ + cursor: z.string().optional(), + limit: z.int().gte(1).lte(100).optional().default(100), +}) + +/** + * Tenant knowledge spaces + */ +export const zListKnowledgeSpacesResponse = zKnowledgeSpaceList + +export const zCreateKnowledgeSpaceBody = zCreateKnowledgeSpace + +export const zCreateKnowledgeSpaceHeaders = z.object({ + 'x-trace-id': z.string().optional(), +}) + +/** + * Created knowledge space + */ +export const zCreateKnowledgeSpaceResponse = zKnowledgeSpaceCreationResponse diff --git a/packages/contracts/openapi-ts.knowledge-fs.config.ts b/packages/contracts/openapi-ts.knowledge-fs.config.ts new file mode 100644 index 00000000000..603267c39d1 --- /dev/null +++ b/packages/contracts/openapi-ts.knowledge-fs.config.ts @@ -0,0 +1,50 @@ +import { defineConfig } from '@hey-api/openapi-ts' + +const input = process.env.KNOWLEDGE_FS_OPENAPI +const outputPath = process.env.KNOWLEDGE_FS_OUTPUT ?? 'generated/knowledge-fs' + +if (!input) throw new Error('KNOWLEDGE_FS_OPENAPI must point to the filtered pinned export') + +export default defineConfig({ + input, + logs: { + file: false, + }, + output: { + clean: true, + entryFile: false, + fileName: { + suffix: '.gen', + }, + path: outputPath, + }, + parser: { + patch: { + input: (spec) => { + const paths = spec.paths as Record | undefined + if (!paths) return + + for (const [path, pathItem] of Object.entries(paths)) { + delete paths[path] + paths[`/knowledge-fs${path}`] = pathItem + } + }, + }, + }, + plugins: [ + { + comments: false, + name: '@hey-api/typescript', + }, + { + name: 'zod', + }, + { + contracts: { + strategy: 'single', + }, + name: 'orpc', + validator: 'zod', + }, + ], +}) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 4f900c881e5..ac0c28e7911 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,6 +8,10 @@ "types": "./marketplace.ts", "import": "./marketplace.ts" }, + "./console": { + "types": "./console.ts", + "import": "./console.ts" + }, "./api/*": { "types": "./generated/api/*.ts", "import": "./generated/api/*.ts" @@ -15,11 +19,16 @@ "./enterprise/*": { "types": "./generated/enterprise/*.ts", "import": "./generated/enterprise/*.ts" + }, + "./knowledge-fs/*": { + "types": "./generated/knowledge-fs/*.ts", + "import": "./generated/knowledge-fs/*.ts" } }, "scripts": { "gen-api-contract": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi && uv run --project ../../api ../../api/dev/generate_fastopenapi_specs.py --output-dir openapi && node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api", "gen-enterprise-contract": "openapi-ts -f openapi-ts.enterprise.config.ts", + "gen-knowledge-fs-contract": "node scripts/generate-knowledge-fs-contract.mjs", "test": "vp test", "type-check": "tsc" }, diff --git a/packages/contracts/scripts/generate-knowledge-fs-contract.mjs b/packages/contracts/scripts/generate-knowledge-fs-contract.mjs new file mode 100644 index 00000000000..c23e133b384 --- /dev/null +++ b/packages/contracts/scripts/generate-knowledge-fs-contract.mjs @@ -0,0 +1,45 @@ +import { execFileSync } from 'node:child_process' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const workspaceRoot = resolve(packageRoot, '../..') +const repository = resolve( + process.env.KNOWLEDGE_FS_REPO ?? resolve(workspaceRoot, '../knowledge-fs'), +) +const temporaryDirectory = await mkdtemp(join(tmpdir(), 'dify-knowledge-fs-types-')) + +try { + const openapiPath = join(temporaryDirectory, 'knowledge-fs.console.json') + run( + 'uv', + [ + 'run', + '--project', + resolve(workspaceRoot, 'api'), + resolve(workspaceRoot, 'api/dev/generate_knowledge_fs_contract.py'), + '--repository', + repository, + '--check', + '--output-openapi', + openapiPath, + ], + workspaceRoot, + ) + run('pnpm', ['exec', 'openapi-ts', '-f', 'openapi-ts.knowledge-fs.config.ts'], packageRoot, { + KNOWLEDGE_FS_OPENAPI: openapiPath, + }) + run('pnpm', ['exec', 'vp', 'fmt', 'generated/knowledge-fs'], packageRoot) +} finally { + await rm(temporaryDirectory, { force: true, recursive: true }) +} + +function run(command, args, cwd, extraEnv = {}) { + execFileSync(command, args, { + cwd, + env: { ...process.env, ...extraEnv }, + stdio: 'inherit', + }) +} diff --git a/web/.env.example b/web/.env.example index f33b2912a4b..794fcdf9752 100644 --- a/web/.env.example +++ b/web/.env.example @@ -122,3 +122,4 @@ NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED=true NEXT_PUBLIC_ENABLE_TRIAL_APP=true NEXT_PUBLIC_ENABLE_EXPLORE_BANNER=true NEXT_PUBLIC_RBAC_ENABLED=false +NEXT_PUBLIC_KNOWLEDGE_FS_ENABLED=false diff --git a/web/app/components/apps/first-empty-state/action-card.tsx b/web/app/components/apps/first-empty-state/action-card.tsx index 71907e878db..7f93055476b 100644 --- a/web/app/components/apps/first-empty-state/action-card.tsx +++ b/web/app/components/apps/first-empty-state/action-card.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { useId } from 'react' import CornerLabel from '@/app/components/base/corner-label' import Link from '@/next/link' @@ -13,19 +14,35 @@ type BaseProps = { icon: ReactNode title: string visualStyle?: VisualStyle + className?: string } type ButtonActionCardProps = BaseProps & { + disabled?: false + disabledReason?: never href?: never onClick: () => void } +type DisabledActionCardProps = BaseProps & { + disabled: true + disabledReason: string + href?: never + onClick?: never + visualStyle: 'list' +} + type LinkActionCardProps = BaseProps & { + disabled?: never + disabledReason?: never href: string onClick?: never } -type FirstEmptyActionCardProps = ButtonActionCardProps | LinkActionCardProps +type FirstEmptyActionCardProps = + | ButtonActionCardProps + | DisabledActionCardProps + | LinkActionCardProps const baseCardClassName = 'relative flex rounded-xl bg-components-button-secondary-bg text-left shadow-xs transition-colors hover:bg-components-panel-on-panel-item-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden' @@ -67,7 +84,9 @@ function ActionCardContent({ icon, title, visualStyle = 'default', -}: BaseProps) { + disabled = false, + disabledReason, +}: BaseProps & { disabled?: boolean; disabledReason?: string }) { if (visualStyle === 'list') { return ( <> @@ -80,17 +99,39 @@ function ActionCardContent({ textClassName="text-util-colors-indigo-indigo-700" /> )} - + {icon} - + {title} - + {description} + {disabledReason && ( + + {disabledReason} + + )} ) } @@ -131,6 +172,8 @@ function ActionCardContent({ } function FirstEmptyActionCard(props: FirstEmptyActionCardProps) { + const descriptionId = useId() + const isDisabled = props.disabled === true const className = cn( baseCardClassName, props.visualStyle === 'list' @@ -141,6 +184,9 @@ function FirstEmptyActionCard(props: FirstEmptyActionCardProps) { : props.visualStyle === 'list' ? undefined : 'min-h-[204px] p-6', + isDisabled && + 'cursor-not-allowed text-text-disabled shadow-none hover:bg-components-button-secondary-bg', + props.className, ) if (props.href) { @@ -165,9 +211,12 @@ function FirstEmptyActionCard(props: FirstEmptyActionCardProps) { return ( ) } diff --git a/web/app/components/base/infotip/index.tsx b/web/app/components/base/infotip/index.tsx index b19174a9392..64f303f0d6e 100644 --- a/web/app/components/base/infotip/index.tsx +++ b/web/app/components/base/infotip/index.tsx @@ -1,5 +1,6 @@ 'use client' +import type { Placement } from '@langgenius/dify-ui/popover' import type { MouseEvent, ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' @@ -55,6 +56,10 @@ type InfotipProps = { iconSize?: InfotipIconSize /** Extra classes on the popup body (width / padding / whitespace overrides). */ popupClassName?: string + /** Popup placement. Defaults to `top`. */ + placement?: Placement + /** Distance between the trigger and popup. */ + sideOffset?: number } export function Infotip({ @@ -64,6 +69,8 @@ export function Infotip({ iconVariant = 'question', iconSize = 'medium', popupClassName, + placement = 'top', + sideOffset, }: InfotipProps) { const handleClick = (event: MouseEvent) => { event.stopPropagation() @@ -88,7 +95,8 @@ export function Infotip({ /> vi.fn(() => ({}))) +const useInfiniteQueryMock = vi.hoisted(() => + vi.fn(() => ({ + data: { pageParams: [null], pages: [{ items: [] }] }, + error: null, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetchingNextPage: false, + isFetchNextPageError: false, + isPending: false, + refetch: vi.fn(), + })), +) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + useInfiniteQuery: useInfiniteQueryMock, + } +}) + +vi.mock('@/service/client', () => ({ + consoleQuery: { + knowledgeFs: { + listKnowledgeSpaces: { + infiniteOptions: knowledgeFsInfiniteOptionsMock, + }, + }, + }, +})) + +function NewKnowledgeGuideDismissedProbe() { + const dismissed = useNewKnowledgeGuideDismissedValue() + + return {String(dismissed)} +} + const mockPush = vi.fn() const mockReplace = vi.fn() let mockConsoleState = { isCurrentWorkspaceEditor: true, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: true, + knowledgeFsEnabled: false, workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'], } vi.mock('@/next/navigation', () => ({ @@ -175,10 +221,12 @@ vi.mock('@/app/components/datasets/create/website/base/checkbox-with-label', () describe('List', () => { beforeEach(async () => { vi.clearAllMocks() + localStorage.clear() mockConsoleState = { isCurrentWorkspaceEditor: true, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: true, + knowledgeFsEnabled: false, workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'], } const { useDatasetList } = await import('@/service/knowledge/use-dataset') @@ -207,11 +255,135 @@ describe('List', () => { expect(screen.getByText(/externalAPIPanelTitle/)).toBeInTheDocument() }) + it('should show the Legacy and New views when KnowledgeFS is enabled', () => { + mockConsoleState.knowledgeFsEnabled = true + + renderWithNuqs() + + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.legacy' }), + ).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.new' })).toBeInTheDocument() + }) + + it('should keep the legacy query active without requesting KnowledgeFS when disabled', async () => { + renderWithNuqs(, { searchParams: '?view=new' }) + + expect( + screen.queryByRole('button', { name: 'dataset.newKnowledge.new' }), + ).not.toBeInTheDocument() + expect( + screen.queryByRole('region', { name: 'dataset.newKnowledge.new' }), + ).not.toBeInTheDocument() + expect(screen.getByTestId('datasets-component')).toBeInTheDocument() + expect(knowledgeFsInfiniteOptionsMock).not.toHaveBeenCalled() + expect(useInfiniteQueryMock).not.toHaveBeenCalled() + + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + expect(useDatasetList).toHaveBeenCalled() + }) + + it('should switch to New Knowledge and persist the selected view in the URL', async () => { + const user = userEvent.setup() + mockConsoleState.knowledgeFsEnabled = true + const { onUrlUpdate } = renderWithNuqs() + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.new' })) + + expect( + await screen.findByRole('region', { name: 'dataset.newKnowledge.new' }), + ).toBeInTheDocument() + expect(screen.queryByTestId('datasets-component')).not.toBeInTheDocument() + await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) + expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('view')).toBe('new') + }) + + it('should restore the New Knowledge view from the URL', () => { + mockConsoleState.knowledgeFsEnabled = true + + renderWithNuqs(, { searchParams: '?view=new' }) + + expect(screen.getByRole('region', { name: 'dataset.newKnowledge.new' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'dataset.newKnowledge.new' })).toHaveAttribute( + 'aria-pressed', + 'true', + ) + }) + + it('should show the first-visit guide once and remember dismissal', async () => { + const user = userEvent.setup() + mockConsoleState.knowledgeFsEnabled = true + const firstRender = renderWithNuqs() + + const guide = await screen.findByRole('dialog', { + name: 'dataset.newKnowledge.guideTitle', + }) + await user.click(within(guide).getByRole('button', { name: 'dataset.newKnowledge.gotIt' })) + firstRender.unmount() + + renderWithNuqs() + + expect( + screen.queryByRole('dialog', { name: 'dataset.newKnowledge.guideTitle' }), + ).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.guideTitle' })) + expect( + await screen.findByRole('dialog', { name: 'dataset.newKnowledge.guideTitle' }), + ).toBeInTheDocument() + }) + + it('should keep a dismissed guide closed after hydrating a full page reload', async () => { + const user = userEvent.setup() + mockConsoleState.knowledgeFsEnabled = true + const firstRender = renderWithNuqs() + const guide = await screen.findByRole('dialog', { + name: 'dataset.newKnowledge.guideTitle', + }) + await user.click(within(guide).getByRole('button', { name: 'dataset.newKnowledge.gotIt' })) + firstRender.unmount() + + const store = createStore() + seedRegisteredConsoleStateFixture(store) + const { wrapper: NuqsWrapper } = createNuqsTestWrapper() + const app = ( + + + <> + + + + + + ) + const container = document.createElement('div') + document.body.append(container) + container.innerHTML = renderToString(app) + const root = hydrateRoot(container, app) + + try { + await waitFor(() => { + expect( + screen.getByRole('status', { name: 'new knowledge guide dismissed' }), + ).toHaveTextContent('true') + }) + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'dataset.newKnowledge.guideTitle' }), + ).toHaveAttribute('aria-expanded', 'false') + }) + } finally { + act(() => root.unmount()) + container.remove() + } + }) + it('should hide external API panel button without dataset.external.connect', () => { mockConsoleState = { isCurrentWorkspaceEditor: true, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: true, + knowledgeFsEnabled: false, workspacePermissionKeys: ['dataset.create_and_management'], } @@ -323,6 +495,7 @@ describe('List', () => { isCurrentWorkspaceEditor: false, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: true, + knowledgeFsEnabled: false, workspacePermissionKeys: ['dataset.create_and_management'], } const { useDatasetList } = await import('@/service/knowledge/use-dataset') @@ -347,6 +520,7 @@ describe('List', () => { isCurrentWorkspaceEditor: true, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: true, + knowledgeFsEnabled: false, workspacePermissionKeys: [], } const { useDatasetList } = await import('@/service/knowledge/use-dataset') @@ -461,6 +635,7 @@ describe('List', () => { isCurrentWorkspaceEditor: true, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: true, + knowledgeFsEnabled: false, workspacePermissionKeys: ['dataset.create_and_management'], } vi.doMock('@/context/external-api-panel-context', () => ({ @@ -509,6 +684,7 @@ describe('List', () => { isCurrentWorkspaceEditor: true, isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: false, + knowledgeFsEnabled: false, workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'], } diff --git a/web/app/components/datasets/list/header.tsx b/web/app/components/datasets/list/header.tsx index 582554ba2c6..2d4a89e4336 100644 --- a/web/app/components/datasets/list/header.tsx +++ b/web/app/components/datasets/list/header.tsx @@ -1,5 +1,6 @@ 'use client' +import type { KnowledgeViewSwitcherProps } from '@/features/new-rag/components/knowledge-view-switcher' import { Button } from '@langgenius/dify-ui/button' import { DropdownMenu, @@ -15,6 +16,7 @@ import { getStepByStepTourDropdownMenuContentProps, useStepByStepTourControlledDropdown, } from '@/app/components/step-by-step-tour/dropdown-menu' +import { KnowledgeViewSwitcher } from '@/features/new-rag/components/knowledge-view-switcher' import { TagFilter } from '@/features/tag-management/components/tag-filter' import ServiceApi from '../extra-info/service-api' @@ -37,6 +39,7 @@ type Props = { stepByStepTourCreateMenuHighlightPart?: string stepByStepTourCreateMenuOpen?: boolean stepByStepTourCreateMenuTarget?: string + knowledgeViewSwitcherProps?: KnowledgeViewSwitcherProps } const DatasetListHeader = ({ @@ -58,6 +61,7 @@ const DatasetListHeader = ({ stepByStepTourCreateMenuHighlightPart, stepByStepTourCreateMenuOpen, stepByStepTourCreateMenuTarget, + knowledgeViewSwitcherProps, }: Props) => { const { t } = useTranslation() const showCreateMenu = canCreateDataset || canConnectExternalDataset @@ -67,15 +71,19 @@ const DatasetListHeader = ({ return (
-
-

- {t(($) => $.knowledge, { ns: 'dataset' })} -

-
+
+
+

+ {t(($) => $.knowledge, { ns: 'dataset' })} +

+ {knowledgeViewSwitcherProps && } +
+
{canConnectExternalDataset && ( - + )}
diff --git a/web/app/components/datasets/list/index.tsx b/web/app/components/datasets/list/index.tsx index 6029e2b8845..f2d3f7af8e5 100644 --- a/web/app/components/datasets/list/index.tsx +++ b/web/app/components/datasets/list/index.tsx @@ -1,8 +1,10 @@ 'use client' +import type { KnowledgeViewSwitcherProps } from '@/features/new-rag/components/knowledge-view-switcher' +// Libraries import { useBoolean, useDebounceFn } from 'ahooks' import { useAtomValue, useSetAtom } from 'jotai' -// Libraries +import { parseAsStringLiteral, useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -17,7 +19,9 @@ import { } from '@/app/components/step-by-step-tour/target-registry' import { useExternalApiPanel } from '@/context/external-api-panel-context' import { workspacePermissionKeysAtom } from '@/context/permission-state' +import { systemFeaturesAtom } from '@/context/system-features-state' import { isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' +import { NewKnowledgeList } from '@/features/new-rag/new-knowledge-list' import { TagManagementModal } from '@/features/tag-management/components/tag-management-modal' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter } from '@/next/navigation' @@ -34,7 +38,13 @@ import Datasets from './datasets' import DatasetFirstEmptyState from './first-empty-state' import DatasetListHeader from './header' -const List = () => { +const knowledgeViewParser = parseAsStringLiteral(['legacy', 'new']).withDefault('legacy') + +function LegacyList({ + knowledgeViewSwitcherProps, +}: { + knowledgeViewSwitcherProps?: KnowledgeViewSwitcherProps +}) { const { t } = useTranslation() const { push } = useRouter() const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom) @@ -131,86 +141,57 @@ const List = () => { return (
+ push('/datasets/create')} + onCreateFromPipeline={() => push('/datasets/create-from-pipeline')} + onConnectDataset={() => push('/datasets/connect')} + onExternalApiClick={() => setShowExternalApiPanel(true)} + onIncludeAllChange={toggleIncludeAll} + onKeywordsChange={handleKeywordsChange} + onOpenTagManagement={() => setShowTagManagementModal(true)} + onTagsChange={handleTagsChange} + stepByStepTourCreateMenuOpen={ + activeKnowledgeGuide ? shouldOpenStepByStepTourCreateMenu : undefined + } + stepByStepTourCreateMenuTarget={STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsCreate} + stepByStepTourCreateMenuHighlightPart={ + STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsCreateMenu + } + knowledgeViewSwitcherProps={knowledgeViewSwitcherProps} + /> {showEmptyDataList ? ( - <> - push('/datasets/create')} - onCreateFromPipeline={() => push('/datasets/create-from-pipeline')} - onConnectDataset={() => push('/datasets/connect')} - onExternalApiClick={() => setShowExternalApiPanel(true)} - onIncludeAllChange={toggleIncludeAll} - onKeywordsChange={handleKeywordsChange} - onOpenTagManagement={() => setShowTagManagementModal(true)} - onTagsChange={handleTagsChange} - stepByStepTourCreateMenuOpen={ - activeKnowledgeGuide ? shouldOpenStepByStepTourCreateMenu : undefined - } - stepByStepTourCreateMenuTarget={STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsCreate} - stepByStepTourCreateMenuHighlightPart={ - STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsCreateMenu - } - /> - - + ) : ( - <> - push('/datasets/create')} - onCreateFromPipeline={() => push('/datasets/create-from-pipeline')} - onConnectDataset={() => push('/datasets/connect')} - onExternalApiClick={() => setShowExternalApiPanel(true)} - onIncludeAllChange={toggleIncludeAll} - onKeywordsChange={handleKeywordsChange} - onOpenTagManagement={() => setShowTagManagementModal(true)} - onTagsChange={handleTagsChange} - stepByStepTourCreateMenuOpen={ - activeKnowledgeGuide ? shouldOpenStepByStepTourCreateMenu : undefined - } - stepByStepTourCreateMenuTarget={STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsCreate} - stepByStepTourCreateMenuHighlightPart={ - STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsCreateMenu - } - /> - $['filterEmpty.noKnowledge'], { ns: 'dataset' })} - /> - ) : undefined - } - fetchNextPage={datasetListQuery.fetchNextPage} - hasNextPage={datasetListQuery.hasNextPage} - isFetching={datasetListQuery.isFetching} - isFetchingNextPage={datasetListQuery.isFetchingNextPage} - isLoading={datasetListQuery.isLoading} - isPlaceholderData={datasetListQuery.isPlaceholderData} - onOpenTagManagement={() => setShowTagManagementModal(true)} - stepByStepTourActionMenuHighlightPart={ - STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsFirstCardActionsMenu - } - stepByStepTourActionMenuOpen={shouldOpenStepByStepTourDatasetCardActionMenu} - stepByStepTourCardTarget={STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsFirstCard} - /> - + $['filterEmpty.noKnowledge'], { ns: 'dataset' })} /> + ) : undefined + } + fetchNextPage={datasetListQuery.fetchNextPage} + hasNextPage={datasetListQuery.hasNextPage} + isFetching={datasetListQuery.isFetching} + isFetchingNextPage={datasetListQuery.isFetchingNextPage} + isLoading={datasetListQuery.isLoading} + isPlaceholderData={datasetListQuery.isPlaceholderData} + onOpenTagManagement={() => setShowTagManagementModal(true)} + stepByStepTourActionMenuHighlightPart={ + STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsFirstCardActionsMenu + } + stepByStepTourActionMenuOpen={shouldOpenStepByStepTourDatasetCardActionMenu} + stepByStepTourCardTarget={STEP_BY_STEP_TOUR_TARGETS.knowledgeWithDatasetsFirstCard} + /> )} { ) } +function KnowledgeFsList() { + const [view, setView] = useQueryState('view', knowledgeViewParser) + const onViewChange = (nextView: 'legacy' | 'new') => { + void setView(nextView) + } + + if (view === 'new') return + + return ( + + ) +} + +function List() { + const { knowledge_fs_enabled: knowledgeFsEnabled } = useAtomValue(systemFeaturesAtom) + + if (!knowledgeFsEnabled) return + + return +} + export default List diff --git a/web/env.ts b/web/env.ts index 0bf9edeaac0..1ef3bff7406 100644 --- a/web/env.ts +++ b/web/env.ts @@ -88,6 +88,7 @@ const clientSchema = { NEXT_PUBLIC_ALLOW_REGISTER: coercedBoolean.default(true), NEXT_PUBLIC_ALLOW_CREATE_WORKSPACE: coercedBoolean.default(true), NEXT_PUBLIC_IS_EMAIL_SETUP: coercedBoolean.default(true), + NEXT_PUBLIC_KNOWLEDGE_FS_ENABLED: coercedBoolean.default(false), NEXT_PUBLIC_ENABLE_CHANGE_EMAIL: coercedBoolean.default(true), NEXT_PUBLIC_CREATORS_PLATFORM_FEATURES_ENABLED: coercedBoolean.default(true), NEXT_PUBLIC_ENABLE_TRIAL_APP: coercedBoolean.default(true), @@ -272,6 +273,9 @@ export const env = createEnv({ NEXT_PUBLIC_IS_EMAIL_SETUP: isServer ? process.env.NEXT_PUBLIC_IS_EMAIL_SETUP : getRuntimeEnvFromBody('isEmailSetup'), + NEXT_PUBLIC_KNOWLEDGE_FS_ENABLED: isServer + ? process.env.NEXT_PUBLIC_KNOWLEDGE_FS_ENABLED + : getRuntimeEnvFromBody('knowledgeFsEnabled'), NEXT_PUBLIC_ENABLE_CHANGE_EMAIL: isServer ? process.env.NEXT_PUBLIC_ENABLE_CHANGE_EMAIL : getRuntimeEnvFromBody('enableChangeEmail'), diff --git a/web/features/new-rag/README.md b/web/features/new-rag/README.md new file mode 100644 index 00000000000..85a81cb206c --- /dev/null +++ b/web/features/new-rag/README.md @@ -0,0 +1,24 @@ +# New RAG + +Renders the KnowledgeFS-backed knowledge list, creation entry points, and first-visit guidance. + +## Internal Modules + +- `components/knowledge-space-card` +- `components/knowledge-view-switcher` +- `components/new-knowledge-list-states` +- `storage` + +## External Modules + +- `app/components/apps/first-empty-state/action-card` +- `app/components/base/infotip` +- `app/components/base/search-input` +- `app/components/base/skeleton` +- `app/components/datasets/external-api/external-api-panel` +- `app/components/datasets/extra-info/service-api` +- `context/external-api-panel-context` +- `context/permission-state` +- `hooks/use-format-time-from-now` +- `service/knowledge/use-dataset` +- `utils/permission` diff --git a/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx new file mode 100644 index 00000000000..5f8fe80d8c6 --- /dev/null +++ b/web/features/new-rag/__tests__/new-knowledge-list.spec.tsx @@ -0,0 +1,353 @@ +import type { KnowledgeSpaceList } from '@dify/contracts/knowledge-fs/types.gen' +import type { InfiniteData } from '@tanstack/react-query' +import { screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { renderWithNuqs } from '@/test/nuqs-testing' +import { NewKnowledgeList } from '../new-knowledge-list' + +type ListKnowledgeSpacesInfiniteOptions = { + getNextPageParam: (lastPage: KnowledgeSpaceList) => string | undefined + initialPageParam: string | null + input: (pageParam: unknown) => { + query: { + cursor?: string + limit: number + } + } +} + +const externalApiPanelMock = vi.hoisted(() => ({ + open: false, + setOpen: vi.fn(), +})) +const toastInfoMock = vi.hoisted(() => vi.fn()) + +vi.mock('@langgenius/dify-ui/toast', () => ({ + toast: { info: toastInfoMock }, +})) + +const queryMock = vi.hoisted(() => ({ + data: undefined as InfiniteData | undefined, + error: null as unknown, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isPending: false, + refetch: vi.fn(), +})) + +const consoleQueryMock = vi.hoisted(() => ({ + infiniteOptions: vi.fn((_options: ListKnowledgeSpacesInfiniteOptions) => ({})), +})) + +const permissionStateMock = vi.hoisted(() => ({ + workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'], + workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'), +})) + +vi.mock('@/context/external-api-panel-context', () => ({ + useExternalApiPanel: () => ({ + showExternalApiPanel: externalApiPanelMock.open, + setShowExternalApiPanel: externalApiPanelMock.setOpen, + }), +})) + +vi.mock('@/service/knowledge/use-dataset', () => ({ + useDatasetApiBaseUrl: () => ({ data: { api_base_url: 'https://api.example.com' } }), +})) + +vi.mock('@/app/components/datasets/extra-info/service-api', () => ({ + default: () => , +})) + +vi.mock('@/app/components/datasets/external-api/external-api-panel', () => ({ + default: () =>
external API panel
, +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + useInfiniteQuery: () => queryMock, + } +}) + +vi.mock('jotai', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + useAtomValue: (atom: unknown) => { + if (atom === permissionStateMock.workspacePermissionKeysAtom) + return permissionStateMock.workspacePermissionKeys + return original.useAtomValue(atom as Parameters[0]) + }, + } +}) + +vi.mock('@/context/permission-state', () => ({ + workspacePermissionKeysAtom: permissionStateMock.workspacePermissionKeysAtom, +})) + +vi.mock('@/service/client', () => ({ + consoleQuery: { + knowledgeFs: { + listKnowledgeSpaces: { + infiniteOptions: consoleQueryMock.infiniteOptions, + }, + }, + }, +})) + +const setResolvedPage = (items: KnowledgeSpaceList['items'] = []) => { + queryMock.data = { + pageParams: [null], + pages: [{ items }], + } +} + +describe('NewKnowledgeList', () => { + beforeEach(() => { + vi.clearAllMocks() + externalApiPanelMock.open = false + queryMock.data = undefined + queryMock.error = null + queryMock.hasNextPage = false + queryMock.isFetchNextPageError = false + queryMock.isFetchingNextPage = false + queryMock.isPending = false + permissionStateMock.workspacePermissionKeys = [ + 'dataset.create_and_management', + 'dataset.external.connect', + ] + }) + + it('shows a scoped loading state', () => { + queryMock.isPending = true + + renderWithNuqs() + + expect(screen.getByRole('status', { name: 'common.loading' })).toBeInTheDocument() + }) + + it('requests the generated KnowledgeFS collection contract with cursor pagination', () => { + setResolvedPage() + + renderWithNuqs() + + const options = consoleQueryMock.infiniteOptions.mock.calls.at(-1)?.[0] + expect(options).toBeDefined() + expect(options?.initialPageParam).toBeNull() + expect(options?.input(null)).toEqual({ query: { limit: 30 } }) + expect(options?.input('next-page')).toEqual({ + query: { cursor: 'next-page', limit: 30 }, + }) + expect(options?.getNextPageParam({ items: [], nextCursor: 'next-page' })).toBe('next-page') + expect(options?.getNextPageParam({ items: [] })).toBeUndefined() + }) + + it('renders real knowledge spaces as unavailable until the detail contract is supported', () => { + setResolvedPage([ + { + createdAt: '2026-07-15T00:00:00Z', + description: 'Answers for customer support', + iconRef: 'builtin:camera', + id: 'space-1', + name: 'Support knowledge', + revision: 1, + slug: 'support-knowledge', + tenantId: 'tenant-1', + updatedAt: '2026-07-18T00:00:00Z', + }, + { + createdAt: '2026-07-16T00:00:00Z', + id: 'space-2', + name: 'Engineering handbook', + revision: 1, + slug: 'engineering-handbook', + tenantId: 'tenant-1', + updatedAt: '2026-07-19T00:00:00Z', + }, + ]) + + renderWithNuqs() + const list = screen.getByRole('list', { name: 'dataset.knowledge' }) + const supportCard = within(list).getByRole('article', { + name: 'Support knowledge. dataset.cornerLabel.unavailable', + }) + expect(supportCard).toBeInTheDocument() + expect( + within(list).getByRole('article', { + name: 'Engineering handbook. dataset.cornerLabel.unavailable', + }), + ).toBeInTheDocument() + expect(within(list).getByText('Answers for customer support')).toBeInTheDocument() + expect(within(list).getByText('dataset.newKnowledge.noDescription')).toBeInTheDocument() + expect(within(supportCard).getByLabelText('camera')).toBeInTheDocument() + expect(within(list).getAllByText('dataset.newKnowledge.cardType')).toHaveLength(2) + expect(within(list).getAllByText('dataset.newKnowledge.tags')).toHaveLength(2) + expect(within(list).getAllByText('dataset.newKnowledge.documentsUnavailable')).toHaveLength(2) + expect(within(list).getAllByText('dataset.newKnowledge.appsUnavailable')).toHaveLength(2) + expect(within(list).queryByRole('link')).not.toBeInTheDocument() + expect(within(list).queryByRole('button')).not.toBeInTheDocument() + }) + + it('keeps backend-dependent metadata filters interactive and filters loaded items by search', async () => { + const user = userEvent.setup() + setResolvedPage([ + { + createdAt: '2026-07-15T00:00:00Z', + description: 'Answers for customer support', + id: 'space-1', + name: 'Support knowledge', + revision: 1, + slug: 'support-knowledge', + tenantId: 'tenant-1', + updatedAt: '2026-07-18T00:00:00Z', + }, + { + createdAt: '2026-07-16T00:00:00Z', + id: 'space-2', + name: 'Engineering handbook', + revision: 1, + slug: 'engineering-handbook', + tenantId: 'tenant-1', + updatedAt: '2026-07-19T00:00:00Z', + }, + ]) + + renderWithNuqs() + + await user.click(screen.getByRole('button', { name: 'dataset.externalAPIPanelTitle' })) + expect(externalApiPanelMock.setOpen).toHaveBeenCalledWith(true) + expect(screen.getByRole('button', { name: 'dataset.serviceApi.title' })).toBeInTheDocument() + + const tags = screen.getByRole('button', { name: 'dataset.newKnowledge.tags' }) + const creators = screen.getByRole('button', { name: 'dataset.newKnowledge.creators' }) + const search = screen.getByRole('searchbox', { name: 'common.operation.search' }) + const create = screen.getByRole('button', { name: 'common.operation.create' }) + + expect(tags).toBeEnabled() + expect(creators).toBeEnabled() + await user.click(tags) + expect(toastInfoMock).toHaveBeenCalledWith('dataset.newKnowledge.filtersUnavailable') + expect(search).toBeEnabled() + expect(create).toBeDisabled() + expect(create).toHaveAccessibleDescription('dataset.cornerLabel.unavailable') + + await user.type(search, 'customer support') + expect(screen.getByText('Support knowledge')).toBeInTheDocument() + expect(screen.queryByText('Engineering handbook')).not.toBeInTheDocument() + }) + + it('shows unavailable empty-state creation entries to authorized users', () => { + setResolvedPage() + + renderWithNuqs() + + const connectSource = screen.getByRole('button', { + name: 'dataset.newKnowledge.connectSource', + }) + const uploadFiles = screen.getByRole('button', { + name: 'dataset.newKnowledge.uploadFiles', + }) + const startEmpty = screen.getByRole('button', { + name: 'dataset.newKnowledge.startEmpty', + }) + + expect(connectSource).toBeDisabled() + expect(connectSource).toHaveAccessibleDescription( + 'dataset.newKnowledge.connectSourceDescription dataset.cornerLabel.unavailable dataset.firstEmpty.recommended', + ) + expect(uploadFiles).toBeDisabled() + expect(uploadFiles).toHaveAccessibleDescription( + 'dataset.newKnowledge.uploadFilesDescription dataset.cornerLabel.unavailable', + ) + expect(startEmpty).toBeDisabled() + expect(startEmpty).toHaveAccessibleDescription( + 'dataset.newKnowledge.startEmptyDescription dataset.cornerLabel.unavailable', + ) + expect(screen.getByText('dataset.newKnowledge.connectSourceDescription')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.uploadFilesDescription')).toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.startEmptyDescription')).toBeInTheDocument() + expect(screen.getByText('dataset.firstEmpty.recommended')).toBeInTheDocument() + expect(screen.queryByTestId('empty-knowledge-card')).not.toBeInTheDocument() + }) + + it('hides creation entries from read-only users', () => { + permissionStateMock.workspacePermissionKeys = [] + setResolvedPage() + + renderWithNuqs() + + expect( + screen.queryByRole('button', { name: /^dataset\.newKnowledge\.startEmpty/ }), + ).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: /common\.operation\.create/ }), + ).not.toBeInTheDocument() + expect(screen.getByText('dataset.newKnowledge.readOnlyEmpty')).toBeInTheDocument() + }) + + it.each([404, 503])('shows an unavailable state for a %s response', (status) => { + queryMock.error = { status } + renderWithNuqs() + + expect(screen.getByText('dataset.newKnowledge.unavailableTitle')).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.operation.retry' })).not.toBeInTheDocument() + }) + + it('shows a retryable error state for other failures', async () => { + const user = userEvent.setup() + queryMock.error = new Error('request failed') + renderWithNuqs() + + expect(screen.getByText('dataset.newKnowledge.errorTitle')).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) + expect(queryMock.refetch).toHaveBeenCalledOnce() + }) + + it('shows one retry action when loading the next page fails', async () => { + const user = userEvent.setup() + setResolvedPage([ + { + createdAt: '2026-07-15T00:00:00Z', + id: 'space-1', + name: 'Support knowledge', + revision: 1, + slug: 'support-knowledge', + tenantId: 'tenant-1', + updatedAt: '2026-07-18T00:00:00Z', + }, + ]) + queryMock.isFetchNextPageError = true + + renderWithNuqs() + + await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) + expect(queryMock.fetchNextPage).toHaveBeenCalledOnce() + }) + + it('keeps full collection pagination available while local search is active', async () => { + const user = userEvent.setup() + setResolvedPage([ + { + createdAt: '2026-07-15T00:00:00Z', + id: 'space-1', + name: 'Support knowledge', + revision: 1, + slug: 'support-knowledge', + tenantId: 'tenant-1', + updatedAt: '2026-07-18T00:00:00Z', + }, + ]) + queryMock.hasNextPage = true + + renderWithNuqs() + + expect(screen.getByText('Support knowledge')).toBeInTheDocument() + await user.type(screen.getByRole('searchbox', { name: 'common.operation.search' }), 'support') + await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.loadMore' })) + expect(queryMock.fetchNextPage).toHaveBeenCalledOnce() + }) +}) diff --git a/web/features/new-rag/components/knowledge-space-card.tsx b/web/features/new-rag/components/knowledge-space-card.tsx new file mode 100644 index 00000000000..59af90516f8 --- /dev/null +++ b/web/features/new-rag/components/knowledge-space-card.tsx @@ -0,0 +1,92 @@ +import type { KnowledgeSpace } from '@dify/contracts/knowledge-fs/types.gen' +import { useTranslation } from 'react-i18next' +import CornerLabel from '@/app/components/base/corner-label' +import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' + +function getBuiltinIconName(iconRef: string | undefined) { + if (!iconRef?.startsWith('builtin:')) return undefined + return iconRef.slice('builtin:'.length).replaceAll('-', ' ') +} + +export function KnowledgeSpaceCard({ knowledgeSpace }: { knowledgeSpace: KnowledgeSpace }) { + const { t } = useTranslation('dataset') + const { formatTimeFromNow } = useFormatTimeFromNow() + const unavailable = t(($) => $['cornerLabel.unavailable']) + const iconName = getBuiltinIconName(knowledgeSpace.iconRef) + const updatedAt = Date.parse(knowledgeSpace.updatedAt) + const formattedUpdatedAt = Number.isNaN(updatedAt) + ? knowledgeSpace.updatedAt + : formatTimeFromNow(updatedAt) + + return ( +
  • +
    + +
    +
    $['newKnowledge.cardType'])} + title={iconName} + className="flex size-10 shrink-0 items-center justify-center rounded-[10px] border-[0.5px] border-divider-regular bg-components-icon-bg-orange-dark-soft" + > + {iconName ? ( + + {iconName.charAt(0).toUpperCase()} + + ) : ( + + )} +
    +
    +

    + {knowledgeSpace.name} +

    +
    + {t(($) => $['newKnowledge.cardType'])} + · + {unavailable} +
    +
    +
    +

    + {knowledgeSpace.description || t(($) => $['newKnowledge.noDescription'])} +

    +
    $['newKnowledge.tags'])}. ${unavailable}`} + className="mt-1 flex min-w-0 items-center gap-1 px-4" + > + + {t(($) => $['newKnowledge.tags'])} + + {unavailable} +
    +
    + + + + {t(($) => $['newKnowledge.documentsUnavailable'])} + + + + + {t(($) => $['newKnowledge.appsUnavailable'])} + + + / + + + {t(($) => $['newKnowledge.updated'], { + date: formattedUpdatedAt, + })} + +
    +
    +
  • + ) +} diff --git a/web/features/new-rag/components/knowledge-view-switcher.tsx b/web/features/new-rag/components/knowledge-view-switcher.tsx new file mode 100644 index 00000000000..a025416a5bf --- /dev/null +++ b/web/features/new-rag/components/knowledge-view-switcher.tsx @@ -0,0 +1,106 @@ +import { Button } from '@langgenius/dify-ui/button' +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverTitle, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' +import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + useNewKnowledgeGuideDismissedValue, + useSetNewKnowledgeGuideDismissed, +} from '@/features/new-rag/storage' + +export type KnowledgeViewSwitcherProps = { + value: 'legacy' | 'new' + onChange: (value: 'legacy' | 'new') => void +} + +export function KnowledgeViewSwitcher({ value, onChange }: KnowledgeViewSwitcherProps) { + const { t } = useTranslation('dataset') + const guideDismissed = useNewKnowledgeGuideDismissedValue() + const setGuideDismissed = useSetNewKnowledgeGuideDismissed() + const [guideOpenOverride, setGuideOpenOverride] = useState(null) + const guideOpen = guideOpenOverride ?? !guideDismissed + + const dismissGuide = () => { + setGuideDismissed(true) + setGuideOpenOverride(false) + } + + return ( +
    + $['newKnowledge.viewLabel'])} + value={[value]} + onValueChange={(values) => { + const nextValue = values[0] + if (nextValue === 'legacy' || nextValue === 'new') onChange(nextValue) + }} + > + + {t(($) => $['newKnowledge.legacy'])} + + + {t(($) => $['newKnowledge.new'])} + + + + $['newKnowledge.guideTitle'])} + render={ + + } + /> + + + + +
    + + {t(($) => $['newKnowledge.guideTitle'])} + + + {t(($) => $['newKnowledge.guideDescription'])} + +
    + + {t(($) => $['newKnowledge.learnMore'])} + + +
    +
    +
    +
    +
    + ) +} diff --git a/web/features/new-rag/components/new-knowledge-list-states.tsx b/web/features/new-rag/components/new-knowledge-list-states.tsx new file mode 100644 index 00000000000..7e66823a63b --- /dev/null +++ b/web/features/new-rag/components/new-knowledge-list-states.tsx @@ -0,0 +1,212 @@ +import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' +import FirstEmptyActionCard from '@/app/components/apps/first-empty-state/action-card' +import { Infotip } from '@/app/components/base/infotip' +import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skeleton' + +const LOADING_CARD_IDS = [ + 'loading-card-1', + 'loading-card-2', + 'loading-card-3', + 'loading-card-4', + 'loading-card-5', + 'loading-card-6', + 'loading-card-7', + 'loading-card-8', +] as const + +const EMPTY_GHOST_CARD_IDS = Array.from({ length: 16 }, (_, index) => `empty-ghost-card-${index}`) + +export const KNOWLEDGE_SPACE_GRID_CLASS_NAME = + 'grid grid-cols-[repeat(auto-fill,minmax(min(100%,280px),1fr))] gap-2.5' + +export function UnavailableReason({ label, reason }: { label: string; reason: string }) { + return ( + + {reason} + + ) +} + +export function NewKnowledgeLoadingState() { + const { t } = useTranslation('common') + + return ( +
    $.loading)}> + {LOADING_CARD_IDS.map((id) => ( +
    + +
    + +
    + + +
    +
    + + +
    +
    + ))} +
    + ) +} + +export function NewKnowledgePageState({ + action, + description, + title, +}: { + action?: ReactNode + description: ReactNode + title: ReactNode +}) { + return ( +
    +
    + +
    +

    {title}

    +

    {description}

    + {action ?
    {action}
    : null} +
    + ) +} + +function EmptyAction({ + description, + iconClassName, + recommended = false, + title, +}: { + description: string + iconClassName: string + recommended?: boolean + title: string +}) { + const { t } = useTranslation('dataset') + const unavailable = t(($) => $['cornerLabel.unavailable']) + const recommendedLabel = t(($) => $['firstEmpty.recommended']) + + return ( + } + title={title} + visualStyle="list" + /> + ) +} + +function EmptyGhostGrid() { + return ( +
    +
    + {EMPTY_GHOST_CARD_IDS.map((id) => ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ))} +
    +
    + ) +} + +export function NewKnowledgeEmptyState({ + canConnect, + canCreate, +}: { + canConnect: boolean + canCreate: boolean +}) { + const { t } = useTranslation('dataset') + const canStart = canConnect || canCreate + + return ( +
    + +
    +
    +
    + +
    +
    +

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

    +

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

    +
    +
    + {canStart ? ( +
    + {canConnect && ( + $['newKnowledge.connectSource'])} + description={t(($) => $['newKnowledge.connectSourceDescription'])} + /> + )} + {canCreate && ( + $['newKnowledge.uploadFiles'])} + description={t(($) => $['newKnowledge.uploadFilesDescription'])} + /> + )} + {canCreate && ( + <> +
    + + {t(($) => $['firstEmpty.or'])} + +
    + $['newKnowledge.startEmpty'])} + description={t(($) => $['newKnowledge.startEmptyDescription'])} + /> + + )} +
    + ) : ( + + {t(($) => $['newKnowledge.readOnlyEmpty'])} + + )} +
    +
    + ) +} diff --git a/web/features/new-rag/new-knowledge-list.tsx b/web/features/new-rag/new-knowledge-list.tsx new file mode 100644 index 00000000000..6bef586e93e --- /dev/null +++ b/web/features/new-rag/new-knowledge-list.tsx @@ -0,0 +1,217 @@ +'use client' + +import { Button } from '@langgenius/dify-ui/button' +import { toast } from '@langgenius/dify-ui/toast' +import { useInfiniteQuery } from '@tanstack/react-query' +import { useAtomValue } from 'jotai' +import { useId, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { SearchInput } from '@/app/components/base/search-input' +import ExternalAPIPanel from '@/app/components/datasets/external-api/external-api-panel' +import ServiceApi from '@/app/components/datasets/extra-info/service-api' +import { useExternalApiPanel } from '@/context/external-api-panel-context' +import { workspacePermissionKeysAtom } from '@/context/permission-state' +import { consoleQuery } from '@/service/client' +import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset' +import { hasPermission } from '@/utils/permission' +import { KnowledgeSpaceCard } from './components/knowledge-space-card' +import { KnowledgeViewSwitcher } from './components/knowledge-view-switcher' +import { + KNOWLEDGE_SPACE_GRID_CLASS_NAME, + NewKnowledgeEmptyState, + NewKnowledgeLoadingState, + NewKnowledgePageState, + UnavailableReason, +} from './components/new-knowledge-list-states' + +const PAGE_SIZE = 30 + +function isUnavailableError(error: unknown) { + if (!error || typeof error !== 'object') return false + const status = 'status' in error ? error.status : undefined + if (status === 404 || status === 503) return true + + const data = 'data' in error ? error.data : undefined + if (!data || typeof data !== 'object') return false + const dataStatus = 'status' in data ? data.status : undefined + return dataStatus === 404 || dataStatus === 503 +} + +function MetadataFilter({ label, onClick }: { label: string; onClick: () => void }) { + return ( + + ) +} + +export function NewKnowledgeList({ + view, + onViewChange, +}: { + view: 'legacy' | 'new' + onViewChange: (value: 'legacy' | 'new') => void +}) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const { data: apiBaseInfo } = useDatasetApiBaseUrl() + const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel() + const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) + const canCreate = hasPermission(workspacePermissionKeys, 'dataset.create_and_management') + const canConnect = hasPermission(workspacePermissionKeys, 'dataset.external.connect') + const showCreateAction = canCreate || canConnect + const filtersUnavailable = t(($) => $['newKnowledge.filtersUnavailable']) + const showFilterBoundary = () => toast.info(filtersUnavailable) + const unavailable = t(($) => $['cornerLabel.unavailable']) + const createLabel = tCommon(($) => $['operation.create']) + const createUnavailableId = useId() + const [searchValue, setSearchValue] = useState('') + const knowledgeSpacesQuery = useInfiniteQuery( + consoleQuery.knowledgeFs.listKnowledgeSpaces.infiniteOptions({ + input: (pageParam) => ({ + query: { + limit: PAGE_SIZE, + ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}), + }, + }), + getNextPageParam: (lastPage) => lastPage.nextCursor, + initialPageParam: null as string | null, + }), + ) + const knowledgeSpaces = knowledgeSpacesQuery.data?.pages.flatMap((page) => page.items) ?? [] + const normalizedSearchValue = searchValue.trim().toLocaleLowerCase() + const visibleKnowledgeSpaces = normalizedSearchValue + ? knowledgeSpaces.filter( + (knowledgeSpace) => + knowledgeSpace.name.toLocaleLowerCase().includes(normalizedSearchValue) || + knowledgeSpace.description?.toLocaleLowerCase().includes(normalizedSearchValue), + ) + : knowledgeSpaces + + return ( +
    $['newKnowledge.new'])} + className="relative flex grow flex-col overflow-y-auto bg-background-body" + > +
    +
    +
    +

    + {t(($) => $.knowledge)} +

    + +
    +
    + {canConnect && ( + + )} + +
    +
    +
    +
    + $['newKnowledge.tags'])} onClick={showFilterBoundary} /> + $['newKnowledge.creators'])} + onClick={showFilterBoundary} + /> + +
    + {showCreateAction && ( +
    + + + {unavailable} + + +
    + )} +
    +
    +
    + {knowledgeSpacesQuery.isPending ? ( + + ) : knowledgeSpacesQuery.error && !knowledgeSpacesQuery.data ? ( + isUnavailableError(knowledgeSpacesQuery.error) ? ( + $['newKnowledge.unavailableTitle'])} + description={t(($) => $['newKnowledge.unavailableDescription'])} + /> + ) : ( + $['newKnowledge.errorTitle'])} + description={t(($) => $['newKnowledge.errorDescription'])} + action={ + + } + /> + ) + ) : knowledgeSpaces.length === 0 ? ( + + ) : ( + <> +
      $.knowledge)}> + {visibleKnowledgeSpaces.map((knowledgeSpace) => ( + + ))} +
    + {knowledgeSpacesQuery.isFetchNextPageError ? ( +
    + {t(($) => $['newKnowledge.errorDescription'])} + +
    + ) : knowledgeSpacesQuery.hasNextPage ? ( +
    + +
    + ) : null} + + )} +
    + {showExternalApiPanel && canConnect && ( + setShowExternalApiPanel(false)} + /> + )} +
    + ) +} diff --git a/web/features/new-rag/storage.ts b/web/features/new-rag/storage.ts new file mode 100644 index 00000000000..20609f3c8a2 --- /dev/null +++ b/web/features/new-rag/storage.ts @@ -0,0 +1,13 @@ +'use client' + +import { createLocalStorageState } from 'foxact/create-local-storage-state' + +const NEW_KNOWLEDGE_GUIDE_DISMISSED_STORAGE_KEY = 'dify-new-knowledge-guide-dismissed' + +const [ + _useNewKnowledgeGuideDismissed, + useNewKnowledgeGuideDismissedValue, + useSetNewKnowledgeGuideDismissed, +] = createLocalStorageState(NEW_KNOWLEDGE_GUIDE_DISMISSED_STORAGE_KEY, false) + +export { useNewKnowledgeGuideDismissedValue, useSetNewKnowledgeGuideDismissed } diff --git a/web/features/system-features/config.ts b/web/features/system-features/config.ts index 764574c0e55..a6d8468407b 100644 --- a/web/features/system-features/config.ts +++ b/web/features/system-features/config.ts @@ -60,6 +60,7 @@ export const defaultSystemFeatures = { enable_explore_banner: false, enable_learn_app: true, enable_step_by_step_tour: false, + knowledge_fs_enabled: false, } satisfies GetSystemFeaturesResponse export const cloudSystemFeatures = { @@ -76,6 +77,7 @@ export const cloudSystemFeatures = { is_allow_create_workspace: env.NEXT_PUBLIC_ALLOW_CREATE_WORKSPACE, is_email_setup: env.NEXT_PUBLIC_IS_EMAIL_SETUP, enable_change_email: env.NEXT_PUBLIC_ENABLE_CHANGE_EMAIL, + knowledge_fs_enabled: env.NEXT_PUBLIC_KNOWLEDGE_FS_ENABLED, license: { ...defaultSystemFeatures.license, diff --git a/web/i18n/ar-TN/dataset.json b/web/i18n/ar-TN/dataset.json index 809a78b746a..1fe8028eaa4 100644 --- a/web/i18n/ar-TN/dataset.json +++ b/web/i18n/ar-TN/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "مطلوب نموذج إعادة الترتيب لخلط قواعد المعرفة عالية الجودة والاقتصادية.", "mixtureInternalAndExternalTip": "مطلوب نموذج إعادة الترتيب لخلط المعرفة الداخلية والخارجية.", "multimodal": "متعدد الوسائط", + "newKnowledge.appsUnavailable": "— تطبيقات", + "newKnowledge.cardType": "مساحة معرفة", + "newKnowledge.connectSource": "ربط مصدر", + "newKnowledge.connectSourceDescription": "يبقى المحتوى متزامنًا مع Notion والمواقع الإلكترونية والخدمات الأخرى.", + "newKnowledge.creators": "المنشئون", + "newKnowledge.documentsUnavailable": "— مستندات", + "newKnowledge.emptyDescription": "أضف مستنداتك ليتمكن الوكلاء من قراءتها مباشرة — من دون إعداد الفهرسة.", + "newKnowledge.emptyTitle": "طريقة أذكى لبناء قواعد المعرفة", + "newKnowledge.errorDescription": "تعذر تحميل قواعد المعرفة الجديدة. حاول مرة أخرى.", + "newKnowledge.errorTitle": "حدث خطأ ما", + "newKnowledge.filtersUnavailable": "سيصبح هذا المرشح متاحًا عندما يوفر KnowledgeFS بياناته الوصفية.", + "newKnowledge.gotIt": "فهمت", + "newKnowledge.guideDescription": "اربط مصدرًا وستبقى مستنداته متزامنة ومفهرسة تلقائيًا وجاهزة للاستخدام، من دون إعدادات تحتاج إلى إدارة. ستواصل قواعد المعرفة السابقة العمل كما هي.", + "newKnowledge.guideTitle": "تعرّف على قاعدة المعرفة الجديدة ✨", + "newKnowledge.learnMore": "معرفة المزيد", + "newKnowledge.legacy": "قديم", + "newKnowledge.loadMore": "تحميل المزيد", + "newKnowledge.new": "جديد", + "newKnowledge.noDescription": "لا يوجد وصف", + "newKnowledge.readOnlyEmpty": "اطلب من محرر مساحة العمل إنشاء أول قاعدة معرفة.", + "newKnowledge.startEmpty": "ابدأ فارغًا", + "newKnowledge.startEmptyDescription": "أنشئها الآن وأضف المحتوى لاحقًا.", + "newKnowledge.tags": "الوسوم", + "newKnowledge.unavailableDescription": "KnowledgeFS غير متاح لمساحة العمل هذه حاليًا.", + "newKnowledge.unavailableTitle": "المعرفة الجديدة غير متاحة", + "newKnowledge.updated": "تم التحديث في {{date}}", + "newKnowledge.uploadFiles": "رفع الملفات", + "newKnowledge.uploadFilesDescription": "أضف ملفات من جهازك — ستتم فهرستها بمجرد إنشائها.", + "newKnowledge.viewLabel": "عرض المعرفة", "noExternalKnowledge": "لا توجد واجهة برمجة تطبيقات معرفة خارجية حتى الآن، انقر هنا لإنشاء", "parentMode.fullDoc": "مستند كامل", "parentMode.paragraph": "فقرة", diff --git a/web/i18n/de-DE/dataset.json b/web/i18n/de-DE/dataset.json index 42e5381cd26..327dc36b085 100644 --- a/web/i18n/de-DE/dataset.json +++ b/web/i18n/de-DE/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Für die Mischung von hochwertigen und wirtschaftlichen Wissensbasen ist das Rerank-Modell erforderlich.", "mixtureInternalAndExternalTip": "Das Rerank-Modell ist für die Mischung von internem und externem Wissen erforderlich.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— Apps", + "newKnowledge.cardType": "Wissensbereich", + "newKnowledge.connectSource": "Quelle verbinden", + "newKnowledge.connectSourceDescription": "Inhalte bleiben mit Notion, Websites und anderen Diensten synchron.", + "newKnowledge.creators": "Ersteller", + "newKnowledge.documentsUnavailable": "— Dokumente", + "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.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.learnMore": "Mehr erfahren", + "newKnowledge.legacy": "Legacy", + "newKnowledge.loadMore": "Mehr laden", + "newKnowledge.new": "Neu", + "newKnowledge.noDescription": "Keine Beschreibung", + "newKnowledge.readOnlyEmpty": "Bitten Sie einen Arbeitsbereich-Editor, die erste Wissensdatenbank zu erstellen.", + "newKnowledge.startEmpty": "Leer beginnen", + "newKnowledge.startEmptyDescription": "Jetzt erstellen und Inhalte später hinzufügen.", + "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.uploadFiles": "Dateien hochladen", + "newKnowledge.uploadFilesDescription": "Fügen Sie Dateien von Ihrem Computer hinzu — sie werden beim Erstellen sofort indiziert.", + "newKnowledge.viewLabel": "Wissensansicht", "noExternalKnowledge": "Es gibt noch keine External Knowledge API, klicken Sie hier, um zu erstellen", "parentMode.fullDoc": "Vollständiges Dokument", "parentMode.paragraph": "Absatz", diff --git a/web/i18n/en-US/dataset.json b/web/i18n/en-US/dataset.json index 8a6a099fc14..a35bbea01d0 100644 --- a/web/i18n/en-US/dataset.json +++ b/web/i18n/en-US/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "The Rerank model is required for mixture of high quality and economical knowledge bases.", "mixtureInternalAndExternalTip": "The Rerank model is required for mixture of internal and external knowledge.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— apps", + "newKnowledge.cardType": "Knowledge space", + "newKnowledge.connectSource": "Connect a source", + "newKnowledge.connectSourceDescription": "Content stays in sync with Notion, websites, and other services.", + "newKnowledge.creators": "Creators", + "newKnowledge.documentsUnavailable": "— documents", + "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.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.learnMore": "Learn more", + "newKnowledge.legacy": "Legacy", + "newKnowledge.loadMore": "Load more", + "newKnowledge.new": "New", + "newKnowledge.noDescription": "No description", + "newKnowledge.readOnlyEmpty": "Ask a workspace editor to create the first knowledge base.", + "newKnowledge.startEmpty": "Start empty", + "newKnowledge.startEmptyDescription": "Create it now, add content later.", + "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.uploadFiles": "Upload files", + "newKnowledge.uploadFilesDescription": "Add files from your computer — they're indexed as soon as you create.", + "newKnowledge.viewLabel": "Knowledge view", "noExternalKnowledge": "There is no External Knowledge API yet, click here to create", "parentMode.fullDoc": "Full-doc", "parentMode.paragraph": "Paragraph", diff --git a/web/i18n/es-ES/dataset.json b/web/i18n/es-ES/dataset.json index 72f17df723f..ad851178fdf 100644 --- a/web/i18n/es-ES/dataset.json +++ b/web/i18n/es-ES/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Se requiere el modelo de reclasificación para la mezcla de bases de conocimiento de alta calidad y económicas.", "mixtureInternalAndExternalTip": "El modelo de Rerank es necesario para la mezcla de conocimiento interno y externo.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— aplicaciones", + "newKnowledge.cardType": "Espacio de conocimiento", + "newKnowledge.connectSource": "Conectar una fuente", + "newKnowledge.connectSourceDescription": "El contenido se mantiene sincronizado con Notion, sitios web y otros servicios.", + "newKnowledge.creators": "Creadores", + "newKnowledge.documentsUnavailable": "— documentos", + "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.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.learnMore": "Más información", + "newKnowledge.legacy": "Anterior", + "newKnowledge.loadMore": "Cargar más", + "newKnowledge.new": "Nuevo", + "newKnowledge.noDescription": "Sin descripción", + "newKnowledge.readOnlyEmpty": "Pide a un editor del espacio de trabajo que cree la primera base de conocimiento.", + "newKnowledge.startEmpty": "Empezar vacía", + "newKnowledge.startEmptyDescription": "Créala ahora y añade contenido más tarde.", + "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.uploadFiles": "Subir archivos", + "newKnowledge.uploadFilesDescription": "Añade archivos desde tu ordenador; se indexarán en cuanto los crees.", + "newKnowledge.viewLabel": "Vista de conocimiento", "noExternalKnowledge": "Todavía no hay una API de conocimiento externo, haga clic aquí para crear", "parentMode.fullDoc": "Documento completo", "parentMode.paragraph": "Párrafo", diff --git a/web/i18n/fa-IR/dataset.json b/web/i18n/fa-IR/dataset.json index 1a52b1ec7c6..63ba0fcd682 100644 --- a/web/i18n/fa-IR/dataset.json +++ b/web/i18n/fa-IR/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "مدل دوباره رتبه‌بندی برای ترکیب پایگاه‌های دانش با کیفیت بالا و اقتصادی لازم است.", "mixtureInternalAndExternalTip": "مدل Rerank برای آمیختگی دانش درونی و بیرونی مورد نیاز است.", "multimodal": "چندوجهی", + "newKnowledge.appsUnavailable": "— برنامه", + "newKnowledge.cardType": "فضای دانش", + "newKnowledge.connectSource": "اتصال منبع", + "newKnowledge.connectSourceDescription": "محتوا با Notion، وب‌سایت‌ها و سرویس‌های دیگر همگام می‌ماند.", + "newKnowledge.creators": "ایجادکنندگان", + "newKnowledge.documentsUnavailable": "— سند", + "newKnowledge.emptyDescription": "اسناد خود را اضافه کنید تا عامل‌ها بتوانند مستقیماً آن‌ها را بخوانند — بدون نیاز به تنظیم نمایه‌سازی.", + "newKnowledge.emptyTitle": "روشی هوشمندتر برای ساخت پایگاه‌های دانش", + "newKnowledge.errorDescription": "پایگاه‌های دانش جدید بارگذاری نشدند. دوباره تلاش کنید.", + "newKnowledge.errorTitle": "مشکلی پیش آمد", + "newKnowledge.filtersUnavailable": "این فیلتر پس از ارائه فراداده توسط KnowledgeFS در دسترس خواهد بود.", + "newKnowledge.gotIt": "متوجه شدم", + "newKnowledge.guideDescription": "یک منبع را متصل کنید تا اسناد آن همگام، خودکار نمایه‌سازی و آماده استفاده بمانند؛ بدون تنظیماتی برای مدیریت. پایگاه‌های دانش قبلی شما همچنان مانند گذشته کار می‌کنند.", + "newKnowledge.guideTitle": "با پایگاه دانش جدید آشنا شوید ✨", + "newKnowledge.learnMore": "بیشتر بدانید", + "newKnowledge.legacy": "قدیمی", + "newKnowledge.loadMore": "بارگذاری بیشتر", + "newKnowledge.new": "جدید", + "newKnowledge.noDescription": "بدون توضیح", + "newKnowledge.readOnlyEmpty": "از یک ویرایشگر فضای کاری بخواهید اولین پایگاه دانش را ایجاد کند.", + "newKnowledge.startEmpty": "شروع از خالی", + "newKnowledge.startEmptyDescription": "اکنون ایجاد کنید و بعداً محتوا اضافه کنید.", + "newKnowledge.tags": "برچسب‌ها", + "newKnowledge.unavailableDescription": "KnowledgeFS اکنون برای این فضای کاری در دسترس نیست.", + "newKnowledge.unavailableTitle": "دانش جدید در دسترس نیست", + "newKnowledge.updated": "به‌روزرسانی در {{date}}", + "newKnowledge.uploadFiles": "بارگذاری فایل‌ها", + "newKnowledge.uploadFilesDescription": "فایل‌ها را از رایانه اضافه کنید — به‌محض ایجاد نمایه‌سازی می‌شوند.", + "newKnowledge.viewLabel": "نمای دانش", "noExternalKnowledge": "هنوز هیچ API دانش خارجی وجود ندارد، برای ایجاد اینجا را کلیک کنید", "parentMode.fullDoc": "مستند کامل", "parentMode.paragraph": "پاراگراف", diff --git a/web/i18n/fr-FR/dataset.json b/web/i18n/fr-FR/dataset.json index 74f35ad9776..ad3a673bf4a 100644 --- a/web/i18n/fr-FR/dataset.json +++ b/web/i18n/fr-FR/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Le modèle de reclassement est nécessaire pour le mélange de bases de connaissances de haute qualité et économiques.", "mixtureInternalAndExternalTip": "Le modèle Rerank est nécessaire pour mélanger les connaissances internes et externes.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— applications", + "newKnowledge.cardType": "Espace de connaissances", + "newKnowledge.connectSource": "Connecter une source", + "newKnowledge.connectSourceDescription": "Le contenu reste synchronisé avec Notion, les sites web et d’autres services.", + "newKnowledge.creators": "Créateurs", + "newKnowledge.documentsUnavailable": "— documents", + "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.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.learnMore": "En savoir plus", + "newKnowledge.legacy": "Ancien", + "newKnowledge.loadMore": "Charger plus", + "newKnowledge.new": "Nouveau", + "newKnowledge.noDescription": "Aucune description", + "newKnowledge.readOnlyEmpty": "Demandez à un éditeur de l’espace de travail de créer la première base de connaissances.", + "newKnowledge.startEmpty": "Commencer à vide", + "newKnowledge.startEmptyDescription": "Créez-la maintenant et ajoutez du contenu plus tard.", + "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.uploadFiles": "Importer des fichiers", + "newKnowledge.uploadFilesDescription": "Ajoutez des fichiers depuis votre ordinateur : ils sont indexés dès leur création.", + "newKnowledge.viewLabel": "Vue des connaissances", "noExternalKnowledge": "Il n’y a pas encore d’API de connaissances externes, cliquez ici pour créer", "parentMode.fullDoc": "Doc complet", "parentMode.paragraph": "Paragraphe", diff --git a/web/i18n/hi-IN/dataset.json b/web/i18n/hi-IN/dataset.json index 44b0e92b5e4..0c1973dab26 100644 --- a/web/i18n/hi-IN/dataset.json +++ b/web/i18n/hi-IN/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "उच्च गुणवत्ता और किफायती ज्ञान आधारों के मिश्रण के लिए पुनः रैंकिंग मॉडल आवश्यक है।", "mixtureInternalAndExternalTip": "आंतरिक और बाहरी ज्ञान के मिश्रण के लिए रीरैंक मॉडल की आवश्यकता होती है।", "multimodal": "बहु-मोडल", + "newKnowledge.appsUnavailable": "— ऐप", + "newKnowledge.cardType": "नॉलेज स्पेस", + "newKnowledge.connectSource": "स्रोत कनेक्ट करें", + "newKnowledge.connectSourceDescription": "कंटेंट Notion, वेबसाइटों और अन्य सेवाओं के साथ सिंक रहता है।", + "newKnowledge.creators": "निर्माता", + "newKnowledge.documentsUnavailable": "— दस्तावेज़", + "newKnowledge.emptyDescription": "अपने दस्तावेज़ जोड़ें ताकि एजेंट उन्हें सीधे पढ़ सकें — इंडेक्सिंग कॉन्फ़िगर करने की ज़रूरत नहीं।", + "newKnowledge.emptyTitle": "नॉलेज बेस बनाने का अधिक स्मार्ट तरीका", + "newKnowledge.errorDescription": "नए नॉलेज बेस लोड नहीं हो सके। फिर से कोशिश करें।", + "newKnowledge.errorTitle": "कुछ गलत हुआ", + "newKnowledge.filtersUnavailable": "KnowledgeFS के मेटाडेटा उपलब्ध कराने पर यह फ़िल्टर उपलब्ध होगा।", + "newKnowledge.gotIt": "समझ गया", + "newKnowledge.guideDescription": "किसी स्रोत को कनेक्ट करें और उसके दस्तावेज़ सिंक, अपने-आप इंडेक्स और उपयोग के लिए तैयार रहेंगे—किसी सेटअप को संभालने की ज़रूरत नहीं। आपके पुराने नॉलेज बेस पहले की तरह काम करते रहेंगे।", + "newKnowledge.guideTitle": "नए नॉलेज बेस से मिलें ✨", + "newKnowledge.learnMore": "और जानें", + "newKnowledge.legacy": "पुराना", + "newKnowledge.loadMore": "और लोड करें", + "newKnowledge.new": "नया", + "newKnowledge.noDescription": "कोई विवरण नहीं", + "newKnowledge.readOnlyEmpty": "पहला नॉलेज बेस बनाने के लिए कार्यस्थान संपादक से कहें।", + "newKnowledge.startEmpty": "खाली शुरू करें", + "newKnowledge.startEmptyDescription": "अभी बनाएँ, कंटेंट बाद में जोड़ें।", + "newKnowledge.tags": "टैग", + "newKnowledge.unavailableDescription": "KnowledgeFS अभी इस कार्यस्थान के लिए उपलब्ध नहीं है।", + "newKnowledge.unavailableTitle": "नया नॉलेज उपलब्ध नहीं है", + "newKnowledge.updated": "{{date}} को अपडेट किया गया", + "newKnowledge.uploadFiles": "फ़ाइलें अपलोड करें", + "newKnowledge.uploadFilesDescription": "अपने कंप्यूटर से फ़ाइलें जोड़ें — बनाते ही वे इंडेक्स हो जाएँगी।", + "newKnowledge.viewLabel": "नॉलेज दृश्य", "noExternalKnowledge": "अभी तक कोई बाहरी ज्ञान एपीआई नहीं है, बनाने के लिए यहां क्लिक करें", "parentMode.fullDoc": "पूर्ण-दस्तावेज़", "parentMode.paragraph": "अनुच्‍छेद", diff --git a/web/i18n/id-ID/dataset.json b/web/i18n/id-ID/dataset.json index dee2bf50d21..8de7d4c4637 100644 --- a/web/i18n/id-ID/dataset.json +++ b/web/i18n/id-ID/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Model Rerank diperlukan untuk campuran basis pengetahuan berkualitas tinggi dan ekonomis.", "mixtureInternalAndExternalTip": "Model Rerank diperlukan untuk campuran pengetahuan internal dan eksternal.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— aplikasi", + "newKnowledge.cardType": "Ruang pengetahuan", + "newKnowledge.connectSource": "Hubungkan sumber", + "newKnowledge.connectSourceDescription": "Konten tetap tersinkronisasi dengan Notion, situs web, dan layanan lainnya.", + "newKnowledge.creators": "Pembuat", + "newKnowledge.documentsUnavailable": "— dokumen", + "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.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.learnMore": "Pelajari lebih lanjut", + "newKnowledge.legacy": "Lama", + "newKnowledge.loadMore": "Muat lebih banyak", + "newKnowledge.new": "Baru", + "newKnowledge.noDescription": "Tidak ada deskripsi", + "newKnowledge.readOnlyEmpty": "Minta editor ruang kerja membuat basis pengetahuan pertama.", + "newKnowledge.startEmpty": "Mulai kosong", + "newKnowledge.startEmptyDescription": "Buat sekarang, tambahkan konten nanti.", + "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.uploadFiles": "Unggah file", + "newKnowledge.uploadFilesDescription": "Tambahkan file dari komputer — file akan diindeks segera setelah dibuat.", + "newKnowledge.viewLabel": "Tampilan pengetahuan", "noExternalKnowledge": "Belum ada API Pengetahuan Eksternal, klik di sini untuk membuat", "parentMode.fullDoc": "Dokumen lengkap", "parentMode.paragraph": "Paragraf", diff --git a/web/i18n/it-IT/dataset.json b/web/i18n/it-IT/dataset.json index f6871d8e8bb..473cea14612 100644 --- a/web/i18n/it-IT/dataset.json +++ b/web/i18n/it-IT/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Il modello di riclassificazione è necessario per la miscela di basi di conoscenza di alta qualità ed economiche.", "mixtureInternalAndExternalTip": "Il modello Rerank è necessario per la combinazione di conoscenze interne ed esterne.", "multimodal": "Multimodale", + "newKnowledge.appsUnavailable": "— app", + "newKnowledge.cardType": "Spazio di conoscenza", + "newKnowledge.connectSource": "Collega una fonte", + "newKnowledge.connectSourceDescription": "I contenuti restano sincronizzati con Notion, siti web e altri servizi.", + "newKnowledge.creators": "Autori", + "newKnowledge.documentsUnavailable": "— documenti", + "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.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.learnMore": "Scopri di più", + "newKnowledge.legacy": "Legacy", + "newKnowledge.loadMore": "Carica altro", + "newKnowledge.new": "Nuovo", + "newKnowledge.noDescription": "Nessuna descrizione", + "newKnowledge.readOnlyEmpty": "Chiedi a un editor dello spazio di lavoro di creare la prima knowledge base.", + "newKnowledge.startEmpty": "Inizia da zero", + "newKnowledge.startEmptyDescription": "Creala ora e aggiungi i contenuti in seguito.", + "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.uploadFiles": "Carica file", + "newKnowledge.uploadFilesDescription": "Aggiungi file dal computer: vengono indicizzati appena li crei.", + "newKnowledge.viewLabel": "Vista conoscenza", "noExternalKnowledge": "Non esiste ancora un'API di conoscenza esterna, fai clic qui per creare", "parentMode.fullDoc": "Full-doc", "parentMode.paragraph": "Paragrafo", diff --git a/web/i18n/ja-JP/dataset.json b/web/i18n/ja-JP/dataset.json index 55cb4cdeca5..0b3270cdc34 100644 --- a/web/i18n/ja-JP/dataset.json +++ b/web/i18n/ja-JP/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "高品質なナレッジベースとコスト重視のナレッジベースを混在させるには、Rerank モデルが必要です。", "mixtureInternalAndExternalTip": "内部と外部のナレッジベースを混在させる場合、Rerank モデルが必要です。", "multimodal": "マルチモーダル", + "newKnowledge.appsUnavailable": "— 個のアプリ", + "newKnowledge.cardType": "ナレッジスペース", + "newKnowledge.connectSource": "ソースを接続", + "newKnowledge.connectSourceDescription": "コンテンツは Notion、Web サイト、その他のサービスと同期され続けます。", + "newKnowledge.creators": "作成者", + "newKnowledge.documentsUnavailable": "— 件のドキュメント", + "newKnowledge.emptyDescription": "ドキュメントを追加すると、エージェントが直接読み取れます。インデックス設定は不要です。", + "newKnowledge.emptyTitle": "ナレッジベースをよりスマートに構築", + "newKnowledge.errorDescription": "新しいナレッジベースを読み込めませんでした。もう一度お試しください。", + "newKnowledge.errorTitle": "問題が発生しました", + "newKnowledge.filtersUnavailable": "KnowledgeFS がメタデータを提供すると、このフィルターを利用できます。", + "newKnowledge.gotIt": "了解", + "newKnowledge.guideDescription": "ソースを接続すると、そのドキュメントは同期され、自動的にインデックス化されてすぐに利用できます。管理する設定はありません。従来のナレッジベースもこれまでどおり動作します。", + "newKnowledge.guideTitle": "新しいナレッジベースへようこそ ✨", + "newKnowledge.learnMore": "詳細を見る", + "newKnowledge.legacy": "旧バージョン", + "newKnowledge.loadMore": "さらに読み込む", + "newKnowledge.new": "新バージョン", + "newKnowledge.noDescription": "説明なし", + "newKnowledge.readOnlyEmpty": "最初のナレッジベースの作成をワークスペース編集者に依頼してください。", + "newKnowledge.startEmpty": "空から始める", + "newKnowledge.startEmptyDescription": "今すぐ作成し、コンテンツは後から追加できます。", + "newKnowledge.tags": "タグ", + "newKnowledge.unavailableDescription": "このワークスペースでは現在 KnowledgeFS を利用できません。", + "newKnowledge.unavailableTitle": "新しいナレッジは利用できません", + "newKnowledge.updated": "{{date}} に更新", + "newKnowledge.uploadFiles": "ファイルをアップロード", + "newKnowledge.uploadFilesDescription": "コンピューターからファイルを追加すると、作成時にすぐインデックス化されます。", + "newKnowledge.viewLabel": "ナレッジ表示", "noExternalKnowledge": "外部ナレッジベース連携 API がありません。ここをクリックして作成してください", "parentMode.fullDoc": "全体", "parentMode.paragraph": "段落", diff --git a/web/i18n/ko-KR/dataset.json b/web/i18n/ko-KR/dataset.json index d7c36a07186..8527e050235 100644 --- a/web/i18n/ko-KR/dataset.json +++ b/web/i18n/ko-KR/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "고품질과 경제적 지식 베이스의 혼합을 위해서는 재순위 모델이 필요합니다.", "mixtureInternalAndExternalTip": "리랭크 모델은 내부 및 외부 지식의 혼합에 필요합니다.", "multimodal": "멀티모달", + "newKnowledge.appsUnavailable": "앱 —개", + "newKnowledge.cardType": "지식 공간", + "newKnowledge.connectSource": "소스 연결", + "newKnowledge.connectSourceDescription": "콘텐츠가 Notion, 웹사이트 및 기타 서비스와 계속 동기화됩니다.", + "newKnowledge.creators": "생성자", + "newKnowledge.documentsUnavailable": "문서 —개", + "newKnowledge.emptyDescription": "문서를 추가하면 에이전트가 직접 읽을 수 있습니다. 인덱싱 설정은 필요하지 않습니다.", + "newKnowledge.emptyTitle": "지식 베이스를 구축하는 더 스마트한 방법", + "newKnowledge.errorDescription": "새 지식 베이스를 불러오지 못했습니다. 다시 시도하세요.", + "newKnowledge.errorTitle": "문제가 발생했습니다", + "newKnowledge.filtersUnavailable": "KnowledgeFS에서 메타데이터를 제공하면 이 필터를 사용할 수 있습니다.", + "newKnowledge.gotIt": "확인", + "newKnowledge.guideDescription": "소스를 연결하면 문서가 계속 동기화되고 자동으로 인덱싱되어 바로 사용할 수 있습니다. 관리할 설정은 없습니다. 기존 지식 베이스도 그대로 작동합니다.", + "newKnowledge.guideTitle": "새로운 지식 베이스를 만나보세요 ✨", + "newKnowledge.learnMore": "자세히 알아보기", + "newKnowledge.legacy": "레거시", + "newKnowledge.loadMore": "더 보기", + "newKnowledge.new": "신규", + "newKnowledge.noDescription": "설명 없음", + "newKnowledge.readOnlyEmpty": "워크스페이스 편집자에게 첫 번째 지식 베이스 생성을 요청하세요.", + "newKnowledge.startEmpty": "빈 상태로 시작", + "newKnowledge.startEmptyDescription": "지금 만들고 콘텐츠는 나중에 추가하세요.", + "newKnowledge.tags": "태그", + "newKnowledge.unavailableDescription": "현재 이 워크스페이스에서 KnowledgeFS를 사용할 수 없습니다.", + "newKnowledge.unavailableTitle": "새 지식을 사용할 수 없음", + "newKnowledge.updated": "{{date}} 업데이트", + "newKnowledge.uploadFiles": "파일 업로드", + "newKnowledge.uploadFilesDescription": "컴퓨터에서 파일을 추가하면 생성 즉시 인덱싱됩니다.", + "newKnowledge.viewLabel": "지식 보기", "noExternalKnowledge": "아직 외부 지식 API 가 없으므로 여기를 클릭하여 생성하십시오.", "parentMode.fullDoc": "전체 문서", "parentMode.paragraph": "단락", diff --git a/web/i18n/nl-NL/dataset.json b/web/i18n/nl-NL/dataset.json index da3020af4c7..25c5068c754 100644 --- a/web/i18n/nl-NL/dataset.json +++ b/web/i18n/nl-NL/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "The Rerank model is required for mixture of high quality and economical knowledge bases.", "mixtureInternalAndExternalTip": "The Rerank model is required for mixture of internal and external knowledge.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— apps", + "newKnowledge.cardType": "Kennisruimte", + "newKnowledge.connectSource": "Bron verbinden", + "newKnowledge.connectSourceDescription": "Inhoud blijft gesynchroniseerd met Notion, websites en andere diensten.", + "newKnowledge.creators": "Makers", + "newKnowledge.documentsUnavailable": "— documenten", + "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.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.learnMore": "Meer informatie", + "newKnowledge.legacy": "Oud", + "newKnowledge.loadMore": "Meer laden", + "newKnowledge.new": "Nieuw", + "newKnowledge.noDescription": "Geen beschrijving", + "newKnowledge.readOnlyEmpty": "Vraag een werkruimte-editor om de eerste kennisbank te maken.", + "newKnowledge.startEmpty": "Leeg beginnen", + "newKnowledge.startEmptyDescription": "Maak de kennisbank nu en voeg later inhoud toe.", + "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.uploadFiles": "Bestanden uploaden", + "newKnowledge.uploadFilesDescription": "Voeg bestanden van je computer toe; ze worden geïndexeerd zodra je ze aanmaakt.", + "newKnowledge.viewLabel": "Kennisweergave", "noExternalKnowledge": "There is no External Knowledge API yet, click here to create", "parentMode.fullDoc": "Full-doc", "parentMode.paragraph": "Paragraph", diff --git a/web/i18n/pl-PL/dataset.json b/web/i18n/pl-PL/dataset.json index 61028b11788..aff189f12d3 100644 --- a/web/i18n/pl-PL/dataset.json +++ b/web/i18n/pl-PL/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Model ponownego rankingu jest wymagany dla mieszanki wysokiej jakości i ekonomicznych baz wiedzy.", "mixtureInternalAndExternalTip": "Model Rerank jest wymagany do połączenia wiedzy wewnętrznej i zewnętrznej.", "multimodal": "Multimodalny", + "newKnowledge.appsUnavailable": "— aplikacji", + "newKnowledge.cardType": "Przestrzeń wiedzy", + "newKnowledge.connectSource": "Połącz źródło", + "newKnowledge.connectSourceDescription": "Treści pozostają zsynchronizowane z Notion, witrynami internetowymi i innymi usługami.", + "newKnowledge.creators": "Twórcy", + "newKnowledge.documentsUnavailable": "— dokumentów", + "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.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.learnMore": "Dowiedz się więcej", + "newKnowledge.legacy": "Starsza wersja", + "newKnowledge.loadMore": "Załaduj więcej", + "newKnowledge.new": "Nowa wersja", + "newKnowledge.noDescription": "Brak opisu", + "newKnowledge.readOnlyEmpty": "Poproś edytora obszaru roboczego o utworzenie pierwszej bazy wiedzy.", + "newKnowledge.startEmpty": "Zacznij od pustej", + "newKnowledge.startEmptyDescription": "Utwórz ją teraz, a zawartość dodaj później.", + "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.uploadFiles": "Prześlij pliki", + "newKnowledge.uploadFilesDescription": "Dodaj pliki z komputera — zostaną zindeksowane od razu po utworzeniu.", + "newKnowledge.viewLabel": "Widok wiedzy", "noExternalKnowledge": "Nie ma jeszcze interfejsu API wiedzy zewnętrznej, kliknij tutaj, aby utworzyć", "parentMode.fullDoc": "Pełna wersja dokumentu", "parentMode.paragraph": "Akapit", diff --git a/web/i18n/pt-BR/dataset.json b/web/i18n/pt-BR/dataset.json index c35099ff91c..585852ad2c5 100644 --- a/web/i18n/pt-BR/dataset.json +++ b/web/i18n/pt-BR/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "O modelo de reclassificação é necessário para a mistura de bases de conhecimento de alta qualidade e econômicas.", "mixtureInternalAndExternalTip": "O modelo de Reclassificação é necessário para a mistura de conhecimento interno e externo.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— apps", + "newKnowledge.cardType": "Espaço de conhecimento", + "newKnowledge.connectSource": "Conectar uma fonte", + "newKnowledge.connectSourceDescription": "O conteúdo permanece sincronizado com Notion, sites e outros serviços.", + "newKnowledge.creators": "Criadores", + "newKnowledge.documentsUnavailable": "— documentos", + "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.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.learnMore": "Saiba mais", + "newKnowledge.legacy": "Legado", + "newKnowledge.loadMore": "Carregar mais", + "newKnowledge.new": "Novo", + "newKnowledge.noDescription": "Sem descrição", + "newKnowledge.readOnlyEmpty": "Peça a um editor do espaço de trabalho para criar a primeira base de conhecimento.", + "newKnowledge.startEmpty": "Começar vazia", + "newKnowledge.startEmptyDescription": "Crie agora e adicione conteúdo depois.", + "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.uploadFiles": "Enviar arquivos", + "newKnowledge.uploadFilesDescription": "Adicione arquivos do seu computador; eles são indexados assim que você os cria.", + "newKnowledge.viewLabel": "Visualização de conhecimento", "noExternalKnowledge": "Ainda não existe uma API de conhecimento externo, clique aqui para criar", "parentMode.fullDoc": "Documento completo", "parentMode.paragraph": "Parágrafo", diff --git a/web/i18n/ro-RO/dataset.json b/web/i18n/ro-RO/dataset.json index c340e2476a5..16f6b5981d3 100644 --- a/web/i18n/ro-RO/dataset.json +++ b/web/i18n/ro-RO/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Modelul de reclasificare este necesar pentru amestecul de baze de cunoștințe de înaltă calitate și economice.", "mixtureInternalAndExternalTip": "Modelul Rerank este necesar pentru amestecul de cunoștințe interne și externe.", "multimodal": "Multimodal", + "newKnowledge.appsUnavailable": "— aplicații", + "newKnowledge.cardType": "Spațiu de cunoștințe", + "newKnowledge.connectSource": "Conectează o sursă", + "newKnowledge.connectSourceDescription": "Conținutul rămâne sincronizat cu Notion, site-uri web și alte servicii.", + "newKnowledge.creators": "Creatori", + "newKnowledge.documentsUnavailable": "— documente", + "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.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.learnMore": "Află mai multe", + "newKnowledge.legacy": "Versiune veche", + "newKnowledge.loadMore": "Încarcă mai multe", + "newKnowledge.new": "Versiune nouă", + "newKnowledge.noDescription": "Fără descriere", + "newKnowledge.readOnlyEmpty": "Cere unui editor al spațiului de lucru să creeze prima bază de cunoștințe.", + "newKnowledge.startEmpty": "Începe de la zero", + "newKnowledge.startEmptyDescription": "Creeaz-o acum și adaugă conținut mai târziu.", + "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.uploadFiles": "Încarcă fișiere", + "newKnowledge.uploadFilesDescription": "Adaugă fișiere de pe computer — sunt indexate imediat ce le creezi.", + "newKnowledge.viewLabel": "Vizualizare cunoștințe", "noExternalKnowledge": "Nu există încă un API de cunoștințe externe, faceți clic aici pentru a crea", "parentMode.fullDoc": "Documentar complet", "parentMode.paragraph": "Paragraf", diff --git a/web/i18n/ru-RU/dataset.json b/web/i18n/ru-RU/dataset.json index 5251f291f63..e256ffe64d1 100644 --- a/web/i18n/ru-RU/dataset.json +++ b/web/i18n/ru-RU/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Для смешивания высококачественных и экономичных баз знаний требуется модель переранжирования.", "mixtureInternalAndExternalTip": "Модель Rerank необходима для смешивания внутренних и внешних знаний.", "multimodal": "Мультимодальный", + "newKnowledge.appsUnavailable": "— приложений", + "newKnowledge.cardType": "Пространство знаний", + "newKnowledge.connectSource": "Подключить источник", + "newKnowledge.connectSourceDescription": "Контент остаётся синхронизированным с Notion, веб-сайтами и другими сервисами.", + "newKnowledge.creators": "Создатели", + "newKnowledge.documentsUnavailable": "— документов", + "newKnowledge.emptyDescription": "Добавьте документы, и агенты смогут читать их напрямую — без настройки индексации.", + "newKnowledge.emptyTitle": "Более умный способ создавать базы знаний", + "newKnowledge.errorDescription": "Не удалось загрузить новые базы знаний. Повторите попытку.", + "newKnowledge.errorTitle": "Что-то пошло не так", + "newKnowledge.filtersUnavailable": "Фильтр станет доступен, когда KnowledgeFS предоставит метаданные.", + "newKnowledge.gotIt": "Понятно", + "newKnowledge.guideDescription": "Подключите источник, и его документы будут синхронизироваться, автоматически индексироваться и сразу станут готовы к работе — без дополнительной настройки. Ваши прежние базы знаний продолжат работать как раньше.", + "newKnowledge.guideTitle": "Познакомьтесь с новой базой знаний ✨", + "newKnowledge.learnMore": "Подробнее", + "newKnowledge.legacy": "Предыдущая версия", + "newKnowledge.loadMore": "Загрузить ещё", + "newKnowledge.new": "Новая версия", + "newKnowledge.noDescription": "Нет описания", + "newKnowledge.readOnlyEmpty": "Попросите редактора рабочего пространства создать первую базу знаний.", + "newKnowledge.startEmpty": "Начать с пустой", + "newKnowledge.startEmptyDescription": "Создайте сейчас, а контент добавьте позже.", + "newKnowledge.tags": "Теги", + "newKnowledge.unavailableDescription": "KnowledgeFS сейчас недоступен для этого рабочего пространства.", + "newKnowledge.unavailableTitle": "Новая база знаний недоступна", + "newKnowledge.updated": "Обновлено {{date}}", + "newKnowledge.uploadFiles": "Загрузить файлы", + "newKnowledge.uploadFilesDescription": "Добавьте файлы с компьютера — они будут проиндексированы сразу после создания.", + "newKnowledge.viewLabel": "Вид базы знаний", "noExternalKnowledge": "У нас еще нет External Knowledge API, нажмите здесь, чтобы создать", "parentMode.fullDoc": "Полный документ", "parentMode.paragraph": "Параграф", diff --git a/web/i18n/sl-SI/dataset.json b/web/i18n/sl-SI/dataset.json index 8b1f0522dd3..9992792ddb9 100644 --- a/web/i18n/sl-SI/dataset.json +++ b/web/i18n/sl-SI/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Model za ponovno razvrščanje je potreben za mešanico baz znanja visoke kakovosti in varčnih baz.", "mixtureInternalAndExternalTip": "Model za ponovno razvrščanje je potreben za mešanico notranjega in zunanjega znanja.", "multimodal": "Multimodalen", + "newKnowledge.appsUnavailable": "— aplikacij", + "newKnowledge.cardType": "Prostor znanja", + "newKnowledge.connectSource": "Poveži vir", + "newKnowledge.connectSourceDescription": "Vsebina ostaja sinhronizirana z Notionom, spletnimi mesti in drugimi storitvami.", + "newKnowledge.creators": "Ustvarjalci", + "newKnowledge.documentsUnavailable": "— dokumentov", + "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.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.learnMore": "Več informacij", + "newKnowledge.legacy": "Staro", + "newKnowledge.loadMore": "Naloži več", + "newKnowledge.new": "Novo", + "newKnowledge.noDescription": "Brez opisa", + "newKnowledge.readOnlyEmpty": "Prosite urednika delovnega prostora, naj ustvari prvo bazo znanja.", + "newKnowledge.startEmpty": "Začni prazno", + "newKnowledge.startEmptyDescription": "Ustvarite jo zdaj in vsebino dodajte pozneje.", + "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.uploadFiles": "Naloži datoteke", + "newKnowledge.uploadFilesDescription": "Dodajte datoteke iz računalnika — indeksirajo se takoj ob ustvarjanju.", + "newKnowledge.viewLabel": "Prikaz znanja", "noExternalKnowledge": "Zunanjega API-ja za znanje še ni, kliknite tukaj za ustvarjanje", "parentMode.fullDoc": "Celoten dokument", "parentMode.paragraph": "Odstavek", diff --git a/web/i18n/th-TH/dataset.json b/web/i18n/th-TH/dataset.json index ee6c6d41283..7f1eeb27f5b 100644 --- a/web/i18n/th-TH/dataset.json +++ b/web/i18n/th-TH/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "โมเดล Rerank จําเป็นสําหรับการผสมผสานระหว่างฐานความรู้คุณภาพสูงและประหยัด", "mixtureInternalAndExternalTip": "โมเดล Rerank จําเป็นสําหรับการผสมผสานระหว่างความรู้ภายในและภายนอก", "multimodal": "หลายรูปแบบ", + "newKnowledge.appsUnavailable": "— แอป", + "newKnowledge.cardType": "พื้นที่ความรู้", + "newKnowledge.connectSource": "เชื่อมต่อแหล่งข้อมูล", + "newKnowledge.connectSourceDescription": "เนื้อหาจะซิงค์กับ Notion เว็บไซต์ และบริการอื่น ๆ อยู่เสมอ", + "newKnowledge.creators": "ผู้สร้าง", + "newKnowledge.documentsUnavailable": "— เอกสาร", + "newKnowledge.emptyDescription": "เพิ่มเอกสารของคุณ แล้วเอเจนต์จะอ่านได้โดยตรง โดยไม่ต้องตั้งค่าการทำดัชนี", + "newKnowledge.emptyTitle": "วิธีที่ชาญฉลาดกว่าในการสร้างฐานความรู้", + "newKnowledge.errorDescription": "ไม่สามารถโหลดฐานความรู้ใหม่ได้ โปรดลองอีกครั้ง", + "newKnowledge.errorTitle": "เกิดข้อผิดพลาด", + "newKnowledge.filtersUnavailable": "ตัวกรองนี้จะพร้อมใช้งานเมื่อ KnowledgeFS เปิดเผยข้อมูลเมตา", + "newKnowledge.gotIt": "เข้าใจแล้ว", + "newKnowledge.guideDescription": "เชื่อมต่อแหล่งข้อมูล แล้วเอกสารจะซิงค์ ทำดัชนีอัตโนมัติ และพร้อมใช้งานโดยไม่ต้องจัดการการตั้งค่า ฐานความรู้เดิมของคุณยังคงทำงานได้ตามปกติ", + "newKnowledge.guideTitle": "พบกับฐานความรู้แบบใหม่ ✨", + "newKnowledge.learnMore": "เรียนรู้เพิ่มเติม", + "newKnowledge.legacy": "แบบเดิม", + "newKnowledge.loadMore": "โหลดเพิ่มเติม", + "newKnowledge.new": "แบบใหม่", + "newKnowledge.noDescription": "ไม่มีคำอธิบาย", + "newKnowledge.readOnlyEmpty": "ขอให้ผู้แก้ไขพื้นที่ทำงานสร้างฐานความรู้แรก", + "newKnowledge.startEmpty": "เริ่มจากว่างเปล่า", + "newKnowledge.startEmptyDescription": "สร้างตอนนี้แล้วเพิ่มเนื้อหาภายหลัง", + "newKnowledge.tags": "แท็ก", + "newKnowledge.unavailableDescription": "ขณะนี้ KnowledgeFS ไม่พร้อมใช้งานสำหรับพื้นที่ทำงานนี้", + "newKnowledge.unavailableTitle": "ความรู้แบบใหม่ไม่พร้อมใช้งาน", + "newKnowledge.updated": "อัปเดตเมื่อ {{date}}", + "newKnowledge.uploadFiles": "อัปโหลดไฟล์", + "newKnowledge.uploadFilesDescription": "เพิ่มไฟล์จากคอมพิวเตอร์ ไฟล์จะถูกทำดัชนีทันทีที่สร้าง", + "newKnowledge.viewLabel": "มุมมองความรู้", "noExternalKnowledge": "ยังไม่มี External Knowledge API คลิกที่นี่เพื่อสร้าง", "parentMode.fullDoc": "เอกสารฉบับเต็ม", "parentMode.paragraph": "วรรค", diff --git a/web/i18n/tr-TR/dataset.json b/web/i18n/tr-TR/dataset.json index 6f74525b48d..457c5804d8c 100644 --- a/web/i18n/tr-TR/dataset.json +++ b/web/i18n/tr-TR/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Yüksek kaliteli ve ekonomik bilgi tabanlarının karışımı için Yeniden Sıralama modeli gereklidir.", "mixtureInternalAndExternalTip": "Yeniden Sıralama modeli, iç ve dış bilgilerin karışımı için gereklidir.", "multimodal": "Çok Modlu", + "newKnowledge.appsUnavailable": "— uygulama", + "newKnowledge.cardType": "Bilgi alanı", + "newKnowledge.connectSource": "Kaynak bağla", + "newKnowledge.connectSourceDescription": "İçerik Notion, web siteleri ve diğer hizmetlerle senkronize kalır.", + "newKnowledge.creators": "Oluşturanlar", + "newKnowledge.documentsUnavailable": "— belge", + "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.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.learnMore": "Daha fazla bilgi", + "newKnowledge.legacy": "Eski", + "newKnowledge.loadMore": "Daha fazla yükle", + "newKnowledge.new": "Yeni", + "newKnowledge.noDescription": "Açıklama yok", + "newKnowledge.readOnlyEmpty": "İlk bilgi tabanını oluşturması için bir çalışma alanı editörüne başvurun.", + "newKnowledge.startEmpty": "Boş başla", + "newKnowledge.startEmptyDescription": "Şimdi oluşturun, içeriği daha sonra ekleyin.", + "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.uploadFiles": "Dosya yükle", + "newKnowledge.uploadFilesDescription": "Bilgisayarınızdan dosya ekleyin; oluşturduğunuz anda indekslenirler.", + "newKnowledge.viewLabel": "Bilgi görünümü", "noExternalKnowledge": "Henüz Harici Bilgi API'si yok, oluşturmak için buraya tıklayın", "parentMode.fullDoc": "Tam doküman", "parentMode.paragraph": "Paragraf", diff --git a/web/i18n/uk-UA/dataset.json b/web/i18n/uk-UA/dataset.json index 980247db8c3..5e8157cfb2b 100644 --- a/web/i18n/uk-UA/dataset.json +++ b/web/i18n/uk-UA/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Модель перерангування потрібна для суміші високоякісних та економічних баз знань.", "mixtureInternalAndExternalTip": "Модель Rerank необхідна для поєднання внутрішніх і зовнішніх знань.", "multimodal": "Мультимодальний", + "newKnowledge.appsUnavailable": "— застосунків", + "newKnowledge.cardType": "Простір знань", + "newKnowledge.connectSource": "Підключити джерело", + "newKnowledge.connectSourceDescription": "Вміст залишається синхронізованим із Notion, вебсайтами та іншими сервісами.", + "newKnowledge.creators": "Автори", + "newKnowledge.documentsUnavailable": "— документів", + "newKnowledge.emptyDescription": "Додайте документи, і агенти зможуть читати їх безпосередньо — без налаштування індексації.", + "newKnowledge.emptyTitle": "Розумніший спосіб створювати бази знань", + "newKnowledge.errorDescription": "Не вдалося завантажити нові бази знань. Спробуйте ще раз.", + "newKnowledge.errorTitle": "Щось пішло не так", + "newKnowledge.filtersUnavailable": "Фільтр стане доступним, коли KnowledgeFS надасть метадані.", + "newKnowledge.gotIt": "Зрозуміло", + "newKnowledge.guideDescription": "Підключіть джерело, і його документи залишатимуться синхронізованими, автоматично індексуватимуться та будуть готові до використання без додаткового налаштування. Ваші попередні бази знань працюватимуть як і раніше.", + "newKnowledge.guideTitle": "Познайомтеся з новою базою знань ✨", + "newKnowledge.learnMore": "Докладніше", + "newKnowledge.legacy": "Попередня версія", + "newKnowledge.loadMore": "Завантажити ще", + "newKnowledge.new": "Нова версія", + "newKnowledge.noDescription": "Немає опису", + "newKnowledge.readOnlyEmpty": "Попросіть редактора робочого простору створити першу базу знань.", + "newKnowledge.startEmpty": "Почати з порожньої", + "newKnowledge.startEmptyDescription": "Створіть зараз, а вміст додайте пізніше.", + "newKnowledge.tags": "Теги", + "newKnowledge.unavailableDescription": "KnowledgeFS зараз недоступний для цього робочого простору.", + "newKnowledge.unavailableTitle": "Нова база знань недоступна", + "newKnowledge.updated": "Оновлено {{date}}", + "newKnowledge.uploadFiles": "Завантажити файли", + "newKnowledge.uploadFilesDescription": "Додайте файли з комп’ютера — вони індексуються одразу після створення.", + "newKnowledge.viewLabel": "Вигляд бази знань", "noExternalKnowledge": "API зовнішніх знань поки що не існує, натисніть тут, щоб створити", "parentMode.fullDoc": "Повний документ", "parentMode.paragraph": "Абзац", diff --git a/web/i18n/vi-VN/dataset.json b/web/i18n/vi-VN/dataset.json index 65eb32146c4..fd031b96c11 100644 --- a/web/i18n/vi-VN/dataset.json +++ b/web/i18n/vi-VN/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "Mô hình xếp hạng lại là cần thiết cho sự kết hợp của các cơ sở kiến thức chất lượng cao và tiết kiệm.", "mixtureInternalAndExternalTip": "Mô hình Rerank là cần thiết cho sự kết hợp giữa kiến thức bên trong và bên ngoài.", "multimodal": "Đa phương thức", + "newKnowledge.appsUnavailable": "— ứng dụng", + "newKnowledge.cardType": "Không gian kiến thức", + "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.creators": "Người tạo", + "newKnowledge.documentsUnavailable": "— tài liệu", + "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.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.learnMore": "Tìm hiểu thêm", + "newKnowledge.legacy": "Cũ", + "newKnowledge.loadMore": "Tải thêm", + "newKnowledge.new": "Mới", + "newKnowledge.noDescription": "Không có mô tả", + "newKnowledge.readOnlyEmpty": "Yêu cầu biên tập viên không gian làm việc tạo cơ sở kiến thức đầu tiên.", + "newKnowledge.startEmpty": "Bắt đầu trống", + "newKnowledge.startEmptyDescription": "Tạo ngay bây giờ và thêm nội dung sau.", + "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.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.viewLabel": "Chế độ xem kiến thức", "noExternalKnowledge": "Chưa có API Kiến thức Bên ngoài, hãy nhấp vào đây để tạo", "parentMode.fullDoc": "Tài liệu đầy đủ", "parentMode.paragraph": "Đoạn", diff --git a/web/i18n/zh-Hans/dataset.json b/web/i18n/zh-Hans/dataset.json index 85f79325a95..214d8492e28 100644 --- a/web/i18n/zh-Hans/dataset.json +++ b/web/i18n/zh-Hans/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "混合使用高质量和经济型知识库需要配置 Rerank 模型。", "mixtureInternalAndExternalTip": "混合使用内部和外部知识时需要配置 Rerank 模型。", "multimodal": "多模态", + "newKnowledge.appsUnavailable": "— 个应用", + "newKnowledge.cardType": "知识空间", + "newKnowledge.connectSource": "连接数据源", + "newKnowledge.connectSourceDescription": "内容会与 Notion、网站及其他服务保持同步。", + "newKnowledge.creators": "创建者", + "newKnowledge.documentsUnavailable": "— 篇文档", + "newKnowledge.emptyDescription": "添加文档后,智能体即可直接读取,无需配置索引。", + "newKnowledge.emptyTitle": "更智能的知识库构建方式", + "newKnowledge.errorDescription": "无法加载新知识库,请重试。", + "newKnowledge.errorTitle": "出现错误", + "newKnowledge.filtersUnavailable": "KnowledgeFS 提供元数据后即可使用此筛选器。", + "newKnowledge.gotIt": "知道了", + "newKnowledge.guideDescription": "连接数据源后,其文档会保持同步、自动建立索引并可立即使用,无需管理任何配置。旧版知识库仍会照常运行。", + "newKnowledge.guideTitle": "认识全新的知识库 ✨", + "newKnowledge.learnMore": "了解更多", + "newKnowledge.legacy": "旧版", + "newKnowledge.loadMore": "加载更多", + "newKnowledge.new": "新版", + "newKnowledge.noDescription": "暂无描述", + "newKnowledge.readOnlyEmpty": "请联系工作区编辑者创建第一个知识库。", + "newKnowledge.startEmpty": "从空白开始", + "newKnowledge.startEmptyDescription": "现在创建,稍后再添加内容。", + "newKnowledge.tags": "标签", + "newKnowledge.unavailableDescription": "KnowledgeFS 当前在此工作区不可用。", + "newKnowledge.unavailableTitle": "新版知识库不可用", + "newKnowledge.updated": "更新于 {{date}}", + "newKnowledge.uploadFiles": "上传文件", + "newKnowledge.uploadFilesDescription": "从电脑添加文件,创建后会立即建立索引。", + "newKnowledge.viewLabel": "知识库视图", "noExternalKnowledge": "还没有外部知识库 API,点击此处创建", "parentMode.fullDoc": "全文", "parentMode.paragraph": "段落", diff --git a/web/i18n/zh-Hant/dataset.json b/web/i18n/zh-Hant/dataset.json index d1d9aa1a532..fd37657e68a 100644 --- a/web/i18n/zh-Hant/dataset.json +++ b/web/i18n/zh-Hant/dataset.json @@ -131,6 +131,35 @@ "mixtureHighQualityAndEconomicTip": "混合高品質和經濟知識庫需要重新排序模型。", "mixtureInternalAndExternalTip": "Rerank 模型是內部和外部知識的混合所必需的。", "multimodal": "多模態", + "newKnowledge.appsUnavailable": "— 個應用程式", + "newKnowledge.cardType": "知識空間", + "newKnowledge.connectSource": "連接資料來源", + "newKnowledge.connectSourceDescription": "內容會與 Notion、網站及其他服務保持同步。", + "newKnowledge.creators": "建立者", + "newKnowledge.documentsUnavailable": "— 份文件", + "newKnowledge.emptyDescription": "加入文件後,智慧代理即可直接讀取,無需設定索引。", + "newKnowledge.emptyTitle": "更聰明的知識庫建立方式", + "newKnowledge.errorDescription": "無法載入新知識庫,請再試一次。", + "newKnowledge.errorTitle": "發生錯誤", + "newKnowledge.filtersUnavailable": "KnowledgeFS 提供中繼資料後即可使用此篩選器。", + "newKnowledge.gotIt": "知道了", + "newKnowledge.guideDescription": "連接資料來源後,其文件會保持同步、自動建立索引並可立即使用,無需管理任何設定。舊版知識庫仍會照常運作。", + "newKnowledge.guideTitle": "認識全新的知識庫 ✨", + "newKnowledge.learnMore": "瞭解更多", + "newKnowledge.legacy": "舊版", + "newKnowledge.loadMore": "載入更多", + "newKnowledge.new": "新版", + "newKnowledge.noDescription": "暫無描述", + "newKnowledge.readOnlyEmpty": "請聯絡工作區編輯者建立第一個知識庫。", + "newKnowledge.startEmpty": "從空白開始", + "newKnowledge.startEmptyDescription": "現在建立,稍後再加入內容。", + "newKnowledge.tags": "標籤", + "newKnowledge.unavailableDescription": "KnowledgeFS 目前在此工作區不可用。", + "newKnowledge.unavailableTitle": "新版知識庫不可用", + "newKnowledge.updated": "更新於 {{date}}", + "newKnowledge.uploadFiles": "上傳檔案", + "newKnowledge.uploadFilesDescription": "從電腦加入檔案,建立後會立即建立索引。", + "newKnowledge.viewLabel": "知識庫檢視", "noExternalKnowledge": "目前還沒有外部知識 API,按兩下此處創建", "parentMode.fullDoc": "完整文件", "parentMode.paragraph": "段", diff --git a/web/service/client.ts b/web/service/client.ts index f72b5798f21..70dd0a528f8 100644 --- a/web/service/client.ts +++ b/web/service/client.ts @@ -1,7 +1,7 @@ import type { AgentAppPagination } from '@dify/contracts/api/console/agent/types.gen' import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen' -import type { consoleRouterContract } from '@dify/contracts/api/console/router.gen' import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen' +import type { consoleRouterContract } from '@dify/contracts/console' import type { GetReleaseResponse, ListReleasesResponse, diff --git a/web/service/console-router-loader.ts b/web/service/console-router-loader.ts index 58989f3e7d9..d4a5891d3c1 100644 --- a/web/service/console-router-loader.ts +++ b/web/service/console-router-loader.ts @@ -16,8 +16,14 @@ async function loadEnterpriseContract(): Promise { return { enterprise: contract } } +async function loadKnowledgeFsContract(): Promise { + const { contract } = await import('@dify/contracts/knowledge-fs/orpc.gen') + return { knowledgeFs: contract } +} + export async function loadConsoleContractForSegment(segment: string) { if (segment === 'enterprise') return loadEnterpriseContract() + if (segment === 'knowledgeFs') return loadKnowledgeFsContract() const generatedContract = await loadGeneratedConsoleContract(segment) if (generatedContract) return generatedContract diff --git a/web/test/console/state-fixture.ts b/web/test/console/state-fixture.ts index 204b2e01e4e..d82cbbc6a4a 100644 --- a/web/test/console/state-fixture.ts +++ b/web/test/console/state-fixture.ts @@ -27,6 +27,7 @@ export type ConsoleStateFixture = { isLoadingWorkspacePermissionKeys?: boolean workspacePermissionKeys?: string[] datasetRbacEnabled?: boolean + knowledgeFsEnabled?: boolean langGeniusVersionInfo?: Partial refreshUserProfile?: () => void refreshCurrentWorkspace?: () => void @@ -135,6 +136,7 @@ export const seedRegisteredConsoleStateFixture = (store: JotaiStore) => { store.set(systemFeaturesAtom, { ...defaultSystemFeatures, rbac_enabled: state.datasetRbacEnabled ?? false, + knowledge_fs_enabled: state.knowledgeFsEnabled ?? false, }) store.set(langGeniusVersionInfoAtom, { ...defaultLangGeniusVersionInfo, @@ -207,6 +209,7 @@ export const createPermissionStateModuleMock = (getState: ConsoleStateFixtureRes export const createSystemFeaturesStateModuleMock = (getState: ConsoleStateFixtureResolver) => { registerConsoleStateFixture('systemFeatures', () => ({ datasetRbacEnabled: getState().datasetRbacEnabled, + knowledgeFsEnabled: getState().knowledgeFsEnabled, })) return { systemFeaturesAtom,