feat(dataset): add New Knowledge list and empty state (#39300)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-07-22 20:00:50 +08:00 committed by GitHub
parent 1271cfcf3f
commit 34a5ca191b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 2694 additions and 103 deletions

View File

@ -287,7 +287,24 @@ def _proxy_knowledge_fs_non_get(
@bp.route(
"/knowledge-fs/<path:upstream_path>",
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/<path:upstream_path>",
methods=["GET"],
provide_automatic_options=False,
)
@_console_api_errors

View File

@ -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

View File

@ -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, <br>**Default:** 15728640 | | Yes |
| plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes |

View File

@ -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, <br>**Default:** 15728640 | | Yes |
| plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes |

View File

@ -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]:

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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,
}

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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: {

View File

@ -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,
}

View File

@ -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<KnowledgeSpace>
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]

View File

@ -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

View File

@ -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<string, unknown> | 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',
},
],
})

View File

@ -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"
},

View File

@ -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',
})
}

View File

@ -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

View File

@ -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"
/>
)}
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-background-section text-text-tertiary">
<span
className={cn(
'flex size-9 shrink-0 items-center justify-center rounded-lg bg-background-section text-text-tertiary',
disabled && 'text-text-disabled',
)}
>
{icon}
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate system-md-medium text-text-secondary" title={title}>
<span
className={cn(
'truncate system-md-medium text-text-secondary',
disabled && 'text-text-disabled',
)}
title={title}
>
{title}
</span>
<span className="truncate system-xs-regular text-text-tertiary" title={description}>
<span
className={cn(
'truncate system-xs-regular text-text-tertiary',
disabled && 'text-text-disabled',
)}
title={description}
>
{description}
</span>
</span>
{disabledReason && (
<span className="ml-3 shrink-0 system-xs-medium text-text-disabled">
{disabledReason}
</span>
)}
</>
)
}
@ -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 (
<button
type="button"
disabled={isDisabled}
aria-label={isDisabled ? props.title : undefined}
aria-describedby={isDisabled ? descriptionId : undefined}
className={className}
data-step-by-step-tour-target={props.stepByStepTourTarget}
onClick={props.onClick}
onClick={isDisabled ? undefined : props.onClick}
>
<ActionCardContent
badge={props.badge}
@ -176,7 +225,14 @@ function FirstEmptyActionCard(props: FirstEmptyActionCardProps) {
icon={props.icon}
title={props.title}
visualStyle={props.visualStyle}
disabled={isDisabled}
disabledReason={isDisabled ? props.disabledReason : undefined}
/>
{isDisabled && (
<span id={descriptionId} className="sr-only">
{props.description} {props.disabledReason} {props.badge}
</span>
)}
</button>
)
}

View File

@ -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<HTMLButtonElement>) => {
event.stopPropagation()
@ -88,7 +95,8 @@ export function Infotip({
/>
</PopoverTrigger>
<PopoverContent
placement="top"
placement={placement}
sideOffset={sideOffset}
popupClassName={cn(
'max-w-[300px] rounded-md px-3 py-2 system-xs-regular text-text-tertiary',
popupClassName,

View File

@ -1,15 +1,61 @@
import type { ReactNode } from 'react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createStore, Provider } from 'jotai'
import { hydrateRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useNewKnowledgeGuideDismissedValue } from '@/features/new-rag/storage'
import { render } from '@/test/console/render'
import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture'
import { createNuqsTestWrapper, renderWithNuqs } from '@/test/nuqs-testing'
import List from '../index'
const knowledgeFsInfiniteOptionsMock = vi.hoisted(() => 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<typeof import('@tanstack/react-query')>()
return {
...original,
useInfiniteQuery: useInfiniteQueryMock,
}
})
vi.mock('@/service/client', () => ({
consoleQuery: {
knowledgeFs: {
listKnowledgeSpaces: {
infiniteOptions: knowledgeFsInfiniteOptionsMock,
},
},
},
}))
function NewKnowledgeGuideDismissedProbe() {
const dismissed = useNewKnowledgeGuideDismissedValue()
return <output aria-label="new knowledge guide dismissed">{String(dismissed)}</output>
}
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(<List />)
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(<List />, { 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(<List />)
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(<List />, { 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(<List />)
const guide = await screen.findByRole('dialog', {
name: 'dataset.newKnowledge.guideTitle',
})
await user.click(within(guide).getByRole('button', { name: 'dataset.newKnowledge.gotIt' }))
firstRender.unmount()
renderWithNuqs(<List />)
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(<List />)
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 = (
<Provider store={store}>
<NuqsWrapper>
<>
<List />
<NewKnowledgeGuideDismissedProbe />
</>
</NuqsWrapper>
</Provider>
)
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'],
}

View File

@ -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 (
<div className="sticky top-0 z-10 flex flex-col gap-[14px] bg-background-body px-8 pt-4 pb-2">
<div className="flex h-6 w-full items-center gap-2">
<h1 className="min-w-0 flex-1 text-[18px]/[21.6px] font-semibold text-text-primary">
{t(($) => $.knowledge, { ns: 'dataset' })}
</h1>
<div className="flex shrink-0 items-center gap-2">
<div className="flex min-h-6 w-full flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h1 className="min-w-0 text-[18px]/[21.6px] font-semibold text-text-primary">
{t(($) => $.knowledge, { ns: 'dataset' })}
</h1>
{knowledgeViewSwitcherProps && <KnowledgeViewSwitcher {...knowledgeViewSwitcherProps} />}
</div>
<div className="flex max-w-full shrink-0 flex-wrap items-center gap-2">
{canConnectExternalDataset && (
<button
type="button"
className="flex h-6 items-center justify-center gap-1 overflow-hidden rounded-md px-1.5 py-1 text-text-tertiary hover:bg-state-base-hover"
<Button
variant="ghost"
size="small"
className="gap-1 overflow-hidden px-1.5 text-text-tertiary"
onClick={onExternalApiClick}
>
<span
@ -85,7 +93,7 @@ const DatasetListHeader = ({
<span className="px-0.5 system-xs-medium">
{t(($) => $.externalAPIPanelTitle, { ns: 'dataset' })}
</span>
</button>
</Button>
)}
<ServiceApi apiBaseUrl={apiBaseUrl} />
</div>

View File

@ -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 (
<div className="relative flex grow flex-col overflow-y-auto bg-background-body">
<DatasetListHeader
apiBaseUrl={apiBaseInfo?.api_base_url ?? ''}
canConnectExternalDataset={canConnectExternalDataset}
canCreateDataset={canCreateDataset}
includeAll={includeAll}
isCurrentWorkspaceOwner={isCurrentWorkspaceOwner}
keywords={keywords}
tagFilterValue={tagFilterValue}
onCreateDataset={() => 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 ? (
<>
<DatasetListHeader
apiBaseUrl={apiBaseInfo?.api_base_url ?? ''}
canConnectExternalDataset={canConnectExternalDataset}
canCreateDataset={canCreateDataset}
includeAll={includeAll}
isCurrentWorkspaceOwner={isCurrentWorkspaceOwner}
keywords={keywords}
tagFilterValue={tagFilterValue}
onCreateDataset={() => 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
}
/>
<DatasetFirstEmptyState
canConnectExternalDataset={canConnectExternalDataset}
canCreateDataset={canCreateDataset}
/>
</>
<DatasetFirstEmptyState
canConnectExternalDataset={canConnectExternalDataset}
canCreateDataset={canCreateDataset}
/>
) : (
<>
<DatasetListHeader
apiBaseUrl={apiBaseInfo?.api_base_url ?? ''}
canConnectExternalDataset={canConnectExternalDataset}
canCreateDataset={canCreateDataset}
includeAll={includeAll}
isCurrentWorkspaceOwner={isCurrentWorkspaceOwner}
keywords={keywords}
tagFilterValue={tagFilterValue}
onCreateDataset={() => 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
}
/>
<Datasets
datasetList={datasetListQuery.data}
emptyElement={
showFilteredEmptyState ? (
<FilterEmptyState
title={t(($) => $['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}
/>
</>
<Datasets
datasetList={datasetListQuery.data}
emptyElement={
showFilteredEmptyState ? (
<FilterEmptyState title={t(($) => $['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}
/>
)}
<TagManagementModal
type="knowledge"
@ -228,4 +209,30 @@ const List = () => {
)
}
function KnowledgeFsList() {
const [view, setView] = useQueryState('view', knowledgeViewParser)
const onViewChange = (nextView: 'legacy' | 'new') => {
void setView(nextView)
}
if (view === 'new') return <NewKnowledgeList view={view} onViewChange={onViewChange} />
return (
<LegacyList
knowledgeViewSwitcherProps={{
value: view,
onChange: onViewChange,
}}
/>
)
}
function List() {
const { knowledge_fs_enabled: knowledgeFsEnabled } = useAtomValue(systemFeaturesAtom)
if (!knowledgeFsEnabled) return <LegacyList />
return <KnowledgeFsList />
}
export default List

View File

@ -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'),

View File

@ -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`

View File

@ -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<KnowledgeSpaceList> | 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: () => <button type="button">dataset.serviceApi.title</button>,
}))
vi.mock('@/app/components/datasets/external-api/external-api-panel', () => ({
default: () => <div>external API panel</div>,
}))
vi.mock('@tanstack/react-query', async (importOriginal) => {
const original = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...original,
useInfiniteQuery: () => queryMock,
}
})
vi.mock('jotai', async (importOriginal) => {
const original = await importOriginal<typeof import('jotai')>()
return {
...original,
useAtomValue: (atom: unknown) => {
if (atom === permissionStateMock.workspacePermissionKeysAtom)
return permissionStateMock.workspacePermissionKeys
return original.useAtomValue(atom as Parameters<typeof original.useAtomValue>[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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
expect(screen.getByRole('status', { name: 'common.loading' })).toBeInTheDocument()
})
it('requests the generated KnowledgeFS collection contract with cursor pagination', () => {
setResolvedPage()
renderWithNuqs(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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(<NewKnowledgeList view="new" onViewChange={vi.fn()} />)
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()
})
})

View File

@ -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 (
<li>
<article
aria-label={`${knowledgeSpace.name}. ${unavailable}`}
className="relative flex h-[166px] w-full cursor-not-allowed flex-col overflow-hidden rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg text-left shadow-xs"
>
<CornerLabel
label={unavailable}
className="absolute top-0 right-0"
labelClassName="rounded-tr-xl"
/>
<div className="flex w-full items-center gap-3 px-4 pt-4 pb-1.5">
<div
aria-label={iconName ?? t(($) => $['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 ? (
<span aria-hidden className="system-md-semibold text-text-tertiary">
{iconName.charAt(0).toUpperCase()}
</span>
) : (
<span aria-hidden className="i-ri-book-open-line size-5 text-text-tertiary" />
)}
</div>
<div className="min-w-0 flex-1 py-px pr-16">
<h2 className="truncate system-md-semibold text-text-secondary">
{knowledgeSpace.name}
</h2>
<div className="mt-0.5 flex min-w-0 items-center gap-1 system-2xs-medium-uppercase text-text-disabled">
<span className="truncate">{t(($) => $['newKnowledge.cardType'])}</span>
<span aria-hidden>·</span>
<span className="shrink-0">{unavailable}</span>
</div>
</div>
</div>
<p className="line-clamp-2 min-h-8 w-full px-4 py-0.5 body-xs-regular text-text-tertiary">
{knowledgeSpace.description || t(($) => $['newKnowledge.noDescription'])}
</p>
<div
aria-label={`${t(($) => $['newKnowledge.tags'])}. ${unavailable}`}
className="mt-1 flex min-w-0 items-center gap-1 px-4"
>
<span className="rounded-md bg-background-section px-1.5 py-0.5 system-2xs-medium-uppercase text-text-disabled">
{t(($) => $['newKnowledge.tags'])}
</span>
<span className="system-2xs-regular text-text-disabled">{unavailable}</span>
</div>
<div className="mt-auto flex w-full min-w-0 items-center gap-2 px-4 pt-1 pb-2.5 system-xs-regular text-text-tertiary">
<span className="flex shrink-0 items-center gap-1 text-text-disabled">
<span aria-hidden className="i-ri-file-text-line size-3.5" />
<span aria-hidden></span>
<span className="sr-only">{t(($) => $['newKnowledge.documentsUnavailable'])}</span>
</span>
<span className="flex shrink-0 items-center gap-1 text-text-disabled">
<span aria-hidden className="i-ri-robot-2-line size-3.5" />
<span aria-hidden></span>
<span className="sr-only">{t(($) => $['newKnowledge.appsUnavailable'])}</span>
</span>
<span aria-hidden className="text-divider-deep">
/
</span>
<span className="ml-auto min-w-0 truncate text-right">
{t(($) => $['newKnowledge.updated'], {
date: formattedUpdatedAt,
})}
</span>
</div>
</article>
</li>
)
}

View File

@ -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<boolean | null>(null)
const guideOpen = guideOpenOverride ?? !guideDismissed
const dismissGuide = () => {
setGuideDismissed(true)
setGuideOpenOverride(false)
}
return (
<div className="relative max-w-full shrink-0">
<SegmentedControl
className="max-w-full rounded-md p-px"
aria-label={t(($) => $['newKnowledge.viewLabel'])}
value={[value]}
onValueChange={(values) => {
const nextValue = values[0]
if (nextValue === 'legacy' || nextValue === 'new') onChange(nextValue)
}}
>
<SegmentedControlItem
className="h-[22px] rounded-md px-1 py-px system-xs-medium"
value="legacy"
>
{t(($) => $['newKnowledge.legacy'])}
</SegmentedControlItem>
<SegmentedControlItem
className="h-[22px] rounded-md py-px pr-5 pl-1 system-xs-medium"
value="new"
>
{t(($) => $['newKnowledge.new'])}
</SegmentedControlItem>
</SegmentedControl>
<Popover open={guideOpen} onOpenChange={setGuideOpenOverride}>
<PopoverTrigger
aria-label={t(($) => $['newKnowledge.guideTitle'])}
render={
<button
type="button"
className="absolute top-[5px] right-1 z-10 flex size-3.5 items-center justify-center rounded-sm text-text-tertiary outline-hidden hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<span aria-hidden className="i-ri-question-line size-3.5" />
</button>
}
/>
<PopoverContent
placement="bottom"
sideOffset={13}
popupClassName="relative flex max-h-[calc(100dvh-2rem)] min-h-[162px] w-80 max-w-[calc(100vw-2rem)] flex-col"
>
<span
aria-hidden
className="absolute -top-[9.59px] left-1/2 flex size-[19.456px] -translate-x-1/2 items-center justify-center"
>
<span className="size-[13.757px] -rotate-45 rounded-tr-[2px] border-t border-r border-divider-subtle bg-components-panel-bg" />
</span>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-4 pt-3.5 pb-4">
<PopoverTitle className="system-md-medium text-text-primary">
{t(($) => $['newKnowledge.guideTitle'])}
</PopoverTitle>
<PopoverDescription className="mt-2 system-sm-regular text-text-secondary">
{t(($) => $['newKnowledge.guideDescription'])}
</PopoverDescription>
<div className="mt-auto flex flex-wrap items-center justify-end gap-3 pt-3">
<a
href="https://docs.dify.ai/en/guides/knowledge-base"
target="_blank"
rel="noreferrer"
className="rounded-sm system-xs-regular text-text-accent outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
{t(($) => $['newKnowledge.learnMore'])}
</a>
<Button variant="primary" size="small" onClick={dismissGuide}>
{t(($) => $['newKnowledge.gotIt'])}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
)
}

View File

@ -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 (
<Infotip
aria-label={label}
iconVariant="information"
iconSize="large"
placement="bottom"
sideOffset={6}
className="size-6 rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
popupClassName="max-w-[260px] bg-components-tooltip-bg shadow-lg"
>
{reason}
</Infotip>
)
}
export function NewKnowledgeLoadingState() {
const { t } = useTranslation('common')
return (
<div className={KNOWLEDGE_SPACE_GRID_CLASS_NAME} role="status" aria-label={t(($) => $.loading)}>
{LOADING_CARD_IDS.map((id) => (
<div
key={id}
className="h-[166px] rounded-xl border border-components-card-border bg-components-card-bg p-4 shadow-xs"
>
<SkeletonContainer className="h-full">
<div className="flex gap-3">
<SkeletonRectangle className="size-10 animate-pulse rounded-lg motion-reduce:animate-none" />
<div className="flex-1 space-y-2">
<SkeletonRectangle className="h-4 w-2/3 animate-pulse motion-reduce:animate-none" />
<SkeletonRectangle className="h-3 w-1/3 animate-pulse motion-reduce:animate-none" />
</div>
</div>
<SkeletonRectangle className="mt-4 h-3 w-full animate-pulse motion-reduce:animate-none" />
<SkeletonRectangle className="mt-2 h-3 w-4/5 animate-pulse motion-reduce:animate-none" />
</SkeletonContainer>
</div>
))}
</div>
)
}
export function NewKnowledgePageState({
action,
description,
title,
}: {
action?: ReactNode
description: ReactNode
title: ReactNode
}) {
return (
<div className="flex min-h-[420px] flex-col items-center justify-center px-6 text-center">
<div className="mb-5 flex size-12 items-center justify-center rounded-xl border border-components-card-border bg-components-card-bg shadow-xs">
<span aria-hidden className="i-ri-book-open-line size-6 text-text-tertiary" />
</div>
<h2 className="title-2xl-semi-bold text-text-primary">{title}</h2>
<p className="mt-2 max-w-[520px] body-md-regular text-text-tertiary">{description}</p>
{action ? <div className="mt-6">{action}</div> : null}
</div>
)
}
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 (
<FirstEmptyActionCard
disabled
disabledReason={unavailable}
badge={recommended ? recommendedLabel : undefined}
className="min-h-[58px] py-2 backdrop-blur-[6px]"
description={description}
icon={<span aria-hidden className={`${iconClassName} size-4 text-text-disabled`} />}
title={title}
visualStyle="list"
/>
)
}
function EmptyGhostGrid() {
return (
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,black_22%,black_68%,transparent)] [-webkit-mask-image:linear-gradient(to_bottom,transparent,black_22%,black_68%,transparent)]"
>
<div className="absolute top-0 left-1/2 grid w-[1200px] -translate-x-1/2 grid-cols-4 gap-2.5 opacity-35">
{EMPTY_GHOST_CARD_IDS.map((id) => (
<div
key={id}
className="h-[209px] rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg p-4 shadow-xs"
>
<div className="flex items-center gap-3">
<div className="size-10 shrink-0 rounded-[10px] bg-background-section" />
<div className="min-w-0 flex-1 space-y-2">
<div className="h-3 w-2/3 rounded-sm bg-background-section" />
<div className="h-2.5 w-1/3 rounded-sm bg-background-section" />
</div>
</div>
<div className="mt-4 h-2.5 w-full rounded-sm bg-background-section" />
<div className="mt-2 h-2.5 w-4/5 rounded-sm bg-background-section" />
<div className="mt-5 h-5 w-16 rounded-md bg-background-section" />
</div>
))}
</div>
</div>
)
}
export function NewKnowledgeEmptyState({
canConnect,
canCreate,
}: {
canConnect: boolean
canCreate: boolean
}) {
const { t } = useTranslation('dataset')
const canStart = canConnect || canCreate
return (
<div className="relative isolate flex min-h-[calc(100vh-134px)] items-center justify-center overflow-hidden px-4 py-16 text-center sm:px-6">
<EmptyGhostGrid />
<div className="relative z-10 flex w-full max-w-[520px] flex-col items-center gap-6">
<div className="flex flex-col items-center gap-3">
<div className="flex size-14 items-center justify-center rounded-xl border border-dashed border-divider-regular bg-components-card-bg p-1 backdrop-blur-[6px]">
<span aria-hidden className="i-ri-book-open-line size-6 text-text-accent" />
</div>
<div className="flex flex-col items-center gap-1">
<h2 className="title-lg-semi-bold text-text-primary">
{t(($) => $['newKnowledge.emptyTitle'])}
</h2>
<p className="body-sm-regular text-text-tertiary">
{t(($) => $['newKnowledge.emptyDescription'])}
</p>
</div>
</div>
{canStart ? (
<div className="flex w-full flex-col gap-2 pb-8">
{canConnect && (
<EmptyAction
recommended
iconClassName="i-custom-vender-solid-development-api-connection-mod"
title={t(($) => $['newKnowledge.connectSource'])}
description={t(($) => $['newKnowledge.connectSourceDescription'])}
/>
)}
{canCreate && (
<EmptyAction
iconClassName="i-ri-file-text-line"
title={t(($) => $['newKnowledge.uploadFiles'])}
description={t(($) => $['newKnowledge.uploadFilesDescription'])}
/>
)}
{canCreate && (
<>
<div className="flex h-4 items-center gap-2 system-xs-medium-uppercase text-text-tertiary">
<span className="h-px flex-1 bg-divider-subtle" />
<span>{t(($) => $['firstEmpty.or'])}</span>
<span className="h-px flex-1 bg-divider-subtle" />
</div>
<EmptyAction
iconClassName="i-ri-folder-6-line"
title={t(($) => $['newKnowledge.startEmpty'])}
description={t(($) => $['newKnowledge.startEmptyDescription'])}
/>
</>
)}
</div>
) : (
<span className="mt-6 body-sm-regular text-text-tertiary">
{t(($) => $['newKnowledge.readOnlyEmpty'])}
</span>
)}
</div>
</div>
)
}

View File

@ -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 (
<button
type="button"
className="flex h-8 items-center rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 text-text-tertiary outline-hidden hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={onClick}
>
<span className="px-1 system-sm-regular">{label}</span>
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
</button>
)
}
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 (
<section
aria-label={t(($) => $['newKnowledge.new'])}
className="relative flex grow flex-col overflow-y-auto bg-background-body"
>
<header className="sticky top-0 z-10 flex flex-col gap-[14px] bg-background-body px-4 pt-4 pb-2 sm:px-8">
<div className="flex min-h-6 flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h1 className="text-[18px]/[21.6px] font-semibold text-text-primary">
{t(($) => $.knowledge)}
</h1>
<KnowledgeViewSwitcher value={view} onChange={onViewChange} />
</div>
<div className="flex max-w-full shrink-0 flex-wrap items-center gap-2">
{canConnect && (
<Button
variant="ghost"
size="small"
className="gap-1 overflow-hidden px-1.5 text-text-tertiary"
onClick={() => setShowExternalApiPanel(true)}
>
<span
aria-hidden
className="i-custom-vender-solid-development-api-connection-mod size-3.5 shrink-0"
/>
<span className="px-0.5 system-xs-medium">{t(($) => $.externalAPIPanelTitle)}</span>
</Button>
)}
<ServiceApi apiBaseUrl={apiBaseInfo?.api_base_url ?? ''} />
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<MetadataFilter label={t(($) => $['newKnowledge.tags'])} onClick={showFilterBoundary} />
<MetadataFilter
label={t(($) => $['newKnowledge.creators'])}
onClick={showFilterBoundary}
/>
<SearchInput
className="w-full min-w-0 sm:w-[200px]"
value={searchValue}
onValueChange={setSearchValue}
/>
</div>
{showCreateAction && (
<div className="flex items-center gap-1">
<Button
disabled
aria-describedby={createUnavailableId}
variant="primary"
size="medium"
className="gap-0.5 px-2 shadow-xs"
>
<span aria-hidden className="i-ri-add-line size-4 shrink-0" />
<span className="pl-1">{createLabel}</span>
</Button>
<span id={createUnavailableId} className="sr-only">
{unavailable}
</span>
<UnavailableReason label={`${createLabel}. ${unavailable}`} reason={unavailable} />
</div>
)}
</div>
</header>
<div className="px-4 pt-2 pb-8 sm:px-8">
{knowledgeSpacesQuery.isPending ? (
<NewKnowledgeLoadingState />
) : knowledgeSpacesQuery.error && !knowledgeSpacesQuery.data ? (
isUnavailableError(knowledgeSpacesQuery.error) ? (
<NewKnowledgePageState
title={t(($) => $['newKnowledge.unavailableTitle'])}
description={t(($) => $['newKnowledge.unavailableDescription'])}
/>
) : (
<NewKnowledgePageState
title={t(($) => $['newKnowledge.errorTitle'])}
description={t(($) => $['newKnowledge.errorDescription'])}
action={
<Button onClick={() => void knowledgeSpacesQuery.refetch()}>
{tCommon(($) => $['operation.retry'])}
</Button>
}
/>
)
) : knowledgeSpaces.length === 0 ? (
<NewKnowledgeEmptyState canConnect={canConnect} canCreate={canCreate} />
) : (
<>
<ul className={KNOWLEDGE_SPACE_GRID_CLASS_NAME} aria-label={t(($) => $.knowledge)}>
{visibleKnowledgeSpaces.map((knowledgeSpace) => (
<KnowledgeSpaceCard key={knowledgeSpace.id} knowledgeSpace={knowledgeSpace} />
))}
</ul>
{knowledgeSpacesQuery.isFetchNextPageError ? (
<div className="mt-6 flex items-center justify-center gap-3" role="alert">
<span>{t(($) => $['newKnowledge.errorDescription'])}</span>
<Button onClick={() => void knowledgeSpacesQuery.fetchNextPage()}>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
) : knowledgeSpacesQuery.hasNextPage ? (
<div className="mt-6 flex justify-center">
<Button
loading={knowledgeSpacesQuery.isFetchingNextPage}
onClick={() => void knowledgeSpacesQuery.fetchNextPage()}
>
{t(($) => $['newKnowledge.loadMore'])}
</Button>
</div>
) : null}
</>
)}
</div>
{showExternalApiPanel && canConnect && (
<ExternalAPIPanel
canManageExternalKnowledgeApi={canConnect}
onClose={() => setShowExternalApiPanel(false)}
/>
)}
</section>
)
}

View File

@ -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<boolean>(NEW_KNOWLEDGE_GUIDE_DISMISSED_STORAGE_KEY, false)
export { useNewKnowledgeGuideDismissedValue, useSetNewKnowledgeGuideDismissed }

View File

@ -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,

View File

@ -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": "فقرة",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "پاراگراف",

View File

@ -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 dautres 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 sest 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 à lemploi, 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 lespace 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 nest 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 ny a pas encore dAPI de connaissances externes, cliquez ici pour créer",
"parentMode.fullDoc": "Doc complet",
"parentMode.paragraph": "Paragraphe",

View File

@ -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": "अनुच्‍छेद",

View File

@ -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",

View File

@ -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 lindicizzazione.",
"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 alluso, 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",

View File

@ -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": "段落",

View File

@ -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": "단락",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "Параграф",

View File

@ -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",

View File

@ -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": "วรรค",

View File

@ -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",

View File

@ -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": "Абзац",

View File

@ -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",

View File

@ -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": "段落",

View File

@ -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": "段",

View File

@ -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,

View File

@ -16,8 +16,14 @@ async function loadEnterpriseContract(): Promise<AnyContractRouter> {
return { enterprise: contract }
}
async function loadKnowledgeFsContract(): Promise<AnyContractRouter> {
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

View File

@ -27,6 +27,7 @@ export type ConsoleStateFixture = {
isLoadingWorkspacePermissionKeys?: boolean
workspacePermissionKeys?: string[]
datasetRbacEnabled?: boolean
knowledgeFsEnabled?: boolean
langGeniusVersionInfo?: Partial<LangGeniusVersionInfo>
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,