mirror of
https://github.com/langgenius/dify.git
synced 2026-07-23 12:08:33 +08:00
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:
parent
1271cfcf3f
commit
34a5ca191b
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
7
packages/contracts/console.ts
Normal file
7
packages/contracts/console.ts
Normal 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,
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
|
||||
47
packages/contracts/generated/knowledge-fs/orpc.gen.ts
Normal file
47
packages/contracts/generated/knowledge-fs/orpc.gen.ts
Normal 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,
|
||||
}
|
||||
138
packages/contracts/generated/knowledge-fs/types.gen.ts
Normal file
138
packages/contracts/generated/knowledge-fs/types.gen.ts
Normal 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]
|
||||
131
packages/contracts/generated/knowledge-fs/zod.gen.ts
Normal file
131
packages/contracts/generated/knowledge-fs/zod.gen.ts
Normal 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
|
||||
50
packages/contracts/openapi-ts.knowledge-fs.config.ts
Normal file
50
packages/contracts/openapi-ts.knowledge-fs.config.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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',
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'],
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
24
web/features/new-rag/README.md
Normal file
24
web/features/new-rag/README.md
Normal 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`
|
||||
353
web/features/new-rag/__tests__/new-knowledge-list.spec.tsx
Normal file
353
web/features/new-rag/__tests__/new-knowledge-list.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
92
web/features/new-rag/components/knowledge-space-card.tsx
Normal file
92
web/features/new-rag/components/knowledge-space-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
106
web/features/new-rag/components/knowledge-view-switcher.tsx
Normal file
106
web/features/new-rag/components/knowledge-view-switcher.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
212
web/features/new-rag/components/new-knowledge-list-states.tsx
Normal file
212
web/features/new-rag/components/new-knowledge-list-states.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
217
web/features/new-rag/new-knowledge-list.tsx
Normal file
217
web/features/new-rag/new-knowledge-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
13
web/features/new-rag/storage.ts
Normal file
13
web/features/new-rag/storage.ts
Normal 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 }
|
||||
@ -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,
|
||||
|
||||
@ -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": "فقرة",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "پاراگراف",
|
||||
|
||||
@ -131,6 +131,35 @@
|
||||
"mixtureHighQualityAndEconomicTip": "Le modèle de reclassement est nécessaire pour le mélange de bases de connaissances de haute qualité et économiques.",
|
||||
"mixtureInternalAndExternalTip": "Le modèle Rerank est nécessaire pour mélanger les connaissances internes et externes.",
|
||||
"multimodal": "Multimodal",
|
||||
"newKnowledge.appsUnavailable": "— applications",
|
||||
"newKnowledge.cardType": "Espace de connaissances",
|
||||
"newKnowledge.connectSource": "Connecter une source",
|
||||
"newKnowledge.connectSourceDescription": "Le contenu reste synchronisé avec Notion, les sites web et d’autres services.",
|
||||
"newKnowledge.creators": "Créateurs",
|
||||
"newKnowledge.documentsUnavailable": "— documents",
|
||||
"newKnowledge.emptyDescription": "Ajoutez vos documents et les agents pourront les lire directement, sans indexation à configurer.",
|
||||
"newKnowledge.emptyTitle": "Une façon plus intelligente de créer des bases de connaissances",
|
||||
"newKnowledge.errorDescription": "Impossible de charger vos nouvelles bases de connaissances. Réessayez.",
|
||||
"newKnowledge.errorTitle": "Une erreur s’est produite",
|
||||
"newKnowledge.filtersUnavailable": "Ce filtre sera disponible lorsque KnowledgeFS exposera ses métadonnées.",
|
||||
"newKnowledge.gotIt": "Compris",
|
||||
"newKnowledge.guideDescription": "Connectez une source et ses documents restent synchronisés, indexés automatiquement et prêts à l’emploi, sans configuration à gérer. Vos anciennes bases de connaissances continuent de fonctionner telles quelles.",
|
||||
"newKnowledge.guideTitle": "Découvrez la nouvelle base de connaissances ✨",
|
||||
"newKnowledge.learnMore": "En savoir plus",
|
||||
"newKnowledge.legacy": "Ancien",
|
||||
"newKnowledge.loadMore": "Charger plus",
|
||||
"newKnowledge.new": "Nouveau",
|
||||
"newKnowledge.noDescription": "Aucune description",
|
||||
"newKnowledge.readOnlyEmpty": "Demandez à un éditeur de l’espace de travail de créer la première base de connaissances.",
|
||||
"newKnowledge.startEmpty": "Commencer à vide",
|
||||
"newKnowledge.startEmptyDescription": "Créez-la maintenant et ajoutez du contenu plus tard.",
|
||||
"newKnowledge.tags": "Étiquettes",
|
||||
"newKnowledge.unavailableDescription": "KnowledgeFS n’est pas disponible pour cet espace de travail actuellement.",
|
||||
"newKnowledge.unavailableTitle": "Les nouvelles connaissances sont indisponibles",
|
||||
"newKnowledge.updated": "Mis à jour le {{date}}",
|
||||
"newKnowledge.uploadFiles": "Importer des fichiers",
|
||||
"newKnowledge.uploadFilesDescription": "Ajoutez des fichiers depuis votre ordinateur : ils sont indexés dès leur création.",
|
||||
"newKnowledge.viewLabel": "Vue des connaissances",
|
||||
"noExternalKnowledge": "Il n’y a pas encore d’API de connaissances externes, cliquez ici pour créer",
|
||||
"parentMode.fullDoc": "Doc complet",
|
||||
"parentMode.paragraph": "Paragraphe",
|
||||
|
||||
@ -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": "अनुच्छेद",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -131,6 +131,35 @@
|
||||
"mixtureHighQualityAndEconomicTip": "Il modello di riclassificazione è necessario per la miscela di basi di conoscenza di alta qualità ed economiche.",
|
||||
"mixtureInternalAndExternalTip": "Il modello Rerank è necessario per la combinazione di conoscenze interne ed esterne.",
|
||||
"multimodal": "Multimodale",
|
||||
"newKnowledge.appsUnavailable": "— app",
|
||||
"newKnowledge.cardType": "Spazio di conoscenza",
|
||||
"newKnowledge.connectSource": "Collega una fonte",
|
||||
"newKnowledge.connectSourceDescription": "I contenuti restano sincronizzati con Notion, siti web e altri servizi.",
|
||||
"newKnowledge.creators": "Autori",
|
||||
"newKnowledge.documentsUnavailable": "— documenti",
|
||||
"newKnowledge.emptyDescription": "Aggiungi i tuoi documenti e gli agenti potranno leggerli direttamente, senza configurare l’indicizzazione.",
|
||||
"newKnowledge.emptyTitle": "Un modo più intelligente per creare knowledge base",
|
||||
"newKnowledge.errorDescription": "Non è stato possibile caricare le nuove knowledge base. Riprova.",
|
||||
"newKnowledge.errorTitle": "Si è verificato un problema",
|
||||
"newKnowledge.filtersUnavailable": "Questo filtro sarà disponibile quando KnowledgeFS esporrà i relativi metadati.",
|
||||
"newKnowledge.gotIt": "Ho capito",
|
||||
"newKnowledge.guideDescription": "Collega una fonte e i suoi documenti resteranno sincronizzati, indicizzati automaticamente e pronti all’uso, senza nulla da gestire. Le knowledge base precedenti continueranno a funzionare come prima.",
|
||||
"newKnowledge.guideTitle": "Scopri la nuova Knowledge Base ✨",
|
||||
"newKnowledge.learnMore": "Scopri di più",
|
||||
"newKnowledge.legacy": "Legacy",
|
||||
"newKnowledge.loadMore": "Carica altro",
|
||||
"newKnowledge.new": "Nuovo",
|
||||
"newKnowledge.noDescription": "Nessuna descrizione",
|
||||
"newKnowledge.readOnlyEmpty": "Chiedi a un editor dello spazio di lavoro di creare la prima knowledge base.",
|
||||
"newKnowledge.startEmpty": "Inizia da zero",
|
||||
"newKnowledge.startEmptyDescription": "Creala ora e aggiungi i contenuti in seguito.",
|
||||
"newKnowledge.tags": "Tag",
|
||||
"newKnowledge.unavailableDescription": "KnowledgeFS non è al momento disponibile per questo spazio di lavoro.",
|
||||
"newKnowledge.unavailableTitle": "La nuova conoscenza non è disponibile",
|
||||
"newKnowledge.updated": "Aggiornato il {{date}}",
|
||||
"newKnowledge.uploadFiles": "Carica file",
|
||||
"newKnowledge.uploadFilesDescription": "Aggiungi file dal computer: vengono indicizzati appena li crei.",
|
||||
"newKnowledge.viewLabel": "Vista conoscenza",
|
||||
"noExternalKnowledge": "Non esiste ancora un'API di conoscenza esterna, fai clic qui per creare",
|
||||
"parentMode.fullDoc": "Full-doc",
|
||||
"parentMode.paragraph": "Paragrafo",
|
||||
|
||||
@ -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": "段落",
|
||||
|
||||
@ -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": "단락",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Параграф",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "วรรค",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Абзац",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "段落",
|
||||
|
||||
@ -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": "段",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user