mirror of
https://github.com/langgenius/dify.git
synced 2026-01-28 15:56:00 +08:00
Compare commits
50 Commits
feat/webap
...
fix/e-admi
| Author | SHA1 | Date | |
|---|---|---|---|
| 85b4386a02 | |||
| 1c2e8e1ce7 | |||
| 33d2c9d2ca | |||
| 4fa3d78ed8 | |||
| 849994d35e | |||
| 2fce4a338c | |||
| 5f7f851b17 | |||
| 559ab46ee1 | |||
| df98223c8c | |||
| 144f9507f8 | |||
| 2e097a1ac0 | |||
| 9f7d8a981f | |||
| c4729f8c20 | |||
| 5cb1cf9eca | |||
| 40b31bafd5 | |||
| d38a2c95fb | |||
| 7d18e2a0ef | |||
| 024f242251 | |||
| de14a55bde | |||
| cbb1d722a5 | |||
| 1769ce16f3 | |||
| 170139bb0f | |||
| ede0deb447 | |||
| d40f2e7d94 | |||
| 70ebfc064b | |||
| d6c252d77e | |||
| b786bbdab5 | |||
| f45321dd27 | |||
| 746d4d8ead | |||
| 7c31e3b6ba | |||
| 7c1116f139 | |||
| b82cc1c2e8 | |||
| fee51ba994 | |||
| 2259dfdc58 | |||
| a239e756b0 | |||
| ac54dd89f4 | |||
| 5310ed4b54 | |||
| bfdce78ca5 | |||
| 00c2258352 | |||
| 9f07584a00 | |||
| a1b3d41712 | |||
| 14f378bbc6 | |||
| 669fb6be0f | |||
| 724ffe55c9 | |||
| b26e20fe34 | |||
| 161ff432f1 | |||
| 99a9def623 | |||
| fe1846c437 | |||
| 8e75eb5c63 | |||
| 970508fcb6 |
2
.github/workflows/build-push.yml
vendored
2
.github/workflows/build-push.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- "main"
|
||||
- "deploy/dev"
|
||||
- "deploy/enterprise"
|
||||
- "e-0154"
|
||||
- "e-260"
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
|
||||
3
.markdownlint.json
Normal file
3
.markdownlint.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"MD024": false
|
||||
}
|
||||
32
CHANGELOG.md
Normal file
32
CHANGELOG.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Dify will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.15.7] - 2025-04-27
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for GPT-4.1 in model providers (#18912)
|
||||
- Added support for Amazon Bedrock DeepSeek-R1 model (#18908)
|
||||
- Added support for Amazon Bedrock Claude Sonnet 3.7 model (#18788)
|
||||
- Refined version compatibility logic in app DSL service
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with creating apps from template categories (#18807, #18868)
|
||||
- Fixed DSL version check when creating apps from explore templates (#18872, #18878)
|
||||
|
||||
## [0.15.6] - 2025-04-22
|
||||
|
||||
### Security
|
||||
|
||||
- Fixed clickjacking vulnerability (#18552)
|
||||
- Fixed reset password security issue (#18366)
|
||||
- Updated reset password token when email code verification succeeds (#18362)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Vertex AI Gemini 2.0 Flash 001 schema (#18405)
|
||||
@ -430,4 +430,7 @@ CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
||||
# Maximum number of submitted thread count in a ThreadPool for parallel node execution
|
||||
MAX_SUBMIT_COUNT=100
|
||||
# Lockout duration in seconds
|
||||
LOGIN_LOCKOUT_DURATION=86400
|
||||
LOGIN_LOCKOUT_DURATION=86400
|
||||
|
||||
# Prevent Clickjacking
|
||||
ALLOW_EMBED=false
|
||||
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
||||
|
||||
CURRENT_VERSION: str = Field(
|
||||
description="Dify version",
|
||||
default="0.15.4",
|
||||
default="0.15.7",
|
||||
)
|
||||
|
||||
COMMIT_SHA: str = Field(
|
||||
|
||||
@ -6,13 +6,9 @@ from flask_restful import Resource, reqparse # type: ignore
|
||||
|
||||
from constants.languages import languages
|
||||
from controllers.console import api
|
||||
from controllers.console.auth.error import (EmailCodeError, InvalidEmailError,
|
||||
InvalidTokenError,
|
||||
PasswordMismatchError)
|
||||
from controllers.console.error import (AccountInFreezeError, AccountNotFound,
|
||||
EmailSendIpLimitError)
|
||||
from controllers.console.wraps import (email_password_login_enabled,
|
||||
setup_required)
|
||||
from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError
|
||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
||||
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||
from events.tenant_event import tenant_was_created
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import email, extract_remote_ip
|
||||
@ -20,7 +16,7 @@ from libs.password import hash_password, valid_password
|
||||
from models.account import Account
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.errors.account import AccountRegisterError
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@ -145,6 +141,8 @@ class ForgotPasswordResetApi(Resource):
|
||||
pass
|
||||
except AccountRegisterError as are:
|
||||
raise AccountInFreezeError()
|
||||
except WorkspacesLimitExceededError:
|
||||
pass
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ from controllers.console.error import (
|
||||
AccountNotFound,
|
||||
EmailSendIpLimitError,
|
||||
NotAllowedCreateWorkspace,
|
||||
WorkspacesLimitExceeded,
|
||||
)
|
||||
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||
from events.tenant_event import tenant_was_created
|
||||
@ -30,7 +31,7 @@ from models.account import Account
|
||||
from services.account_service import AccountService, RegisterService, TenantService
|
||||
from services.billing_service import BillingService
|
||||
from services.errors.account import AccountRegisterError
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@ -88,10 +89,15 @@ class LoginApi(Resource):
|
||||
# SELF_HOSTED only have one workspace
|
||||
tenants = TenantService.get_join_tenants(account)
|
||||
if len(tenants) == 0:
|
||||
return {
|
||||
"result": "fail",
|
||||
"data": "workspace not found, please contact system admin to invite you to join in a workspace",
|
||||
}
|
||||
system_features = FeatureService.get_system_features()
|
||||
|
||||
if system_features.is_allow_create_workspace and not system_features.license.workspaces.is_available():
|
||||
raise WorkspacesLimitExceeded()
|
||||
else:
|
||||
return {
|
||||
"result": "fail",
|
||||
"data": "workspace not found, please contact system admin to invite you to join in a workspace",
|
||||
}
|
||||
|
||||
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
|
||||
AccountService.reset_login_error_rate_limit(args["email"])
|
||||
@ -198,6 +204,9 @@ class EmailCodeLoginApi(Resource):
|
||||
if account:
|
||||
tenant = TenantService.get_join_tenants(account)
|
||||
if not tenant:
|
||||
workspaces = FeatureService.get_system_features().license.workspaces
|
||||
if not workspaces.is_available():
|
||||
raise WorkspacesLimitExceeded()
|
||||
if not FeatureService.get_system_features().is_allow_create_workspace:
|
||||
raise NotAllowedCreateWorkspace()
|
||||
else:
|
||||
@ -215,6 +224,8 @@ class EmailCodeLoginApi(Resource):
|
||||
return NotAllowedCreateWorkspace()
|
||||
except AccountRegisterError as are:
|
||||
raise AccountInFreezeError()
|
||||
except WorkspacesLimitExceededError:
|
||||
raise WorkspacesLimitExceeded()
|
||||
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
||||
AccountService.reset_login_error_rate_limit(args["email"])
|
||||
return {"result": "success", "data": token_pair.model_dump()}
|
||||
|
||||
@ -46,6 +46,18 @@ class NotAllowedCreateWorkspace(BaseHTTPException):
|
||||
code = 400
|
||||
|
||||
|
||||
class WorkspaceMembersLimitExceeded(BaseHTTPException):
|
||||
error_code = "limit_exceeded"
|
||||
description = "Unable to add member because the maximum workspace's member limit was exceeded"
|
||||
code = 400
|
||||
|
||||
|
||||
class WorkspacesLimitExceeded(BaseHTTPException):
|
||||
error_code = "limit_exceeded"
|
||||
description = "Unable to create workspace because the maximum workspace limit was exceeded"
|
||||
code = 400
|
||||
|
||||
|
||||
class AccountBannedError(BaseHTTPException):
|
||||
error_code = "account_banned"
|
||||
description = "Account is banned."
|
||||
|
||||
@ -6,6 +6,7 @@ from flask_restful import Resource, abort, marshal_with, reqparse # type: ignor
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console.error import WorkspaceMembersLimitExceeded
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
@ -17,6 +18,7 @@ from libs.login import login_required
|
||||
from models.account import Account, TenantAccountRole
|
||||
from services.account_service import RegisterService, TenantService
|
||||
from services.errors.account import AccountAlreadyInTenantError
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
class MemberListApi(Resource):
|
||||
@ -54,6 +56,12 @@ class MemberInviteEmailApi(Resource):
|
||||
inviter = current_user
|
||||
invitation_results = []
|
||||
console_web_url = dify_config.CONSOLE_WEB_URL
|
||||
|
||||
workspace_members = FeatureService.get_features(tenant_id=inviter.current_tenant.id).workspace_members
|
||||
|
||||
if not workspace_members.is_available(len(invitee_emails)):
|
||||
raise WorkspaceMembersLimitExceeded()
|
||||
|
||||
for invitee_email in invitee_emails:
|
||||
try:
|
||||
token = RegisterService.invite_new_member(
|
||||
|
||||
@ -11,8 +11,7 @@ from models.model import DifySetup
|
||||
from services.feature_service import FeatureService, LicenseStatus
|
||||
from services.operation_service import OperationService
|
||||
|
||||
from .error import (NotInitValidateError, NotSetupError,
|
||||
UnauthorizedAndForceLogout)
|
||||
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
||||
|
||||
|
||||
def account_initialization_required(view):
|
||||
|
||||
@ -104,7 +104,6 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
||||
|
||||
# recalc llm max tokens
|
||||
prompt_messages = self._organize_prompt_messages()
|
||||
self.recalc_llm_max_tokens(self.model_config, prompt_messages)
|
||||
# invoke model
|
||||
chunks = model_instance.invoke_llm(
|
||||
prompt_messages=prompt_messages,
|
||||
|
||||
@ -84,7 +84,6 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
||||
|
||||
# recalc llm max tokens
|
||||
prompt_messages = self._organize_prompt_messages()
|
||||
self.recalc_llm_max_tokens(self.model_config, prompt_messages)
|
||||
# invoke model
|
||||
chunks: Union[Generator[LLMResultChunk, None, None], LLMResult] = model_instance.invoke_llm(
|
||||
prompt_messages=prompt_messages,
|
||||
|
||||
@ -55,20 +55,6 @@ class AgentChatAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
# Not Include: memory, external data, dataset context
|
||||
self.get_pre_calculate_rest_tokens(
|
||||
app_record=app_record,
|
||||
model_config=application_generate_entity.model_conf,
|
||||
prompt_template_entity=app_config.prompt_template,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
memory = None
|
||||
if application_generate_entity.conversation_id:
|
||||
# get memory of conversation (read-only)
|
||||
|
||||
@ -15,10 +15,8 @@ from core.app.features.annotation_reply.annotation_reply import AnnotationReplyF
|
||||
from core.app.features.hosting_moderation.hosting_moderation import HostingModerationFeature
|
||||
from core.external_data_tool.external_data_fetch import ExternalDataFetch
|
||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||
from core.model_manager import ModelInstance
|
||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
|
||||
from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessage
|
||||
from core.model_runtime.entities.model_entities import ModelPropertyKey
|
||||
from core.model_runtime.errors.invoke import InvokeBadRequestError
|
||||
from core.moderation.input_moderation import InputModeration
|
||||
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
|
||||
@ -31,106 +29,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class AppRunner:
|
||||
def get_pre_calculate_rest_tokens(
|
||||
self,
|
||||
app_record: App,
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
prompt_template_entity: PromptTemplateEntity,
|
||||
inputs: Mapping[str, str],
|
||||
files: Sequence["File"],
|
||||
query: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Get pre calculate rest tokens
|
||||
:param app_record: app record
|
||||
:param model_config: model config entity
|
||||
:param prompt_template_entity: prompt template entity
|
||||
:param inputs: inputs
|
||||
:param files: files
|
||||
:param query: query
|
||||
:return:
|
||||
"""
|
||||
# Invoke model
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
|
||||
)
|
||||
|
||||
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
|
||||
|
||||
max_tokens = 0
|
||||
for parameter_rule in model_config.model_schema.parameter_rules:
|
||||
if parameter_rule.name == "max_tokens" or (
|
||||
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
|
||||
):
|
||||
max_tokens = (
|
||||
model_config.parameters.get(parameter_rule.name)
|
||||
or model_config.parameters.get(parameter_rule.use_template or "")
|
||||
) or 0
|
||||
|
||||
if model_context_tokens is None:
|
||||
return -1
|
||||
|
||||
if max_tokens is None:
|
||||
max_tokens = 0
|
||||
|
||||
# get prompt messages without memory and context
|
||||
prompt_messages, stop = self.organize_prompt_messages(
|
||||
app_record=app_record,
|
||||
model_config=model_config,
|
||||
prompt_template_entity=prompt_template_entity,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
|
||||
|
||||
rest_tokens: int = model_context_tokens - max_tokens - prompt_tokens
|
||||
if rest_tokens < 0:
|
||||
raise InvokeBadRequestError(
|
||||
"Query or prefix prompt is too long, you can reduce the prefix prompt, "
|
||||
"or shrink the max token, or switch to a llm with a larger token limit size."
|
||||
)
|
||||
|
||||
return rest_tokens
|
||||
|
||||
def recalc_llm_max_tokens(
|
||||
self, model_config: ModelConfigWithCredentialsEntity, prompt_messages: list[PromptMessage]
|
||||
):
|
||||
# recalc max_tokens if sum(prompt_token + max_tokens) over model token limit
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
|
||||
)
|
||||
|
||||
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
|
||||
|
||||
max_tokens = 0
|
||||
for parameter_rule in model_config.model_schema.parameter_rules:
|
||||
if parameter_rule.name == "max_tokens" or (
|
||||
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
|
||||
):
|
||||
max_tokens = (
|
||||
model_config.parameters.get(parameter_rule.name)
|
||||
or model_config.parameters.get(parameter_rule.use_template or "")
|
||||
) or 0
|
||||
|
||||
if model_context_tokens is None:
|
||||
return -1
|
||||
|
||||
if max_tokens is None:
|
||||
max_tokens = 0
|
||||
|
||||
prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
|
||||
|
||||
if prompt_tokens + max_tokens > model_context_tokens:
|
||||
max_tokens = max(model_context_tokens - prompt_tokens, 16)
|
||||
|
||||
for parameter_rule in model_config.model_schema.parameter_rules:
|
||||
if parameter_rule.name == "max_tokens" or (
|
||||
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
|
||||
):
|
||||
model_config.parameters[parameter_rule.name] = max_tokens
|
||||
|
||||
def organize_prompt_messages(
|
||||
self,
|
||||
app_record: App,
|
||||
|
||||
@ -50,20 +50,6 @@ class ChatAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
# Not Include: memory, external data, dataset context
|
||||
self.get_pre_calculate_rest_tokens(
|
||||
app_record=app_record,
|
||||
model_config=application_generate_entity.model_conf,
|
||||
prompt_template_entity=app_config.prompt_template,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
memory = None
|
||||
if application_generate_entity.conversation_id:
|
||||
# get memory of conversation (read-only)
|
||||
@ -194,9 +180,6 @@ class ChatAppRunner(AppRunner):
|
||||
if hosting_moderation_result:
|
||||
return
|
||||
|
||||
# Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit
|
||||
self.recalc_llm_max_tokens(model_config=application_generate_entity.model_conf, prompt_messages=prompt_messages)
|
||||
|
||||
# Invoke model
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
|
||||
|
||||
@ -43,20 +43,6 @@ class CompletionAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
# Not Include: memory, external data, dataset context
|
||||
self.get_pre_calculate_rest_tokens(
|
||||
app_record=app_record,
|
||||
model_config=application_generate_entity.model_conf,
|
||||
prompt_template_entity=app_config.prompt_template,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
# organize all inputs and template to prompt messages
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
prompt_messages, stop = self.organize_prompt_messages(
|
||||
@ -152,9 +138,6 @@ class CompletionAppRunner(AppRunner):
|
||||
if hosting_moderation_result:
|
||||
return
|
||||
|
||||
# Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit
|
||||
self.recalc_llm_max_tokens(model_config=application_generate_entity.model_conf, prompt_messages=prompt_messages)
|
||||
|
||||
# Invoke model
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
|
||||
|
||||
@ -26,7 +26,7 @@ class TokenBufferMemory:
|
||||
self.model_instance = model_instance
|
||||
|
||||
def get_history_prompt_messages(
|
||||
self, max_token_limit: int = 2000, message_limit: Optional[int] = None
|
||||
self, max_token_limit: int = 100000, message_limit: Optional[int] = None
|
||||
) -> Sequence[PromptMessage]:
|
||||
"""
|
||||
Get history prompt messages.
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
model: us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
label:
|
||||
en_US: Claude 3.7 Sonnet(US.Cross Region Inference)
|
||||
icon: icon_s_en.svg
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 200000
|
||||
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
|
||||
parameter_rules:
|
||||
- name: enable_cache
|
||||
label:
|
||||
zh_Hans: 启用提示缓存
|
||||
en_US: Enable Prompt Cache
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
help:
|
||||
zh_Hans: 启用提示缓存可以提高性能并降低成本。Claude 3.7 Sonnet支持在system、messages和tools字段中使用缓存检查点。
|
||||
en_US: Enable prompt caching to improve performance and reduce costs. Claude 3.7 Sonnet supports cache checkpoints in system, messages, and tools fields.
|
||||
- name: reasoning_type
|
||||
label:
|
||||
zh_Hans: 推理配置
|
||||
en_US: Reasoning Type
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
placeholder:
|
||||
zh_Hans: 设置推理配置
|
||||
en_US: Set reasoning configuration
|
||||
help:
|
||||
zh_Hans: 控制模型的推理能力。启用时,temperature将固定为1且top_p将被禁用。
|
||||
en_US: Controls the model's reasoning capability. When enabled, temperature will be fixed to 1 and top_p will be disabled.
|
||||
- name: reasoning_budget
|
||||
show_on:
|
||||
- variable: reasoning_type
|
||||
value: true
|
||||
label:
|
||||
zh_Hans: 推理预算
|
||||
en_US: Reasoning Budget
|
||||
type: int
|
||||
default: 1024
|
||||
min: 0
|
||||
max: 128000
|
||||
help:
|
||||
zh_Hans: 推理的预算限制(最小1024),必须小于max_tokens。仅在推理类型为enabled时可用。
|
||||
en_US: Budget limit for reasoning (minimum 1024), must be less than max_tokens. Only available when reasoning type is enabled.
|
||||
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
label:
|
||||
zh_Hans: 最大token数
|
||||
en_US: Max Tokens
|
||||
type: int
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 128000
|
||||
help:
|
||||
zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
|
||||
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
required: false
|
||||
label:
|
||||
zh_Hans: 模型温度
|
||||
en_US: Model Temperature
|
||||
type: float
|
||||
default: 1
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
help:
|
||||
zh_Hans: 生成内容的随机性。当推理功能启用时,该值将被固定为1。
|
||||
en_US: The amount of randomness injected into the response. When reasoning is enabled, this value will be fixed to 1.
|
||||
- name: top_p
|
||||
show_on:
|
||||
- variable: reasoning_type
|
||||
value: disabled
|
||||
use_template: top_p
|
||||
label:
|
||||
zh_Hans: Top P
|
||||
en_US: Top P
|
||||
required: false
|
||||
type: float
|
||||
default: 0.999
|
||||
min: 0.000
|
||||
max: 1.000
|
||||
help:
|
||||
zh_Hans: 在核采样中的概率阈值。当推理功能启用时,该参数将被禁用。
|
||||
en_US: The probability threshold in nucleus sampling. When reasoning is enabled, this parameter will be disabled.
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
required: false
|
||||
type: int
|
||||
default: 0
|
||||
min: 0
|
||||
# tip docs from aws has error, max value is 500
|
||||
max: 500
|
||||
help:
|
||||
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
|
||||
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
|
||||
- name: response_format
|
||||
use_template: response_format
|
||||
pricing:
|
||||
input: '0.003'
|
||||
output: '0.015'
|
||||
unit: '0.001'
|
||||
currency: USD
|
||||
@ -58,6 +58,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
|
||||
# TODO There is invoke issue: context limit on Cohere Model, will add them after fixed.
|
||||
CONVERSE_API_ENABLED_MODEL_INFO = [
|
||||
{"prefix": "anthropic.claude-v2", "support_system_prompts": True, "support_tool_use": False},
|
||||
{"prefix": "us.deepseek", "support_system_prompts": True, "support_tool_use": False},
|
||||
{"prefix": "anthropic.claude-v1", "support_system_prompts": True, "support_tool_use": False},
|
||||
{"prefix": "us.anthropic.claude-3", "support_system_prompts": True, "support_tool_use": True},
|
||||
{"prefix": "eu.anthropic.claude-3", "support_system_prompts": True, "support_tool_use": True},
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
model: us.deepseek.r1-v1:0
|
||||
label:
|
||||
en_US: DeepSeek-R1(US.Cross Region Inference)
|
||||
icon: icon_s_en.svg
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 32768
|
||||
parameter_rules:
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
label:
|
||||
zh_Hans: 最大token数
|
||||
en_US: Max Tokens
|
||||
type: int
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 128000
|
||||
help:
|
||||
zh_Hans: 停止前生成的最大令牌数。
|
||||
en_US: The maximum number of tokens to generate before stopping.
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
required: false
|
||||
label:
|
||||
zh_Hans: 模型温度
|
||||
en_US: Model Temperature
|
||||
type: float
|
||||
default: 1
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
help:
|
||||
zh_Hans: 生成内容的随机性。当推理功能启用时,该值将被固定为1。
|
||||
en_US: The amount of randomness injected into the response. When reasoning is enabled, this value will be fixed to 1.
|
||||
- name: top_p
|
||||
show_on:
|
||||
- variable: reasoning_type
|
||||
value: disabled
|
||||
use_template: top_p
|
||||
label:
|
||||
zh_Hans: Top P
|
||||
en_US: Top P
|
||||
required: false
|
||||
type: float
|
||||
default: 0.999
|
||||
min: 0.000
|
||||
max: 1.000
|
||||
help:
|
||||
zh_Hans: 在核采样中的概率阈值。当推理功能启用时,该参数将被禁用。
|
||||
en_US: The probability threshold in nucleus sampling. When reasoning is enabled, this parameter will be disabled.
|
||||
- name: response_format
|
||||
use_template: response_format
|
||||
pricing:
|
||||
input: '0.001'
|
||||
output: '0.005'
|
||||
unit: '0.001'
|
||||
currency: USD
|
||||
@ -19,8 +19,8 @@ class GoogleProvider(ModelProvider):
|
||||
try:
|
||||
model_instance = self.get_model_instance(ModelType.LLM)
|
||||
|
||||
# Use `gemini-pro` model for validate,
|
||||
model_instance.validate_credentials(model="gemini-pro", credentials=credentials)
|
||||
# Use `gemini-2.0-flash` model for validate,
|
||||
model_instance.validate_credentials(model="gemini-2.0-flash", credentials=credentials)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ex
|
||||
except Exception as ex:
|
||||
|
||||
@ -19,5 +19,3 @@
|
||||
- gemini-exp-1206
|
||||
- gemini-exp-1121
|
||||
- gemini-exp-1114
|
||||
- gemini-pro
|
||||
- gemini-pro-vision
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
model: gemini-pro-vision
|
||||
label:
|
||||
en_US: Gemini Pro Vision
|
||||
model_type: llm
|
||||
features:
|
||||
- vision
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 12288
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_tokens_to_sample
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 4096
|
||||
min: 1
|
||||
max: 4096
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
deprecated: true
|
||||
@ -1,39 +0,0 @@
|
||||
model: gemini-pro
|
||||
label:
|
||||
en_US: Gemini Pro
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 30720
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_tokens_to_sample
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 2048
|
||||
min: 1
|
||||
max: 2048
|
||||
- name: response_format
|
||||
use_template: response_format
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
deprecated: true
|
||||
@ -1057,7 +1057,7 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel):
|
||||
model = "gpt-4o"
|
||||
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model)
|
||||
encoding = tiktoken.get_encoding(model)
|
||||
except KeyError:
|
||||
logger.warning("Warning: model not found. Using cl100k_base encoding.")
|
||||
model = "cl100k_base"
|
||||
|
||||
@ -5,11 +5,6 @@ model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 1048576
|
||||
@ -20,20 +15,21 @@ parameter_rules:
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: presence_penalty
|
||||
use_template: presence_penalty
|
||||
- name: frequency_penalty
|
||||
use_template: frequency_penalty
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
|
||||
@ -85,7 +85,7 @@ class WordExtractor(BaseExtractor):
|
||||
if "image" in rel.target_ref:
|
||||
image_count += 1
|
||||
if rel.is_external:
|
||||
url = rel.reltype
|
||||
url = rel.target_ref
|
||||
response = ssrf_proxy.get(url)
|
||||
if response.status_code == 200:
|
||||
image_ext = mimetypes.guess_extension(response.headers["Content-Type"])
|
||||
|
||||
@ -195,7 +195,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
if output_config.type == "object":
|
||||
# check if output is object
|
||||
if not isinstance(result.get(output_name), dict):
|
||||
if isinstance(result.get(output_name), type(None)):
|
||||
if result.get(output_name) is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
@ -223,7 +223,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
elif output_config.type == "array[number]":
|
||||
# check if array of number available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
@ -244,7 +244,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
elif output_config.type == "array[string]":
|
||||
# check if array of string available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
@ -265,7 +265,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
elif output_config.type == "array[object]":
|
||||
# check if array of object available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
|
||||
@ -968,14 +968,12 @@ def _handle_memory_chat_mode(
|
||||
*,
|
||||
memory: TokenBufferMemory | None,
|
||||
memory_config: MemoryConfig | None,
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
model_config: ModelConfigWithCredentialsEntity, # TODO(-LAN-): Needs to remove
|
||||
) -> Sequence[PromptMessage]:
|
||||
memory_messages: Sequence[PromptMessage] = []
|
||||
# Get messages from memory for chat model
|
||||
if memory and memory_config:
|
||||
rest_tokens = _calculate_rest_token(prompt_messages=[], model_config=model_config)
|
||||
memory_messages = memory.get_history_prompt_messages(
|
||||
max_token_limit=rest_tokens,
|
||||
message_limit=memory_config.window.size if memory_config.window.enabled else None,
|
||||
)
|
||||
return memory_messages
|
||||
|
||||
66
api/poetry.lock
generated
66
api/poetry.lock
generated
@ -10473,44 +10473,44 @@ client = ["SQLAlchemy (>=1.4,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:881839cfeae051b3628d9823b2e56b5cc93a9e2efb435f4cf15f17dc45f21586"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe9399bdc3f29d428f16a2f86c3c8ec20be3eac5f53693ce4980371c3245729b"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a58deb7075d5b69237a3ff4bb51a726670419db6ea62bdcd8bd80c78497d7ab"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2908c0d043a7d03ebd80347266b0e58440bdef5564f84f4d29fb235b5df3b04"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:294440d21a2a51e12d4238e68a5972095534fe9878be57d905c476017bff99fc"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:d8f3192733ac4d77977432947d563d7e1b310b96497acd3c196c9bddb36ed9db"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:02be1666096aff7da6cbd7cdaa8e7917bfed3467cd64b38b1f112e96d3b06a24"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94ff53c5c74b535b2cbf431d907fc13c678bbd009ee633a2aca269a04389f9a"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b231f5e8982c245ee3065cd84a4712d64692348bc609d84467c57b4b72dcbc5"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4177faa809bd55f699e88c96d9bb4635d22e3f59d635ba6fd9ffedf7150b9953"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5376b6f8dc4753cd81ead935c5f518fa0fbe7e133d9e25f648d8c4dabdd4bad7"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:18228d624807d66c87acd8f25fc135665617cab220671eb65b50f5d70fa51f69"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17807445f0cf1f25771c9d86496bd8b5c376f7419912519699f3cc4dc5c12e"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:886f80bd339578bbdba6ed6d0567a0d5c6cfe198d9e587ba6c447654c65b8edc"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6adc8323016d7758d6de7313527f755b0fc6c72985b7d9291be5d96d73ecd1e1"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b591fb2b30d6a72121a80be24ec7a0e9eb51c5500ddc7e4c2496516dd5e3816b"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:845287b9798e476b4d762c3ebda5102be87ca26e5d2c9854002825d60cdb815d"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02"},
|
||||
{file = "tiktoken-0.8.0.tar.gz", hash = "sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c6386ca815e7d96ef5b4ac61e0048cd32ca5a92d5781255e13b31381d28667dc"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75f6d5db5bc2c6274b674ceab1615c1778e6416b14705827d19b40e6355f03e0"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b16f61e6f4625a57a36496d28dd182a8a60ec20a534c5343ba3cafa156ac7"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebcec91babf21297022882344c3f7d9eed855931466c3311b1ad6b64befb3df"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e5fd49e7799579240f03913447c0cdfa1129625ebd5ac440787afc4345990427"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:26242ca9dc8b58e875ff4ca078b9a94d2f0813e6a535dcd2205df5d49d927cc7"},
|
||||
{file = "tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -12389,4 +12389,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "d197cdff507a70323c1d6aca11609188f54970f67715af744fe6def15b7776fd"
|
||||
content-hash = "0df8aef68385b6596306fd18af317a835023d648eb5028cd57ec463f176e4c0f"
|
||||
|
||||
@ -85,7 +85,7 @@ sentry-sdk = { version = "~1.44.1", extras = ["flask"] }
|
||||
sqlalchemy = "~2.0.29"
|
||||
starlette = "0.41.0"
|
||||
tencentcloud-sdk-python-hunyuan = "~3.0.1294"
|
||||
tiktoken = "~0.8.0"
|
||||
tiktoken = "^0.9.0"
|
||||
tokenizers = "~0.15.0"
|
||||
transformers = "~4.35.0"
|
||||
unstructured = { version = "~0.16.1", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"] }
|
||||
|
||||
@ -49,7 +49,7 @@ from services.errors.account import (
|
||||
RoleAlreadyAssignedError,
|
||||
TenantNotFoundError,
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
from tasks.delete_account_task import delete_account_task
|
||||
from tasks.mail_account_deletion_task import send_account_deletion_verification_code
|
||||
@ -599,6 +599,10 @@ class TenantService:
|
||||
if not FeatureService.get_system_features().is_allow_create_workspace and not is_setup:
|
||||
raise WorkSpaceNotAllowedCreateError()
|
||||
|
||||
workspaces = FeatureService.get_system_features().license.workspaces
|
||||
if not workspaces.is_available():
|
||||
raise WorkspacesLimitExceededError()
|
||||
|
||||
if name:
|
||||
tenant = TenantService.create_tenant(name=name, is_setup=is_setup)
|
||||
else:
|
||||
@ -754,8 +758,8 @@ class TenantService:
|
||||
"""Check member permission"""
|
||||
perms = {
|
||||
"add": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
"remove": [TenantAccountRole.OWNER],
|
||||
"update": [TenantAccountRole.OWNER],
|
||||
"remove": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
"update": [TenantAccountRole.OWNER, TenantAccountRole.ADMIN],
|
||||
}
|
||||
if action not in {"add", "remove", "update"}:
|
||||
raise InvalidActionError("Invalid action.")
|
||||
@ -768,13 +772,24 @@ class TenantService:
|
||||
|
||||
if not ta_operator or ta_operator.role not in perms[action]:
|
||||
raise NoPermissionError(f"No permission to {action} member.")
|
||||
|
||||
# Admin cannot remove or update other admin and the owner
|
||||
if action in {"remove", "update"}:
|
||||
if ta_operator.role == TenantAccountRole.ADMIN:
|
||||
if member:
|
||||
ta_member = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=member.id).first()
|
||||
if not ta_member or ta_member.role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN}:
|
||||
raise NoPermissionError(f"No permission to {action} member.")
|
||||
|
||||
|
||||
@staticmethod
|
||||
def remove_member_from_tenant(tenant: Tenant, account: Account, operator: Account) -> None:
|
||||
"""Remove member from tenant"""
|
||||
if operator.id == account.id and TenantService.check_member_permission(tenant, operator, account, "remove"):
|
||||
if operator.id == account.id:
|
||||
raise CannotOperateSelfError("Cannot operate self.")
|
||||
|
||||
TenantService.check_member_permission(tenant, operator, account, "remove")
|
||||
|
||||
ta = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=account.id).first()
|
||||
if not ta:
|
||||
raise MemberNotInTenantError("Member not in tenant.")
|
||||
@ -889,7 +904,10 @@ class RegisterService:
|
||||
if open_id is not None and provider is not None:
|
||||
AccountService.link_account_integrate(provider, open_id, account)
|
||||
|
||||
if FeatureService.get_system_features().is_allow_create_workspace and create_workspace_required:
|
||||
if (FeatureService.get_system_features().is_allow_create_workspace
|
||||
and create_workspace_required
|
||||
and FeatureService.get_system_features().license.workspaces.is_available()
|
||||
):
|
||||
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
|
||||
TenantService.create_tenant_member(tenant, account, role="owner")
|
||||
account.current_tenant = tenant
|
||||
|
||||
@ -55,13 +55,19 @@ def _check_version_compatibility(imported_version: str) -> ImportStatus:
|
||||
except version.InvalidVersion:
|
||||
return ImportStatus.FAILED
|
||||
|
||||
# Compare major version and minor version
|
||||
if current_ver.major != imported_ver.major or current_ver.minor != imported_ver.minor:
|
||||
# If imported version is newer than current, always return PENDING
|
||||
if imported_ver > current_ver:
|
||||
return ImportStatus.PENDING
|
||||
|
||||
if current_ver.micro != imported_ver.micro:
|
||||
# If imported version is older than current's major, return PENDING
|
||||
if imported_ver.major < current_ver.major:
|
||||
return ImportStatus.PENDING
|
||||
|
||||
# If imported version is older than current's minor, return COMPLETED_WITH_WARNINGS
|
||||
if imported_ver.minor < current_ver.minor:
|
||||
return ImportStatus.COMPLETED_WITH_WARNINGS
|
||||
|
||||
# If imported version equals or is older than current's micro, return COMPLETED
|
||||
return ImportStatus.COMPLETED
|
||||
|
||||
|
||||
|
||||
@ -17,6 +17,10 @@ class EnterpriseService:
|
||||
def get_info(cls):
|
||||
return EnterpriseRequest.send_request("GET", "/info")
|
||||
|
||||
@classmethod
|
||||
def get_workspace_info(cls, tenant_id:str):
|
||||
return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info")
|
||||
|
||||
class WebAppAuth:
|
||||
@classmethod
|
||||
def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str) -> bool:
|
||||
|
||||
@ -7,3 +7,7 @@ class WorkSpaceNotAllowedCreateError(BaseServiceError):
|
||||
|
||||
class WorkSpaceNotFoundError(BaseServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class WorkspacesLimitExceededError(BaseServiceError):
|
||||
pass
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from configs import dify_config
|
||||
from services.billing_service import BillingService
|
||||
@ -22,6 +22,32 @@ class LimitationModel(BaseModel):
|
||||
limit: int = 0
|
||||
|
||||
|
||||
class LicenseLimitationModel(BaseModel):
|
||||
"""
|
||||
- enabled: whether this limit is enforced
|
||||
- size: current usage count
|
||||
- limit: maximum allowed count; 0 means unlimited
|
||||
"""
|
||||
|
||||
enabled: bool = Field(False, description="Whether this limit is currently active")
|
||||
size: int = Field(0, description="Number of resources already consumed")
|
||||
limit: int = Field(0, description="Maximum number of resources allowed; 0 means no limit")
|
||||
|
||||
def is_available(self, required: int = 1) -> bool:
|
||||
"""
|
||||
Determine whether the requested amount can be allocated.
|
||||
|
||||
Returns True if:
|
||||
- this limit is not active, or
|
||||
- the limit is zero (unlimited), or
|
||||
- there is enough remaining quota.
|
||||
"""
|
||||
if not self.enabled or self.limit == 0:
|
||||
return True
|
||||
|
||||
return (self.limit - self.size) >= required
|
||||
|
||||
|
||||
class LicenseStatus(StrEnum):
|
||||
NONE = "none"
|
||||
INACTIVE = "inactive"
|
||||
@ -34,6 +60,7 @@ class LicenseStatus(StrEnum):
|
||||
class LicenseModel(BaseModel):
|
||||
status: LicenseStatus = LicenseStatus.NONE
|
||||
expired_at: str = ""
|
||||
workspaces: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
||||
|
||||
|
||||
class BrandingModel(BaseModel):
|
||||
@ -68,6 +95,7 @@ class FeatureModel(BaseModel):
|
||||
model_load_balancing_enabled: bool = False
|
||||
dataset_operator_enabled: bool = False
|
||||
webapp_copyright_enabled: bool = False
|
||||
workspace_members: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
||||
|
||||
# pydantic configs
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
@ -99,6 +127,7 @@ class FeatureService:
|
||||
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
features.webapp_copyright_enabled = True
|
||||
cls._fulfill_params_from_workspace_info(features, tenant_id)
|
||||
|
||||
return features
|
||||
|
||||
@ -130,6 +159,14 @@ class FeatureService:
|
||||
features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED
|
||||
features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_workspace_info(cls, features: FeatureModel, tenant_id: str):
|
||||
workspace_info = EnterpriseService.get_workspace_info(tenant_id)
|
||||
if "WorkspaceMembers" in workspace_info:
|
||||
features.workspace_members.size = workspace_info["WorkspaceMembers"]["used"]
|
||||
features.workspace_members.limit = workspace_info["WorkspaceMembers"]["limit"]
|
||||
features.workspace_members.enabled = workspace_info["WorkspaceMembers"]["enabled"]
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str):
|
||||
billing_info = BillingService.get_info(tenant_id)
|
||||
@ -177,6 +214,9 @@ class FeatureService:
|
||||
if "SSOEnforcedForSignin" in enterprise_info:
|
||||
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
|
||||
|
||||
if "SSOEnforcedForSigninProtocol" in enterprise_info:
|
||||
features.sso_enforced_for_signin_protocol = enterprise_info["SSOEnforcedForSigninProtocol"]
|
||||
|
||||
if "EnableEmailCodeLogin" in enterprise_info:
|
||||
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
|
||||
|
||||
@ -213,3 +253,8 @@ class FeatureService:
|
||||
|
||||
if "expiredAt" in license_info:
|
||||
features.license.expired_at = license_info["expiredAt"]
|
||||
|
||||
if "workspaces" in license_info:
|
||||
features.license.workspaces.enabled = license_info["workspaces"]["enabled"]
|
||||
features.license.workspaces.limit = license_info["workspaces"]["limit"]
|
||||
features.license.workspaces.size = license_info["workspaces"]["used"]
|
||||
|
||||
@ -932,3 +932,6 @@ MAX_SUBMIT_COUNT=100
|
||||
|
||||
# The maximum number of top-k value for RAG.
|
||||
TOP_K_MAX_VALUE=10
|
||||
|
||||
# Prevent Clickjacking
|
||||
ALLOW_EMBED=false
|
||||
@ -1,8 +1,8 @@
|
||||
x-shared-env: &shared-api-worker-env
|
||||
x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -25,7 +25,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -47,7 +47,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.15.4
|
||||
image: langgenius/dify-web:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -56,6 +56,7 @@ services:
|
||||
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
||||
|
||||
|
||||
@ -389,11 +389,12 @@ x-shared-env: &shared-api-worker-env
|
||||
CREATE_TIDB_SERVICE_JOB_ENABLED: ${CREATE_TIDB_SERVICE_JOB_ENABLED:-false}
|
||||
MAX_SUBMIT_COUNT: ${MAX_SUBMIT_COUNT:-100}
|
||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-10}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -416,7 +417,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -438,7 +439,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.15.4
|
||||
image: langgenius/dify-web:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -447,6 +448,7 @@ services:
|
||||
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
||||
|
||||
|
||||
@ -31,3 +31,6 @@ NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
||||
|
||||
# The maximum number of tokens for segmentation
|
||||
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
|
||||
# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||
NEXT_PUBLIC_ALLOW_EMBED=
|
||||
|
||||
@ -20,7 +20,7 @@ import cn from '@/utils/classnames'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
@ -32,6 +32,13 @@ export type IAppDetailLayoutProps = {
|
||||
params: { appId: string }
|
||||
}
|
||||
|
||||
type NavigationType = {
|
||||
name: string
|
||||
href: string
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
}
|
||||
|
||||
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
@ -50,12 +57,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
})))
|
||||
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
|
||||
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
|
||||
const [navigation, setNavigation] = useState<Array<{
|
||||
name: string
|
||||
href: string
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
}>>([])
|
||||
const [navigation, setNavigation] = useState<Array<NavigationType>>([])
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
|
||||
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
|
||||
@ -142,15 +144,10 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
router.replace(`/app/${appId}/configuration`)
|
||||
}
|
||||
else {
|
||||
setAppDetail({ ...res, enable_sso: false })
|
||||
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
|
||||
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
|
||||
fetchAppSSO({ appId }).then((ssoRes) => {
|
||||
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
||||
})
|
||||
}
|
||||
setAppDetail({ ...res })
|
||||
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode) as Array<NavigationType>)
|
||||
}
|
||||
}, [appDetailRes, appId, getNavigations, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail, systemFeatures.enable_web_sso_switch_component])
|
||||
}, [appDetailRes, appId, getNavigations, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail])
|
||||
|
||||
useUnmount(() => {
|
||||
setAppDetail()
|
||||
|
||||
@ -8,19 +8,16 @@ import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
fetchAppSSO,
|
||||
updateAppSSO,
|
||||
updateAppSiteAccessToken,
|
||||
updateAppSiteConfig,
|
||||
updateAppSiteStatus,
|
||||
} from '@/service/apps'
|
||||
import type { App, AppSSO } from '@/types/app'
|
||||
import type { App } from '@/types/app'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
@ -31,18 +28,11 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appId })
|
||||
if (systemFeatures.enable_web_sso_switch_component) {
|
||||
const ssoRes = await fetchAppSSO({ appId })
|
||||
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
||||
}
|
||||
else {
|
||||
setAppDetail({ ...res })
|
||||
}
|
||||
setAppDetail({ ...res })
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
}
|
||||
@ -93,16 +83,6 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||
if (!err)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
|
||||
if (systemFeatures.enable_web_sso_switch_component) {
|
||||
const [sso_err] = await asyncRunSafe<AppSSO>(
|
||||
updateAppSSO({ id: appId, enabled: Boolean(params.enable_sso) }) as Promise<AppSSO>,
|
||||
)
|
||||
if (sso_err) {
|
||||
handleCallbackResult(sso_err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleCallbackResult(err)
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { App } from '@/types/app'
|
||||
@ -31,6 +31,9 @@ import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import AccessControl from '@/app/components/app/app-access-control'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
@ -53,6 +56,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
||||
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [showAccessControl, setShowAccessControl] = useState(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
@ -71,7 +75,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
})
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [app.id])
|
||||
}, [app.id, mutateApps, notify, onPlanInfoChanged, onRefresh, t])
|
||||
|
||||
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
|
||||
name,
|
||||
@ -175,6 +179,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
setShowSwitchModal(false)
|
||||
}
|
||||
|
||||
const onUpdateAccessControl = useCallback(() => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
mutateApps()
|
||||
setShowAccessControl(false)
|
||||
}, [onRefresh, mutateApps, setShowAccessControl])
|
||||
|
||||
const Operations = (props: HtmlContentProps) => {
|
||||
const onMouseLeave = async () => {
|
||||
props.onClose?.()
|
||||
@ -209,6 +220,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
const onClickAccessControl = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowAccessControl(true)
|
||||
}
|
||||
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
@ -252,6 +269,14 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<span className={s.actionName}>{t('app.openInExplore')}</span>
|
||||
</button>
|
||||
<Divider className="!my-1" />
|
||||
{
|
||||
isCurrentWorkspaceEditor && <>
|
||||
<button className={s.actionItem} onClick={onClickAccessControl}>
|
||||
<span className={s.actionName}>{t('app.accessControl')}</span>
|
||||
</button>
|
||||
<Divider />
|
||||
</>
|
||||
}
|
||||
<div
|
||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||
onClick={onClickDelete}
|
||||
@ -278,7 +303,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
}}
|
||||
className='relative h-[160px] group col-span-1 bg-components-card-bg border-[1px] border-solid border-components-card-border rounded-xl shadow-sm inline-flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
|
||||
>
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<div className='flex p-4 pb-3 h-[68px] items-start gap-3 grow-0 shrink-0'>
|
||||
<div className='relative shrink-0'>
|
||||
<AppIcon
|
||||
size="large"
|
||||
@ -301,6 +326,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 w-5 h-5 flex items-center justify-center'>
|
||||
{app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}>
|
||||
<RiGlobalLine className='text-text-accent w-4 h-4' />
|
||||
</Tooltip>}
|
||||
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}>
|
||||
<RiLockLine className='text-text-quaternary w-4 h-4' />
|
||||
</Tooltip>}
|
||||
{app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}>
|
||||
<RiBuildingLine className='text-text-quaternary w-4 h-4' />
|
||||
</Tooltip>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
|
||||
<div
|
||||
@ -357,7 +393,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
popupClassName={
|
||||
(app.mode === 'completion' || app.mode === 'chat')
|
||||
? '!w-[256px] translate-x-[-224px]'
|
||||
: '!w-[160px] translate-x-[-128px]'
|
||||
: '!w-[216px] translate-x-[-128px]'
|
||||
}
|
||||
className={'h-fit !z-20'}
|
||||
/>
|
||||
@ -418,6 +454,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
{showAccessControl && (
|
||||
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
'use client'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDoorLockLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { fetchSystemFeatures, fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
|
||||
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
|
||||
import { setAccessToken } from '@/app/components/share/utils'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { SSOProtocol } from '@/types/feature'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
|
||||
const WebSSOForm: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
|
||||
@ -23,15 +30,15 @@ const WebSSOForm: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const getAppCodeFromRedirectUrl = () => {
|
||||
const getAppCodeFromRedirectUrl = useCallback(() => {
|
||||
const appCode = redirectUrl?.split('/').pop()
|
||||
if (!appCode)
|
||||
return null
|
||||
|
||||
return appCode
|
||||
}
|
||||
}, [redirectUrl])
|
||||
|
||||
const processTokenAndRedirect = async () => {
|
||||
const processTokenAndRedirect = useCallback(async () => {
|
||||
const appCode = getAppCodeFromRedirectUrl()
|
||||
if (!appCode || !tokenFromUrl || !redirectUrl) {
|
||||
showErrorToast('redirect url or app code or token is invalid.')
|
||||
@ -40,48 +47,47 @@ const WebSSOForm: FC = () => {
|
||||
|
||||
await setAccessToken(appCode, tokenFromUrl)
|
||||
router.push(redirectUrl)
|
||||
}
|
||||
}, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
|
||||
|
||||
const handleSSOLogin = async (protocol: string) => {
|
||||
const handleSSOLogin = useCallback(async () => {
|
||||
const appCode = getAppCodeFromRedirectUrl()
|
||||
if (!appCode || !redirectUrl) {
|
||||
showErrorToast('redirect url or app code is invalid.')
|
||||
return
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
case 'saml': {
|
||||
switch (systemFeatures.webapp_auth.sso_config.protocol) {
|
||||
case SSOProtocol.SAML: {
|
||||
const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
|
||||
router.push(samlRes.url)
|
||||
break
|
||||
}
|
||||
case 'oidc': {
|
||||
case SSOProtocol.OIDC: {
|
||||
const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
|
||||
router.push(oidcRes.url)
|
||||
break
|
||||
}
|
||||
case 'oauth2': {
|
||||
case SSOProtocol.OAuth2: {
|
||||
const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
|
||||
router.push(oauth2Res.url)
|
||||
break
|
||||
}
|
||||
case '':
|
||||
break
|
||||
default:
|
||||
showErrorToast('SSO protocol is not supported.')
|
||||
}
|
||||
}
|
||||
}, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol])
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const res = await fetchSystemFeatures()
|
||||
const protocol = res.sso_enforced_for_web_protocol
|
||||
|
||||
if (message) {
|
||||
showErrorToast(message)
|
||||
return
|
||||
}
|
||||
|
||||
if (!tokenFromUrl) {
|
||||
await handleSSOLogin(protocol)
|
||||
await handleSSOLogin()
|
||||
return
|
||||
}
|
||||
|
||||
@ -89,15 +95,45 @@ const WebSSOForm: FC = () => {
|
||||
}
|
||||
|
||||
init()
|
||||
}, [message, tokenFromUrl]) // Added dependencies to useEffect
|
||||
}, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin])
|
||||
if (tokenFromUrl)
|
||||
return <div className='flex items-center justify-center h-full'><Loading /></div>
|
||||
if (message) {
|
||||
return <div className='flex items-center justify-center h-full'>
|
||||
<AppUnavailable code={'App Unavailable'} unknownReason={message} />
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}>
|
||||
<Loading type='area' />
|
||||
if (systemFeatures.webapp_auth.enabled) {
|
||||
if (systemFeatures.webapp_auth.allow_sso) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="flex items-center justify-center h-full">
|
||||
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
|
||||
<div className='flex items-center justify-center w-10 h-10 rounded-xl bg-components-card-bg shadow shadows-shadow-lg mb-2'>
|
||||
<RiDoorLockLine className='w-5 h-5' />
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-primary'>{t('login.webapp.noLoginMethod')}</p>
|
||||
<p className='system-xs-regular text-text-tertiary mt-1'>{t('login.webapp.noLoginMethodTip')}</p>
|
||||
</div>
|
||||
<div className="relative my-2 py-2">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className='bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px w-full'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return <div className="flex items-center justify-center h-full">
|
||||
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(WebSSOForm)
|
||||
|
||||
@ -5,6 +5,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import AppIcon from '../base/app-icon'
|
||||
import SwitchAppModal from '../app/switch-app-modal'
|
||||
import AccessControl from '../app/app-access-control'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
@ -18,7 +19,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppsContext, { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps'
|
||||
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
@ -50,6 +51,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
|
||||
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
|
||||
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
|
||||
const [showAccessControl, setShowAccessControl] = useState<boolean>(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
|
||||
const mutateApps = useContextSelector(
|
||||
@ -175,7 +177,20 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
})
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
|
||||
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t])
|
||||
|
||||
const handleClickAccessControl = useCallback(() => {
|
||||
if (!appDetail)
|
||||
return
|
||||
setShowAccessControl(true)
|
||||
setOpen(false)
|
||||
}, [appDetail])
|
||||
const handleAccessControlUpdate = useCallback(() => {
|
||||
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
|
||||
setAppDetail(res)
|
||||
setShowAccessControl(false)
|
||||
})
|
||||
}, [appDetail, setAppDetail])
|
||||
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
|
||||
@ -374,6 +389,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Divider />
|
||||
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={handleClickAccessControl}>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('app.accessControl')}</span>
|
||||
</div>
|
||||
<Divider className="!my-1" />
|
||||
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
|
||||
setOpen(false)
|
||||
@ -466,6 +485,11 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
showAccessControl && <AccessControl app={appDetail}
|
||||
onConfirm={handleAccessControlUpdate}
|
||||
onClose={() => { setShowAccessControl(false) }} />
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ export type IAppDetailNavProps = {
|
||||
desc: string
|
||||
isExternal?: boolean
|
||||
icon: string
|
||||
icon_background: string
|
||||
icon_background: string | null
|
||||
navigation: Array<{
|
||||
name: string
|
||||
href: string
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import { Fragment, useCallback } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type DialogProps = {
|
||||
className?: string
|
||||
children: ReactNode
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const AccessControlDialog = ({
|
||||
className,
|
||||
children,
|
||||
show,
|
||||
onClose,
|
||||
}: DialogProps) => {
|
||||
const close = useCallback(() => {
|
||||
onClose?.()
|
||||
}, [onClose])
|
||||
return (
|
||||
<Transition appear show={show} as={Fragment}>
|
||||
<Dialog as="div" open={true} className="relative z-20" onClose={() => null}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-background-overlay bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 flex items-center justify-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={cn('w-[600px] min-h-[323px] h-auto bg-components-panel-bg shadow-xl rounded-2xl transition-all transform relative p-0 overflow-y-auto', className)}>
|
||||
<div onClick={() => close()} className="absolute top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer z-10">
|
||||
<RiCloseLine className='w-5 h-5' />
|
||||
</div>
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition >
|
||||
)
|
||||
}
|
||||
|
||||
export default AccessControlDialog
|
||||
@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
|
||||
type AccessControlItemProps = PropsWithChildren<{
|
||||
type: AccessMode
|
||||
}>
|
||||
|
||||
const AccessControlItem: FC<AccessControlItemProps> = ({ type, children }) => {
|
||||
const { currentMenu, setCurrentMenu } = useAccessControlStore(s => ({ currentMenu: s.currentMenu, setCurrentMenu: s.setCurrentMenu }))
|
||||
if (currentMenu !== type) {
|
||||
return <div
|
||||
className="rounded-[10px] border-[1px] cursor-pointer
|
||||
border-components-option-card-option-border bg-components-option-card-option-bg
|
||||
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover"
|
||||
onClick={() => setCurrentMenu(type)} >
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className="rounded-[10px] border-[1.5px]
|
||||
border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm">
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
AccessControlItem.displayName = 'AccessControlItem'
|
||||
|
||||
export default AccessControlItem
|
||||
@ -0,0 +1,204 @@
|
||||
'use client'
|
||||
import { RiAddCircleFill, RiArrowRightSLine, RiOrganizationChart } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { FloatingOverlay } from '@floating-ui/react'
|
||||
import Avatar from '../../base/avatar'
|
||||
import Button from '../../base/button'
|
||||
import Checkbox from '../../base/checkbox'
|
||||
import Input from '../../base/input'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
||||
import Loading from '../../base/loading'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useSearchForWhiteListCandidates } from '@/service/access-control'
|
||||
import type { AccessControlAccount, AccessControlGroup, Subject, SubjectAccount, SubjectGroup } from '@/models/access-control'
|
||||
import { SubjectType } from '@/models/access-control'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
|
||||
export default function AddMemberOrGroupDialog() {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
|
||||
const debouncedKeyword = useDebounce(keyword, { wait: 500 })
|
||||
|
||||
const lastAvailableGroup = selectedGroupsForBreadcrumb[selectedGroupsForBreadcrumb.length - 1]
|
||||
const { isLoading, isFetchingNextPage, fetchNextPage, data } = useSearchForWhiteListCandidates({ keyword: debouncedKeyword, groupId: lastAvailableGroup?.id, resultsPerPage: 10 }, open)
|
||||
const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(e.target.value)
|
||||
}
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
const hasMore = data?.pages?.[0].hasMore ?? false
|
||||
let observer: IntersectionObserver | undefined
|
||||
if (anchorRef.current) {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && !isLoading && hasMore)
|
||||
fetchNextPage()
|
||||
}, { rootMargin: '20px' })
|
||||
observer.observe(anchorRef.current)
|
||||
}
|
||||
return () => observer?.disconnect()
|
||||
}, [isLoading, fetchNextPage, anchorRef, data])
|
||||
|
||||
return <PortalToFollowElem open={open} onOpenChange={setOpen} offset={{ crossAxis: 300 }} placement='bottom-end'>
|
||||
<PortalToFollowElemTrigger asChild>
|
||||
<Button variant='ghost-accent' size='small' className='shrink-0 flex items-center gap-x-0.5' onClick={() => setOpen(!open)}>
|
||||
<RiAddCircleFill className='w-4 h-4' />
|
||||
<span>{t('common.operation.add')}</span>
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
{open && <FloatingOverlay />}
|
||||
<PortalToFollowElemContent className='z-[25]'>
|
||||
<div className='w-[400px] max-h-[400px] relative overflow-y-auto flex flex-col border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur backdrop-blur-[5px] shadow-lg'>
|
||||
<div className='p-2 pb-0.5 sticky top-0 bg-components-panel-bg-blur backdrop-blur-[5px] z-1'>
|
||||
<Input value={keyword} onChange={handleKeywordChange} showLeftIcon placeholder={t('app.accessControlDialog.operateGroupAndMember.searchPlaceholder') as string} />
|
||||
</div>
|
||||
{
|
||||
isLoading
|
||||
? <div className='p-1'><Loading /></div>
|
||||
: (data?.pages?.length ?? 0) > 0
|
||||
? <>
|
||||
<div className='flex items-center h-7 px-2 py-0.5'>
|
||||
<SelectedGroupsBreadCrumb />
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
{renderGroupOrMember(data?.pages ?? [])}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
</div>
|
||||
<div ref={anchorRef} className='h-0'> </div>
|
||||
</>
|
||||
: <div className='flex items-center justify-center h-7 px-2 py-0.5'>
|
||||
<span className='system-xs-regular text-text-tertiary'>{t('app.accessControlDialog.operateGroupAndMember.noResult')}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
}
|
||||
|
||||
type GroupOrMemberData = { subjects: Subject[]; currPage: number }[]
|
||||
function renderGroupOrMember(data: GroupOrMemberData) {
|
||||
return data?.map((page) => {
|
||||
return <div key={`search_group_member_page_${page.currPage}`}>
|
||||
{page.subjects?.map((item, index) => {
|
||||
if (item.subjectType === SubjectType.GROUP)
|
||||
return <GroupItem key={index} group={(item as SubjectGroup).groupData} />
|
||||
return <MemberItem key={index} member={(item as SubjectAccount).accountData} />
|
||||
})}
|
||||
</div>
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
function SelectedGroupsBreadCrumb() {
|
||||
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
|
||||
const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleBreadCrumbClick = useCallback((index: number) => {
|
||||
const newGroups = selectedGroupsForBreadcrumb.slice(0, index + 1)
|
||||
setSelectedGroupsForBreadcrumb(newGroups)
|
||||
}, [setSelectedGroupsForBreadcrumb, selectedGroupsForBreadcrumb])
|
||||
const handleReset = useCallback(() => {
|
||||
setSelectedGroupsForBreadcrumb([])
|
||||
}, [setSelectedGroupsForBreadcrumb])
|
||||
return <div className='flex items-center h-7 px-2 py-0.5 gap-x-0.5'>
|
||||
<span className={classNames('system-xs-regular text-text-tertiary', selectedGroupsForBreadcrumb.length > 0 && 'text-text-accent cursor-pointer')} onClick={handleReset}>{t('app.accessControlDialog.operateGroupAndMember.allMembers')}</span>
|
||||
{selectedGroupsForBreadcrumb.map((group, index) => {
|
||||
return <div key={index} className='flex items-center gap-x-0.5 text-text-tertiary system-xs-regular'>
|
||||
<span>/</span>
|
||||
<span className={index === selectedGroupsForBreadcrumb.length - 1 ? '' : 'text-text-accent cursor-pointer'} onClick={() => handleBreadCrumbClick(index)}>{group.name}</span>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
type GroupItemProps = {
|
||||
group: AccessControlGroup
|
||||
}
|
||||
function GroupItem({ group }: GroupItemProps) {
|
||||
const { t } = useTranslation()
|
||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
|
||||
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
|
||||
const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb)
|
||||
const isChecked = specificGroups.some(g => g.id === group.id)
|
||||
const handleCheckChange = useCallback(() => {
|
||||
if (!isChecked) {
|
||||
const newGroups = [...specificGroups, group]
|
||||
setSpecificGroups(newGroups)
|
||||
}
|
||||
else {
|
||||
const newGroups = specificGroups.filter(g => g.id !== group.id)
|
||||
setSpecificGroups(newGroups)
|
||||
}
|
||||
}, [specificGroups, setSpecificGroups, group, isChecked])
|
||||
|
||||
const handleExpandClick = useCallback(() => {
|
||||
setSelectedGroupsForBreadcrumb([...selectedGroupsForBreadcrumb, group])
|
||||
}, [selectedGroupsForBreadcrumb, setSelectedGroupsForBreadcrumb, group])
|
||||
return <BaseItem>
|
||||
<Checkbox checked={isChecked} className='w-4 h-4 shrink-0' onCheck={handleCheckChange} />
|
||||
<div className='flex item-center grow'>
|
||||
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden mr-2'>
|
||||
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
|
||||
<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />
|
||||
</div>
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-secondary mr-1'>{group.name}</p>
|
||||
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
|
||||
</div>
|
||||
<Button size="small" disabled={isChecked} variant='ghost-accent'
|
||||
className='py-1 px-1.5 shrink-0 flex items-center justify-between' onClick={handleExpandClick}>
|
||||
<span className='px-[3px]'>{t('app.accessControlDialog.operateGroupAndMember.expand')}</span>
|
||||
<RiArrowRightSLine className='w-4 h-4' />
|
||||
</Button>
|
||||
</BaseItem>
|
||||
}
|
||||
|
||||
type MemberItemProps = {
|
||||
member: AccessControlAccount
|
||||
}
|
||||
function MemberItem({ member }: MemberItemProps) {
|
||||
const currentUser = useSelector(s => s.userProfile)
|
||||
const { t } = useTranslation()
|
||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
|
||||
const isChecked = specificMembers.some(m => m.id === member.id)
|
||||
const handleCheckChange = useCallback(() => {
|
||||
if (!isChecked) {
|
||||
const newMembers = [...specificMembers, member]
|
||||
setSpecificMembers(newMembers)
|
||||
}
|
||||
else {
|
||||
const newMembers = specificMembers.filter(m => m.id !== member.id)
|
||||
setSpecificMembers(newMembers)
|
||||
}
|
||||
}, [specificMembers, setSpecificMembers, member, isChecked])
|
||||
return <BaseItem className='pr-3'>
|
||||
<Checkbox checked={isChecked} className='w-4 h-4 shrink-0' onCheck={handleCheckChange} />
|
||||
<div className='flex items-center grow'>
|
||||
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden mr-2'>
|
||||
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
|
||||
<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />
|
||||
</div>
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-secondary mr-1'>{member.name}</p>
|
||||
{currentUser.email === member.email && <p className='system-xs-regular text-text-tertiary'>({t('common.you')})</p>}
|
||||
</div>
|
||||
<p className='system-xs-regular text-text-quaternary'>{member.email}</p>
|
||||
</BaseItem>
|
||||
}
|
||||
|
||||
type BaseItemProps = {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
function BaseItem({ children, className }: BaseItemProps) {
|
||||
return <div className={classNames('p-1 pl-2 flex items-center space-x-2 hover:rounded-lg hover:bg-state-base-hover cursor-pointer', className)}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
102
web/app/components/app/app-access-control/index.tsx
Normal file
102
web/app/components/app/app-access-control/index.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { RiBuildingLine, RiGlobalLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import Button from '../../base/button'
|
||||
import Toast from '../../base/toast'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import AccessControlDialog from './access-control-dialog'
|
||||
import AccessControlItem from './access-control-item'
|
||||
import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import type { App } from '@/types/app'
|
||||
import type { Subject } from '@/models/access-control'
|
||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||
import { useUpdateAccessMode } from '@/service/access-control'
|
||||
|
||||
type AccessControlProps = {
|
||||
app: App
|
||||
onClose: () => void
|
||||
onConfirm?: () => void
|
||||
}
|
||||
|
||||
export default function AccessControl(props: AccessControlProps) {
|
||||
const { app, onClose, onConfirm } = props
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const setAppId = useAccessControlStore(s => s.setAppId)
|
||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||
const currentMenu = useAccessControlStore(s => s.currentMenu)
|
||||
const setCurrentMenu = useAccessControlStore(s => s.setCurrentMenu)
|
||||
const hideTip = systemFeatures.webapp_auth.enabled
|
||||
&& (systemFeatures.webapp_auth.allow_sso
|
||||
|| systemFeatures.webapp_auth.allow_email_password_login
|
||||
|| systemFeatures.webapp_auth.allow_email_code_login)
|
||||
|
||||
useEffect(() => {
|
||||
setAppId(app.id)
|
||||
setCurrentMenu(app.access_mode ?? AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||
}, [app, setAppId, setCurrentMenu])
|
||||
|
||||
const { isPending, mutateAsync: updateAccessMode } = useUpdateAccessMode()
|
||||
const handleConfirm = useCallback(async () => {
|
||||
const submitData: {
|
||||
appId: string
|
||||
accessMode: AccessMode
|
||||
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
|
||||
} = { appId: app.id, accessMode: currentMenu }
|
||||
if (currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) {
|
||||
const subjects: Pick<Subject, 'subjectId' | 'subjectType'>[] = []
|
||||
specificGroups.forEach((group) => {
|
||||
subjects.push({ subjectId: group.id, subjectType: SubjectType.GROUP })
|
||||
})
|
||||
specificMembers.forEach((member) => {
|
||||
subjects.push({
|
||||
subjectId: member.id,
|
||||
subjectType: SubjectType.ACCOUNT,
|
||||
})
|
||||
})
|
||||
submitData.subjects = subjects
|
||||
}
|
||||
await updateAccessMode(submitData)
|
||||
Toast.notify({ type: 'success', message: t('app.accessControlDialog.updateSuccess') })
|
||||
onConfirm?.()
|
||||
}, [updateAccessMode, app, specificGroups, specificMembers, t, onConfirm, currentMenu])
|
||||
return <AccessControlDialog show onClose={onClose}>
|
||||
<div className='flex flex-col gap-y-3'>
|
||||
<div className='pt-6 pr-14 pb-3 pl-6'>
|
||||
<Dialog.Title className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</Dialog.Title>
|
||||
<Dialog.Description className='mt-1 system-xs-regular text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description>
|
||||
</div>
|
||||
<div className='px-6 pb-3 flex flex-col gap-y-1'>
|
||||
<div className='leading-6'>
|
||||
<p className='system-sm-medium'>{t('app.accessControlDialog.accessLabel')}</p>
|
||||
</div>
|
||||
<AccessControlItem type={AccessMode.ORGANIZATION}>
|
||||
<div className='flex items-center p-3'>
|
||||
<div className='grow flex items-center gap-x-2'>
|
||||
<RiBuildingLine className='w-4 h-4 text-text-primary' />
|
||||
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p>
|
||||
</div>
|
||||
{!hideTip && <WebAppSSONotEnabledTip />}
|
||||
</div>
|
||||
</AccessControlItem>
|
||||
<AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}>
|
||||
<SpecificGroupsOrMembers />
|
||||
</AccessControlItem>
|
||||
<AccessControlItem type={AccessMode.PUBLIC}>
|
||||
<div className='flex items-center p-3 gap-x-2'>
|
||||
<RiGlobalLine className='w-4 h-4 text-text-primary' />
|
||||
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.anyone')}</p>
|
||||
</div>
|
||||
</AccessControlItem>
|
||||
</div>
|
||||
<div className='flex items-center justify-end p-6 pt-5 gap-x-2'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button disabled={isPending} loading={isPending} variant='primary' onClick={handleConfirm}>{t('common.operation.confirm')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccessControlDialog>
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
'use client'
|
||||
import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import Avatar from '../../base/avatar'
|
||||
import Divider from '../../base/divider'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import Loading from '../../base/loading'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import AddMemberOrGroupDialog from './add-member-or-group-pop'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||
|
||||
export default function SpecificGroupsOrMembers() {
|
||||
const currentMenu = useAccessControlStore(s => s.currentMenu)
|
||||
const appId = useAccessControlStore(s => s.appId)
|
||||
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
|
||||
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const hideTip = systemFeatures.webapp_auth.enabled
|
||||
&& (systemFeatures.webapp_auth.allow_sso
|
||||
|| systemFeatures.webapp_auth.allow_email_password_login
|
||||
|| systemFeatures.webapp_auth.allow_email_code_login)
|
||||
|
||||
const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||
useEffect(() => {
|
||||
setSpecificGroups(data?.groups ?? [])
|
||||
setSpecificMembers(data?.members ?? [])
|
||||
}, [data, setSpecificGroups, setSpecificMembers])
|
||||
|
||||
if (currentMenu !== AccessMode.SPECIFIC_GROUPS_MEMBERS) {
|
||||
return <div className='flex items-center p-3'>
|
||||
<div className='grow flex items-center gap-x-2'>
|
||||
<RiLockLine className='w-4 h-4 text-text-primary' />
|
||||
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
|
||||
</div>
|
||||
{!hideTip && <WebAppSSONotEnabledTip />}
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className='flex items-center gap-x-1 p-3'>
|
||||
<div className='grow flex items-center gap-x-1'>
|
||||
<RiLockLine className='w-4 h-4 text-text-primary' />
|
||||
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
{!hideTip && <>
|
||||
<WebAppSSONotEnabledTip />
|
||||
<Divider className='h-[14px] ml-2 mr-0' type="vertical" />
|
||||
</>}
|
||||
<AddMemberOrGroupDialog />
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-1 pb-1'>
|
||||
<div className='bg-background-section rounded-lg p-2 flex flex-col gap-y-2 max-h-[400px] overflow-y-auto'>
|
||||
{isPending ? <Loading /> : <RenderGroupsAndMembers />}
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
|
||||
function RenderGroupsAndMembers() {
|
||||
const { t } = useTranslation()
|
||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||
if (specificGroups.length <= 0 && specificMembers.length <= 0)
|
||||
return <div className='px-2 pt-5 pb-1.5'><p className='system-xs-regular text-text-tertiary text-center'>{t('app.accessControlDialog.noGroupsOrMembers')}</p></div>
|
||||
return <>
|
||||
<p className='system-2xs-medium-uppercase text-text-tertiary sticky top-0'>{t('app.accessControlDialog.groups', { count: specificGroups.length ?? 0 })}</p>
|
||||
<div className='flex flex-row flex-wrap gap-1'>
|
||||
{specificGroups.map((group, index) => <GroupItem key={index} group={group} />)}
|
||||
</div>
|
||||
<p className='system-2xs-medium-uppercase text-text-tertiary sticky top-0'>{t('app.accessControlDialog.members', { count: specificMembers.length ?? 0 })}</p>
|
||||
<div className='flex flex-row flex-wrap gap-1'>
|
||||
{specificMembers.map((member, index) => <MemberItem key={index} member={member} />)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
type GroupItemProps = {
|
||||
group: AccessControlGroup
|
||||
}
|
||||
function GroupItem({ group }: GroupItemProps) {
|
||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
|
||||
const handleRemoveGroup = useCallback(() => {
|
||||
setSpecificGroups(specificGroups.filter(g => g.id !== group.id))
|
||||
}, [group, setSpecificGroups, specificGroups])
|
||||
return <BaseItem icon={<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />}
|
||||
onRemove={handleRemoveGroup}>
|
||||
<p className='system-xs-regular text-text-primary'>{group.name}</p>
|
||||
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
|
||||
</BaseItem>
|
||||
}
|
||||
|
||||
type MemberItemProps = {
|
||||
member: AccessControlAccount
|
||||
}
|
||||
function MemberItem({ member }: MemberItemProps) {
|
||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
|
||||
const handleRemoveMember = useCallback(() => {
|
||||
setSpecificMembers(specificMembers.filter(m => m.id !== member.id))
|
||||
}, [member, setSpecificMembers, specificMembers])
|
||||
return <BaseItem icon={<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />}
|
||||
onRemove={handleRemoveMember}>
|
||||
<p className='system-xs-regular text-text-primary'>{member.name}</p>
|
||||
</BaseItem>
|
||||
}
|
||||
|
||||
type BaseItemProps = {
|
||||
icon: React.ReactNode
|
||||
children: React.ReactNode
|
||||
onRemove?: () => void
|
||||
}
|
||||
function BaseItem({ icon, onRemove, children }: BaseItemProps) {
|
||||
return <div className='rounded-full border-[0.5px] bg-components-badge-white-to-dark shadow-xs p-1 pr-1.5 group flex items-center flex-row gap-x-1'>
|
||||
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden'>
|
||||
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
<div className='flex items-center justify-center w-4 h-4 cursor-pointer' onClick={onRemove}>
|
||||
<RiCloseCircleFill className='w-[14px] h-[14px] text-text-quaternary' />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function WebAppSSONotEnabledTip() {
|
||||
const { t } = useTranslation()
|
||||
return <Tooltip asChild={false} popupContent={t('app.accessControlDialog.webAppSSONotEnabledTip')}>
|
||||
<RiAlertFill className='w-4 h-4 text-text-warning-secondary shrink-0' />
|
||||
</Tooltip>
|
||||
}
|
||||
@ -1,13 +1,18 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiLockLine, RiPlanetLine } from '@remixicon/react'
|
||||
import Toast from '../../base/toast'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import Divider from '../../base/divider'
|
||||
import AccessControl from '../app-access-control'
|
||||
import Loading from '../../base/loading'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import Button from '@/app/components/base/button'
|
||||
@ -27,6 +32,9 @@ import { FileText } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
|
||||
export type AppPublisherProps = {
|
||||
disabled?: boolean
|
||||
@ -65,10 +73,31 @@ const AppPublisher = ({
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
|
||||
const appURL = `${appBaseURL}/${appMode}/${accessToken}`
|
||||
const { data: useCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
||||
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && appDetail)
|
||||
refetch()
|
||||
}, [open, appDetail, refetch])
|
||||
|
||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||
const [isAppAccessSet, setIsAppAccessSet] = useState(true)
|
||||
useEffect(() => {
|
||||
if (appDetail && appAccessSubjects) {
|
||||
if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0)
|
||||
setIsAppAccessSet(false)
|
||||
else
|
||||
setIsAppAccessSet(true)
|
||||
}
|
||||
else {
|
||||
setIsAppAccessSet(true)
|
||||
}
|
||||
}, [appAccessSubjects, appDetail])
|
||||
const language = useGetLanguage()
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
|
||||
@ -120,6 +149,13 @@ const AppPublisher = ({
|
||||
}
|
||||
}, [appDetail?.id])
|
||||
|
||||
const handleAccessControlUpdate = useCallback(() => {
|
||||
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
|
||||
setAppDetail(res)
|
||||
setShowAppAccessControl(false)
|
||||
})
|
||||
}, [appDetail, setAppDetail])
|
||||
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
|
||||
return (
|
||||
@ -196,58 +232,95 @@ const AppPublisher = ({
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
|
||||
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
|
||||
{appDetail?.mode === 'workflow'
|
||||
? (
|
||||
<SuggestedAction
|
||||
disabled={!publishedAt}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<LeftIndent02 className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.batchRunApp')}
|
||||
</SuggestedAction>
|
||||
)
|
||||
: (
|
||||
<SuggestedAction
|
||||
{(isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)
|
||||
? <div className='py-2'><Loading /></div>
|
||||
: <>
|
||||
<Divider className='my-0' />
|
||||
<div className='p-4 pt-3'>
|
||||
<div className='flex items-center h-6'>
|
||||
<p className='system-xs-medium text-text-tertiary'>{t('app.publishApp.title')}</p>
|
||||
</div>
|
||||
<div className='h-8 flex items-center pl-2.5 pr-2 py-1 gap-x-0.5 rounded-lg bg-components-input-bg-normal hover:bg-primary-50 hover:text-text-accent cursor-pointer'
|
||||
onClick={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<CodeBrowser className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.embedIntoSite')}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<RiPlanetLine className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.openInExplore')}
|
||||
</SuggestedAction>
|
||||
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||
{appDetail?.mode === 'workflow' && (
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={!publishedAt}
|
||||
published={!!toolPublished}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
workflowAppId={appDetail?.id}
|
||||
icon={{
|
||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
||||
}}
|
||||
name={appDetail?.name}
|
||||
description={appDetail?.description}
|
||||
inputs={inputs}
|
||||
handlePublish={handlePublish}
|
||||
onRefreshData={onRefreshData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
setShowAppAccessControl(true)
|
||||
}}>
|
||||
<div className='grow flex items-center gap-x-1.5 pr-1'>
|
||||
<RiLockLine className='w-4 h-4 text-text-secondary shrink-0' />
|
||||
{appDetail?.access_mode === AccessMode.ORGANIZATION && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>}
|
||||
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>}
|
||||
{appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>}
|
||||
</div>
|
||||
{!isAppAccessSet && <p className='shrink-0 system-xs-regular text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
|
||||
<div className='shrink-0 w-4 h-4 flex items-center justify-center'>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-text-quaternary' />
|
||||
</div>
|
||||
</div>
|
||||
{!isAppAccessSet && <p className='system-xs-regular text-text-warning mt-1'>{t('app.publishApp.notSetDesc')}</p>}
|
||||
</div>
|
||||
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5 flex flex-col gap-y-1'>
|
||||
<Tooltip triggerClassName='flex' disabled={useCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
|
||||
<SuggestedAction disabled={!publishedAt || !useCanAccessApp?.result} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
|
||||
</Tooltip>
|
||||
{appDetail?.mode === 'workflow'
|
||||
? (<Tooltip triggerClassName='flex' disabled={useCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
|
||||
<SuggestedAction
|
||||
disabled={!publishedAt || !useCanAccessApp?.result}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<LeftIndent02 className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.batchRunApp')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
)
|
||||
: (<div className='flex'>
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<CodeBrowser className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.embedIntoSite')}
|
||||
</SuggestedAction>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip triggerClassName='flex' disabled={useCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt || !useCanAccessApp?.result}
|
||||
icon={<RiPlanetLine className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.openInExplore')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
<div className='flex' >
|
||||
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||
</div>
|
||||
|
||||
{appDetail?.mode === 'workflow' && (
|
||||
<div className='flex' >
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={!publishedAt}
|
||||
published={!!toolPublished}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
workflowAppId={appDetail?.id}
|
||||
icon={{
|
||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
||||
}}
|
||||
name={appDetail?.name}
|
||||
description={appDetail?.description}
|
||||
inputs={inputs}
|
||||
handlePublish={handlePublish}
|
||||
onRefreshData={onRefreshData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
<EmbeddedModal
|
||||
@ -257,6 +330,7 @@ const AppPublisher = ({
|
||||
appBaseUrl={appBaseURL}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
{showAppAccessControl && <AccessControl app={appDetail!} onConfirm={handleAccessControlUpdate} onClose={() => { setShowAppAccessControl(false) }} />}
|
||||
</PortalToFollowElem >
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,22 +8,30 @@ export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement
|
||||
disabled?: boolean
|
||||
}>
|
||||
|
||||
const SuggestedAction = ({ icon, link, disabled, children, className, ...props }: SuggestedActionProps) => (
|
||||
<a
|
||||
href={disabled ? undefined : link}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={classNames(
|
||||
'flex justify-start items-center gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1',
|
||||
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className='relative w-4 h-4'>{icon}</div>
|
||||
<div className='grow shrink basis-0 text-[13px] font-medium leading-[18px]'>{children}</div>
|
||||
<ArrowUpRight />
|
||||
</a>
|
||||
)
|
||||
const SuggestedAction = ({ icon, link, disabled, children, className, onClick, ...props }: SuggestedActionProps) => {
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (disabled)
|
||||
return
|
||||
onClick?.(e)
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={disabled ? undefined : link}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={classNames(
|
||||
'flex-1 flex justify-start items-center text-text-secondary gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1',
|
||||
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
<div className='relative w-4 h-4'>{icon}</div>
|
||||
<div className='grow shrink basis-0 text-[13px] font-medium leading-[18px]'>{children}</div>
|
||||
<ArrowUpRight />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuggestedAction
|
||||
|
||||
@ -186,15 +186,17 @@ const Apps = ({
|
||||
<div className='w-[180px] h-8'></div>
|
||||
</div>
|
||||
<div className='relative flex flex-1 overflow-y-auto'>
|
||||
{!searchKeywords && <div className='w-[200px] h-full p-4'>
|
||||
<Sidebar current={currCategory as AppCategories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
{!searchKeywords && <div className='h-full w-[200px] p-4'>
|
||||
<Sidebar current={currCategory as AppCategories} categories={categories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
</div>}
|
||||
<div className='flex-1 h-full overflow-auto shrink-0 grow p-6 pt-2 border-l border-divider-burn'>
|
||||
{searchFilteredList && searchFilteredList.length > 0 && <>
|
||||
<div className='pt-4 pb-1'>
|
||||
{searchKeywords
|
||||
? <p className='title-md-semi-bold text-text-tertiary'>{searchFilteredList.length > 1 ? t('app.newApp.foundResults', { count: searchFilteredList.length }) : t('app.newApp.foundResult', { count: searchFilteredList.length })}</p>
|
||||
: <AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />}
|
||||
: <div className='flex h-[22px] items-center'>
|
||||
<AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -1,39 +1,29 @@
|
||||
'use client'
|
||||
import { RiAppsFill, RiChatSmileAiFill, RiExchange2Fill, RiPassPendingFill, RiQuillPenAiFill, RiSpeakAiFill, RiStickyNoteAddLine, RiTerminalBoxFill, RiThumbUpFill } from '@remixicon/react'
|
||||
import { RiStickyNoteAddLine, RiThumbUpLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
export enum AppCategories {
|
||||
RECOMMENDED = 'Recommended',
|
||||
ASSISTANT = 'Assistant',
|
||||
AGENT = 'Agent',
|
||||
HR = 'HR',
|
||||
PROGRAMMING = 'Programming',
|
||||
WORKFLOW = 'Workflow',
|
||||
WRITING = 'Writing',
|
||||
}
|
||||
|
||||
type SidebarProps = {
|
||||
current: AppCategories
|
||||
onClick?: (category: AppCategories) => void
|
||||
current: AppCategories | string
|
||||
categories: string[]
|
||||
onClick?: (category: AppCategories | string) => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
export default function Sidebar({ current, onClick, onCreateFromBlank }: SidebarProps) {
|
||||
export default function Sidebar({ current, categories, onClick, onCreateFromBlank }: SidebarProps) {
|
||||
const { t } = useTranslation()
|
||||
return <div className="w-full h-full flex flex-col">
|
||||
<ul>
|
||||
return <div className="flex h-full w-full flex-col">
|
||||
<ul className='pt-0.5'>
|
||||
<CategoryItem category={AppCategories.RECOMMENDED} active={current === AppCategories.RECOMMENDED} onClick={onClick} />
|
||||
</ul>
|
||||
<div className='px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
|
||||
<ul className='flex-grow flex flex-col gap-0.5'>
|
||||
<CategoryItem category={AppCategories.ASSISTANT} active={current === AppCategories.ASSISTANT} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.AGENT} active={current === AppCategories.AGENT} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.HR} active={current === AppCategories.HR} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.PROGRAMMING} active={current === AppCategories.PROGRAMMING} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.WORKFLOW} active={current === AppCategories.WORKFLOW} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.WRITING} active={current === AppCategories.WRITING} onClick={onClick} />
|
||||
<div className='system-xs-medium-uppercase mb-0.5 mt-3 px-3 pb-1 pt-2 text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
|
||||
<ul className='flex grow flex-col gap-0.5'>
|
||||
{categories.map(category => (<CategoryItem key={category} category={category} active={current === category} onClick={onClick} />))}
|
||||
</ul>
|
||||
<Divider bgStyle='gradient' />
|
||||
<div className='px-3 py-1 flex items-center gap-1 text-text-tertiary cursor-pointer' onClick={onCreateFromBlank}>
|
||||
@ -45,47 +35,26 @@ export default function Sidebar({ current, onClick, onCreateFromBlank }: Sidebar
|
||||
|
||||
type CategoryItemProps = {
|
||||
active: boolean
|
||||
category: AppCategories
|
||||
onClick?: (category: AppCategories) => void
|
||||
category: AppCategories | string
|
||||
onClick?: (category: AppCategories | string) => void
|
||||
}
|
||||
function CategoryItem({ category, active, onClick }: CategoryItemProps) {
|
||||
return <li
|
||||
className={classNames('p-1 pl-3 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
|
||||
className={classNames('p-1 pl-3 h-8 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
|
||||
onClick={() => { onClick?.(category) }}>
|
||||
<div className='w-5 h-5 inline-flex items-center justify-center rounded-md border border-divider-regular bg-components-icon-bg-midnight-solid group-[.active]:bg-components-icon-bg-blue-solid'>
|
||||
<AppCategoryIcon category={category} />
|
||||
</div>
|
||||
{category === AppCategories.RECOMMENDED && <div className='inline-flex h-5 w-5 items-center justify-center rounded-md'>
|
||||
<RiThumbUpLine className='h-4 w-4 text-components-menu-item-text group-[.active]:text-components-menu-item-text-active' />
|
||||
</div>}
|
||||
<AppCategoryLabel category={category}
|
||||
className={classNames('system-sm-medium text-components-menu-item-text group-[.active]:text-components-menu-item-text-active group-hover:text-components-menu-item-text-hover', active && 'system-sm-semibold')} />
|
||||
</li >
|
||||
}
|
||||
|
||||
type AppCategoryLabelProps = {
|
||||
category: AppCategories
|
||||
category: AppCategories | string
|
||||
className?: string
|
||||
}
|
||||
export function AppCategoryLabel({ category, className }: AppCategoryLabelProps) {
|
||||
const { t } = useTranslation()
|
||||
return <span className={className}>{t(`app.newAppFromTemplate.sidebar.${category}`)}</span>
|
||||
}
|
||||
|
||||
type AppCategoryIconProps = {
|
||||
category: AppCategories
|
||||
}
|
||||
function AppCategoryIcon({ category }: AppCategoryIconProps) {
|
||||
if (category === AppCategories.AGENT)
|
||||
return <RiSpeakAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.ASSISTANT)
|
||||
return <RiChatSmileAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.HR)
|
||||
return <RiPassPendingFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.PROGRAMMING)
|
||||
return <RiTerminalBoxFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.RECOMMENDED)
|
||||
return <RiThumbUpFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.WRITING)
|
||||
return <RiQuillPenAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.WORKFLOW)
|
||||
return <RiExchange2Fill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
return <RiAppsFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
return <span className={className}>{category === AppCategories.RECOMMENDED ? t('app.newAppFromTemplate.sidebar.Recommended') : category}</span>
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type DSLConfirmModalProps = {
|
||||
versions?: {
|
||||
importedVersion: string
|
||||
systemVersion: string
|
||||
}
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
confirmDisabled?: boolean
|
||||
}
|
||||
const DSLConfirmModal = ({
|
||||
versions = { importedVersion: '', systemVersion: '' },
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmDisabled = false,
|
||||
}: DSLConfirmModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => onCancel()}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onConfirm} disabled={confirmDisabled}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSLConfirmModal
|
||||
@ -24,7 +24,7 @@ const OPTION_MAP = {
|
||||
iframe: {
|
||||
getContent: (url: string, token: string) =>
|
||||
`<iframe
|
||||
src="${url}/chatbot/${token}"
|
||||
src="${url}/chat/${token}"
|
||||
style="width: 100%; height: 100%; min-height: 700px"
|
||||
frameborder="0"
|
||||
allow="microphone">
|
||||
@ -35,12 +35,12 @@ const OPTION_MAP = {
|
||||
`<script>
|
||||
window.difyChatbotConfig = {
|
||||
token: '${token}'${isTestEnv
|
||||
? `,
|
||||
? `,
|
||||
isDev: true`
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}'`
|
||||
: ''}
|
||||
: ''}
|
||||
}
|
||||
</script>
|
||||
<script
|
||||
|
||||
@ -21,14 +21,12 @@ import type { AppIconType, AppSSO, Language } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { LanguagesSupported, languages } from '@/i18n/language'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import I18n from '@/context/i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type ISettingsModalProps = {
|
||||
isChat: boolean
|
||||
@ -66,8 +64,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { notify } = useToastContext()
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const {
|
||||
@ -139,7 +135,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
}, [appInfo])
|
||||
}, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon])
|
||||
|
||||
const onHide = () => {
|
||||
onClose()
|
||||
@ -325,28 +321,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
</div>
|
||||
<p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
|
||||
</div>
|
||||
{/* SSO */}
|
||||
{systemFeatures.enable_web_sso_switch_component && (
|
||||
<>
|
||||
<Divider className="h-px my-0" />
|
||||
<div className='w-full'>
|
||||
<p className='mb-1 system-xs-medium-uppercase text-text-tertiary'>{t(`${prefixSettings}.sso.label`)}</p>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.sso.title`)}</div>
|
||||
<Tooltip
|
||||
disabled={systemFeatures.sso_enforced_for_web}
|
||||
popupContent={
|
||||
<div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>
|
||||
}
|
||||
asChild={false}
|
||||
>
|
||||
<Switch disabled={!systemFeatures.sso_enforced_for_web || !isCurrentWorkspaceEditor} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className='pb-0.5 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.sso.description`)}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* more settings switch */}
|
||||
<Divider className="h-px my-0" />
|
||||
{!isShowMore && (
|
||||
|
||||
@ -4,7 +4,7 @@ import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type IAppUnavailableProps = {
|
||||
code?: number
|
||||
code?: number | string
|
||||
isUnknownReason?: boolean
|
||||
unknownReason?: string
|
||||
}
|
||||
@ -17,7 +17,7 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center w-screen h-screen'>
|
||||
<div className='flex items-center justify-center w-full h-full'>
|
||||
<h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium'
|
||||
style={{
|
||||
borderRight: '1px solid rgba(0,0,0,.3)',
|
||||
|
||||
@ -15,12 +15,15 @@ import type {
|
||||
AppMeta,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
export type ChatWithHistoryContextValue = {
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
appData?: AppData
|
||||
accessMode?: AccessMode
|
||||
userCanAccess?: boolean
|
||||
appParams?: ChatConfig
|
||||
appChatListDataLoading?: boolean
|
||||
currentConversationId: string
|
||||
@ -52,6 +55,8 @@ export type ChatWithHistoryContextValue = {
|
||||
}
|
||||
|
||||
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
|
||||
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
userCanAccess: false,
|
||||
currentConversationId: '',
|
||||
appPrevChatTree: [],
|
||||
pinnedConversationList: [],
|
||||
@ -59,21 +64,21 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
|
||||
showConfigPanelBeforeChat: false,
|
||||
newConversationInputs: {},
|
||||
newConversationInputsRef: { current: {} },
|
||||
handleNewConversationInputsChange: () => {},
|
||||
handleNewConversationInputsChange: () => { },
|
||||
inputsForms: [],
|
||||
handleNewConversation: () => {},
|
||||
handleStartChat: () => {},
|
||||
handleChangeConversation: () => {},
|
||||
handlePinConversation: () => {},
|
||||
handleUnpinConversation: () => {},
|
||||
handleDeleteConversation: () => {},
|
||||
handleNewConversation: () => { },
|
||||
handleStartChat: () => { },
|
||||
handleChangeConversation: () => { },
|
||||
handlePinConversation: () => { },
|
||||
handleUnpinConversation: () => { },
|
||||
handleDeleteConversation: () => { },
|
||||
conversationRenaming: false,
|
||||
handleRenameConversation: () => {},
|
||||
handleNewConversationCompleted: () => {},
|
||||
handleRenameConversation: () => { },
|
||||
handleNewConversationCompleted: () => { },
|
||||
chatShouldReloadKey: '',
|
||||
isMobile: false,
|
||||
isInstalledApp: false,
|
||||
handleFeedback: () => {},
|
||||
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
||||
handleFeedback: () => { },
|
||||
currentChatInstanceRef: { current: { handleStop: () => { } } },
|
||||
})
|
||||
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
|
||||
|
||||
@ -42,6 +42,7 @@ import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
@ -72,6 +73,8 @@ function getFormattedChatList(messages: any[]) {
|
||||
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
||||
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
|
||||
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ appId: installedAppInfo?.app.id || appInfo?.app_id, isInstalledApp })
|
||||
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId: installedAppInfo?.app.id || appInfo?.app_id, isInstalledApp })
|
||||
|
||||
useAppFavicon({
|
||||
enable: !installedAppInfo,
|
||||
@ -418,7 +421,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
return {
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
appInfoLoading: appInfoLoading || isGettingAccessMode || isCheckingPermission,
|
||||
accessMode: appAccessMode?.accessMode,
|
||||
userCanAccess: userCanAccessResult?.result,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
currentConversationId,
|
||||
|
||||
@ -27,6 +27,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
className,
|
||||
}) => {
|
||||
const {
|
||||
userCanAccess,
|
||||
appInfoError,
|
||||
appData,
|
||||
appInfoLoading,
|
||||
@ -57,6 +58,8 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
<Loading type='app' />
|
||||
)
|
||||
}
|
||||
if (!userCanAccess)
|
||||
return <AppUnavailable code={403} unknownReason='no permission.' />
|
||||
|
||||
if (appInfoError) {
|
||||
return (
|
||||
@ -114,6 +117,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
||||
const {
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
accessMode,
|
||||
userCanAccess,
|
||||
appData,
|
||||
appParams,
|
||||
appMeta,
|
||||
@ -149,6 +154,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
appData,
|
||||
accessMode,
|
||||
userCanAccess,
|
||||
appParams,
|
||||
appMeta,
|
||||
appChatListDataLoading,
|
||||
|
||||
@ -11,10 +11,14 @@ import { Edit05 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
||||
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
const Sidebar = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isInstalledApp,
|
||||
accessMode,
|
||||
appData,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
@ -115,11 +119,14 @@ const Sidebar = () => {
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{appData?.site.copyright && (
|
||||
<div className='px-4 pb-4 text-xs text-gray-400'>
|
||||
© {(new Date()).getFullYear()} {appData?.site.copyright}
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center justify-between px-4 pb-4 '>
|
||||
<MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} placement='top-start' data={appData?.site} />
|
||||
{appData?.site.copyright && (
|
||||
<div className='text-xs text-gray-400 truncate'>
|
||||
© {(new Date()).getFullYear()} {appData?.site.copyright}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
|
||||
@ -14,8 +14,11 @@ import type {
|
||||
AppMeta,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
accessMode?: AccessMode
|
||||
userCanAccess?: boolean
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
@ -46,6 +49,8 @@ export type EmbeddedChatbotContextValue = {
|
||||
}
|
||||
|
||||
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
||||
userCanAccess: false,
|
||||
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
currentConversationId: '',
|
||||
appPrevChatList: [],
|
||||
pinnedConversationList: [],
|
||||
@ -53,16 +58,16 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
||||
showConfigPanelBeforeChat: false,
|
||||
newConversationInputs: {},
|
||||
newConversationInputsRef: { current: {} },
|
||||
handleNewConversationInputsChange: () => {},
|
||||
handleNewConversationInputsChange: () => { },
|
||||
inputsForms: [],
|
||||
handleNewConversation: () => {},
|
||||
handleStartChat: () => {},
|
||||
handleChangeConversation: () => {},
|
||||
handleNewConversationCompleted: () => {},
|
||||
handleNewConversation: () => { },
|
||||
handleStartChat: () => { },
|
||||
handleChangeConversation: () => { },
|
||||
handleNewConversationCompleted: () => { },
|
||||
chatShouldReloadKey: '',
|
||||
isMobile: false,
|
||||
isInstalledApp: false,
|
||||
handleFeedback: () => {},
|
||||
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
||||
handleFeedback: () => { },
|
||||
currentChatInstanceRef: { current: { handleStop: () => { } } },
|
||||
})
|
||||
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)
|
||||
|
||||
@ -35,6 +35,7 @@ import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
@ -65,6 +66,8 @@ function getFormattedChatList(messages: any[]) {
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
|
||||
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ appId: appInfo?.app_id, isInstalledApp })
|
||||
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp })
|
||||
|
||||
const appData = useMemo(() => {
|
||||
return appInfo
|
||||
@ -319,7 +322,9 @@ export const useEmbeddedChatbot = () => {
|
||||
|
||||
return {
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
appInfoLoading: appInfoLoading || isGettingAccessMode || isCheckingPermission,
|
||||
accessMode: appAccessMode?.accessMode,
|
||||
userCanAccess: userCanAccessResult?.result,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
currentConversationId,
|
||||
|
||||
@ -26,6 +26,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
const Chatbot = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
userCanAccess,
|
||||
isMobile,
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
@ -59,6 +60,9 @@ const Chatbot = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!userCanAccess)
|
||||
return <AppUnavailable code={403} unknownReason='no permission.' />
|
||||
|
||||
if (appInfoError) {
|
||||
return (
|
||||
<AppUnavailable />
|
||||
@ -91,7 +95,7 @@ const Chatbot = () => {
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<div className='p-1.5 bg-white border-[0.5px] border-gray-100 rounded-lg shadow-md cursor-pointer' onClick={handleNewConversation}>
|
||||
<RiLoopLeftLine className="h-4 w-4 text-gray-500"/>
|
||||
<RiLoopLeftLine className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -114,6 +118,8 @@ const EmbeddedChatbotWrapper = () => {
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
appData,
|
||||
accessMode,
|
||||
userCanAccess,
|
||||
appParams,
|
||||
appMeta,
|
||||
appChatListDataLoading,
|
||||
@ -139,6 +145,8 @@ const EmbeddedChatbotWrapper = () => {
|
||||
} = useEmbeddedChatbot()
|
||||
|
||||
return <EmbeddedChatbotContext.Provider value={{
|
||||
userCanAccess,
|
||||
accessMode,
|
||||
appInfoError,
|
||||
appInfoLoading,
|
||||
appData,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { UUID_NIL } from './constants'
|
||||
import type { IChatItem } from './chat/type'
|
||||
import type { ChatItem, ChatItemInTree } from './types'
|
||||
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||
import { getProcessedFilesFromResponse } from '../file-uploader/utils'
|
||||
|
||||
async function decodeBase64AndDecompress(base64String: string) {
|
||||
const binaryString = atob(base64String)
|
||||
@ -19,6 +21,60 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
||||
return inputs
|
||||
}
|
||||
|
||||
function appendQAToChatList(chatList: ChatItem[], item: any) {
|
||||
// we append answer first and then question since will reverse the whole chatList later
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
chatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
})
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
chatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the latest thread messages from all messages of the conversation.
|
||||
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||
*
|
||||
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||
* @returns An array of ChatItems representing the latest thread.
|
||||
*/
|
||||
|
||||
|
||||
function getPrevChatList(fetchedMessages: any[]) {
|
||||
const ret: ChatItem[] = []
|
||||
let nextMessageId = null
|
||||
|
||||
for (const item of fetchedMessages) {
|
||||
if (!item.parent_message_id) {
|
||||
appendQAToChatList(ret, item)
|
||||
break
|
||||
}
|
||||
|
||||
if (!nextMessageId) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
else {
|
||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret.reverse()
|
||||
}
|
||||
|
||||
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
|
||||
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
|
||||
}
|
||||
@ -164,6 +220,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
|
||||
export {
|
||||
getProcessedInputsFromUrlParams,
|
||||
isValidGeneratedAnswer,
|
||||
getPrevChatList,
|
||||
getLastAnswer,
|
||||
buildChatItemTree,
|
||||
getThreadMessages,
|
||||
|
||||
@ -90,6 +90,7 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
}}
|
||||
onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
|
||||
asChild={asChild}
|
||||
className={!asChild ? triggerClassName : ''}
|
||||
>
|
||||
{children || <div className={triggerClassName || 'p-[1px] w-3.5 h-3.5 shrink-0'}><RiQuestionLine className='text-text-quaternary hover:text-text-tertiary w-full h-full' /></div>}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
@ -68,6 +68,10 @@ export type CurrentPlanInfoBackend = {
|
||||
model_load_balancing_enabled: boolean
|
||||
dataset_operator_enabled: boolean
|
||||
webapp_copyright_enabled: boolean
|
||||
workspace_members: {
|
||||
size: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SubscriptionItem = {
|
||||
|
||||
@ -18,12 +18,12 @@ import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
|
||||
import { useDatasetDetailContext } from '@/context/dataset-detail'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
|
||||
|
||||
type IStepOneProps = {
|
||||
datasetId?: string
|
||||
dataSourceType?: DataSourceType
|
||||
dataSourceTypeDisable: Boolean
|
||||
dataSourceTypeDisable: boolean
|
||||
hasConnection: boolean
|
||||
onSetting: () => void
|
||||
files: FileItem[]
|
||||
@ -44,14 +44,20 @@ type IStepOneProps = {
|
||||
type NotionConnectorProps = {
|
||||
onSetting: () => void
|
||||
}
|
||||
export const NotionConnector = ({ onSetting }: NotionConnectorProps) => {
|
||||
export const NotionConnector = (props: NotionConnectorProps) => {
|
||||
const { onSetting } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={s.notionConnectionTip}>
|
||||
<span className={s.notionIcon} />
|
||||
<div className={s.title}>{t('datasetCreation.stepOne.notionSyncTitle')}</div>
|
||||
<div className={s.tip}>{t('datasetCreation.stepOne.notionSyncTip')}</div>
|
||||
<div className='flex w-[640px] flex-col items-start rounded-2xl bg-workflow-process-bg p-6'>
|
||||
<span className={cn(s.notionIcon, 'mb-2 h-12 w-12 rounded-[10px] border-[0.5px] border-components-card-border p-3 shadow-lg shadow-shadow-shadow-5')} />
|
||||
<div className='mb-1 flex flex-col gap-y-1 pb-3 pt-1'>
|
||||
<span className='system-md-semibold text-text-secondary'>
|
||||
{t('datasetCreation.stepOne.notionSyncTitle')}
|
||||
<Icon3Dots className='relative -left-1.5 -top-2.5 inline h-4 w-4 text-text-secondary' />
|
||||
</span>
|
||||
<div className='system-sm-regular text-text-tertiary'>{t('datasetCreation.stepOne.notionSyncTip')}</div>
|
||||
</div>
|
||||
<Button className='h-8' variant='primary' onClick={onSetting}>{t('datasetCreation.stepOne.connect')}</Button>
|
||||
</div>
|
||||
)
|
||||
@ -120,175 +126,195 @@ const StepOne = ({
|
||||
return true
|
||||
if (files.some(file => !file.file.id))
|
||||
return true
|
||||
if (isShowVectorSpaceFull)
|
||||
return true
|
||||
return false
|
||||
return isShowVectorSpaceFull
|
||||
}, [files, isShowVectorSpaceFull])
|
||||
|
||||
return (
|
||||
<div className='flex w-full h-full'>
|
||||
<div className='w-1/2 h-full overflow-y-auto relative'>
|
||||
<div className='flex justify-end'>
|
||||
<div className={classNames(s.form)}>
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className={classNames(s.stepHeader, 'z-10 text-text-secondary bg-components-panel-bg-blur')}>{t('datasetCreation.steps.one')}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className='flex items-center mb-8 flex-wrap gap-4'>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.FILE && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.FILE)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.file')}
|
||||
<div className='h-full w-full overflow-x-auto'>
|
||||
<div className='flex h-full w-full min-w-[1440px]'>
|
||||
<div className='relative h-full w-1/2 overflow-y-auto'>
|
||||
<div className='flex justify-end'>
|
||||
<div className={cn(s.form)}>
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className={cn(s.stepHeader, 'text-text-secondary system-md-semibold')}>
|
||||
{t('datasetCreation.steps.one')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.NOTION && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.NOTION)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.notion)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.notion')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.WEB && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
|
||||
)}
|
||||
onClick={() => changeType(DataSourceType.WEB)}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.web)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.web')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{dataSourceType === DataSourceType.FILE && (
|
||||
<>
|
||||
<FileUploader
|
||||
fileList={files}
|
||||
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined}
|
||||
prepareFileList={updateFileList}
|
||||
onFileListUpdate={updateFileList}
|
||||
onFileUpdate={updateFile}
|
||||
onPreview={updateCurrentFile}
|
||||
notSupportBatchUpload={notSupportBatchUpload}
|
||||
/>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 max-w-[640px]">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.NOTION && (
|
||||
<>
|
||||
{!hasConnection && <NotionConnector onSetting={onSetting} />}
|
||||
{hasConnection && (
|
||||
<>
|
||||
<div className='mb-8 w-[640px]'>
|
||||
<NotionPageSelector
|
||||
value={notionPages.map(page => page.page_id)}
|
||||
onSelect={updateNotionPages}
|
||||
onPreview={updateCurrentPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className='mb-8 grid grid-cols-3 gap-4'>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
dataSourceType === DataSourceType.FILE && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.FILE)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon)} />
|
||||
<span
|
||||
title={t('datasetCreation.stepOne.dataSourceType.file')!}
|
||||
className='truncate'
|
||||
>
|
||||
{t('datasetCreation.stepOne.dataSourceType.file')}
|
||||
</span>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 max-w-[640px]">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
dataSourceType === DataSourceType.NOTION && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.NOTION)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.notion)} />
|
||||
<span
|
||||
title={t('datasetCreation.stepOne.dataSourceType.notion')!}
|
||||
className='truncate'
|
||||
>
|
||||
{t('datasetCreation.stepOne.dataSourceType.notion')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.WEB && (
|
||||
<>
|
||||
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
|
||||
<Website
|
||||
onPreview={setCurrentWebsite}
|
||||
checkedCrawlResult={websitePages}
|
||||
onCheckedCrawlResultChange={updateWebsitePages}
|
||||
onCrawlProviderChange={onWebsiteCrawlProviderChange}
|
||||
onJobIdChange={onWebsiteCrawlJobIdChange}
|
||||
crawlOptions={crawlOptions}
|
||||
onCrawlOptionsChange={onCrawlOptionsChange}
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
dataSourceType === DataSourceType.WEB && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
|
||||
)}
|
||||
onClick={() => changeType(DataSourceType.WEB)}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.web)} />
|
||||
<span
|
||||
title={t('datasetCreation.stepOne.dataSourceType.web')!}
|
||||
className='truncate'
|
||||
>
|
||||
{t('datasetCreation.stepOne.dataSourceType.web')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{dataSourceType === DataSourceType.FILE && (
|
||||
<>
|
||||
<FileUploader
|
||||
fileList={files}
|
||||
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg' : undefined}
|
||||
prepareFileList={updateFileList}
|
||||
onFileListUpdate={updateFileList}
|
||||
onFileUpdate={updateFile}
|
||||
onPreview={updateCurrentFile}
|
||||
notSupportBatchUpload={notSupportBatchUpload}
|
||||
/>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='mb-4 max-w-[640px]'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 max-w-[640px]">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!datasetId && (
|
||||
<>
|
||||
<div className={s.dividerLine} />
|
||||
<span className="inline-flex items-center cursor-pointer text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
|
||||
<RiFolder6Line className="size-4 mr-1" />
|
||||
{t('datasetCreation.stepOne.emptyDatasetCreation')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.NOTION && (
|
||||
<>
|
||||
{!hasConnection && <NotionConnector onSetting={onSetting} />}
|
||||
{hasConnection && (
|
||||
<>
|
||||
<div className='mb-8 w-[640px]'>
|
||||
<NotionPageSelector
|
||||
value={notionPages.map(page => page.page_id)}
|
||||
onSelect={updateNotionPages}
|
||||
onPreview={updateCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='mb-4 max-w-[640px]'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.WEB && (
|
||||
<>
|
||||
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
|
||||
<Website
|
||||
onPreview={setCurrentWebsite}
|
||||
checkedCrawlResult={websitePages}
|
||||
onCheckedCrawlResultChange={updateWebsitePages}
|
||||
onCrawlProviderChange={onWebsiteCrawlProviderChange}
|
||||
onJobIdChange={onWebsiteCrawlJobIdChange}
|
||||
crawlOptions={crawlOptions}
|
||||
onCrawlOptionsChange={onCrawlOptionsChange}
|
||||
/>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='mb-4 max-w-[640px]'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!datasetId && (
|
||||
<>
|
||||
<div className='my-8 h-px max-w-[640px] bg-divider-regular' />
|
||||
<span className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
|
||||
<RiFolder6Line className="mr-1 size-4" />
|
||||
{t('datasetCreation.stepOne.emptyDatasetCreation')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
||||
</div>
|
||||
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-1/2 h-full overflow-y-auto'>
|
||||
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
|
||||
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
|
||||
<div className='h-full w-1/2 overflow-y-auto'>
|
||||
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
|
||||
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import Toast from '../../base/toast'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
@ -14,17 +12,17 @@ import type { App } from '@/models/explore'
|
||||
import Category from '@/app/components/explore/category'
|
||||
import AppCard from '@/app/components/explore/app-card'
|
||||
import { fetchAppDetail, fetchAppList } from '@/service/explore'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import AppTypeSelector from '@/app/components/app/type-selector'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import {
|
||||
DSLImportMode,
|
||||
} from '@/models/app'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
|
||||
type AppsProps = {
|
||||
pageType?: PageType
|
||||
@ -41,8 +39,6 @@ const Apps = ({
|
||||
onSuccess,
|
||||
}: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { push } = useRouter()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
|
||||
|
||||
@ -117,6 +113,14 @@ const Apps = ({
|
||||
|
||||
const [currApp, setCurrApp] = React.useState<App | null>(null)
|
||||
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
|
||||
|
||||
const {
|
||||
handleImportDSL,
|
||||
handleImportDSLConfirm,
|
||||
versions,
|
||||
isFetching,
|
||||
} = useImportDSL()
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
@ -127,31 +131,31 @@ const Apps = ({
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
try {
|
||||
const app = await importDSL({
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
})
|
||||
setIsShowCreateModal(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id }, push)
|
||||
}
|
||||
catch (e) {
|
||||
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
const payload = {
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}
|
||||
await handleImportDSL(payload, {
|
||||
onSuccess: () => {
|
||||
setIsShowCreateModal(false)
|
||||
},
|
||||
onPending: () => {
|
||||
setShowDSLConfirmModal(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onConfirmDSL = useCallback(async () => {
|
||||
await handleImportDSLConfirm({
|
||||
onSuccess,
|
||||
})
|
||||
}, [handleImportDSLConfirm, onSuccess])
|
||||
|
||||
if (!categories || categories.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
@ -234,9 +238,20 @@ const Apps = ({
|
||||
appDescription={currApp?.app.description || ''}
|
||||
show={isShowCreateModal}
|
||||
onConfirm={onCreate}
|
||||
confirmDisabled={isFetching}
|
||||
onHide={() => setIsShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
showDSLConfirmModal && (
|
||||
<DSLConfirmModal
|
||||
versions={versions}
|
||||
onCancel={() => setShowDSLConfirmModal(false)}
|
||||
onConfirm={onConfirmDSL}
|
||||
confirmDisabled={isFetching}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ export type CreateAppModalProps = {
|
||||
description: string
|
||||
use_icon_as_answer_icon?: boolean
|
||||
}) => Promise<void>
|
||||
confirmDisabled?: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
@ -48,6 +49,7 @@ const CreateAppModal = ({
|
||||
appMode,
|
||||
appUseIconAsAnswerIcon,
|
||||
onConfirm,
|
||||
confirmDisabled,
|
||||
onHide,
|
||||
}: CreateAppModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
@ -145,7 +147,7 @@ const CreateAppModal = ({
|
||||
{!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
|
||||
</div>
|
||||
<div className='flex flex-row-reverse'>
|
||||
<Button disabled={!isEditModal && isAppsFull} className='w-24 ml-2' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>
|
||||
<Button disabled={(!isEditModal && isAppsFull) || !name.trim() || confirmDisabled} className='w-24 ml-2' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>
|
||||
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@ -26,15 +26,15 @@ const InstalledApp: FC<IInstalledAppProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full py-2 pl-0 pr-2 sm:p-2'>
|
||||
<div className='h-full py-2 pl-0 pr-2 sm:p-2 bg-background-default'>
|
||||
{installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && (
|
||||
<ChatWithHistory installedAppInfo={installedApp} className='rounded-2xl shadow-md overflow-hidden' />
|
||||
)}
|
||||
{installedApp.app.mode === 'completion' && (
|
||||
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
|
||||
<TextGenerationApp isInstalledApp installedAppInfo={installedApp} />
|
||||
)}
|
||||
{installedApp.app.mode === 'workflow' && (
|
||||
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp}/>
|
||||
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -17,6 +17,7 @@ import type { InvitationResult } from '@/models/common'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
import 'react-multi-email/dist/style.css'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
type IInviteModalProps = {
|
||||
isEmailSetup: boolean
|
||||
onCancel: () => void
|
||||
@ -29,13 +30,26 @@ const InviteModal = ({
|
||||
onSend,
|
||||
}: IInviteModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const licenseLimit = useProviderContextSelector(s => s.licenseLimit)
|
||||
const [emails, setEmails] = useState<string[]>([])
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [isLimited, setIsLimited] = useState(false)
|
||||
const [isLimitExceeded, setIsLimitExceeded] = useState(false)
|
||||
const [usedSize, setUsedSize] = useState(licenseLimit.workspace_members.size ?? 0)
|
||||
useEffect(() => {
|
||||
const limited = licenseLimit.workspace_members.limit > 0
|
||||
const used = emails.length + licenseLimit.workspace_members.size
|
||||
setIsLimited(limited)
|
||||
setUsedSize(used)
|
||||
setIsLimitExceeded(limited && (used > licenseLimit.workspace_members.limit))
|
||||
}, [licenseLimit, emails])
|
||||
|
||||
const { locale } = useContext(I18n)
|
||||
const [role, setRole] = useState<string>('normal')
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (isLimitExceeded)
|
||||
return
|
||||
if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) {
|
||||
try {
|
||||
const { result, invitation_results } = await inviteMember({
|
||||
@ -53,7 +67,7 @@ const InviteModal = ({
|
||||
else {
|
||||
notify({ type: 'error', message: t('common.members.emailInvalid') })
|
||||
}
|
||||
}, [role, emails, notify, onCancel, onSend, t])
|
||||
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t])
|
||||
|
||||
return (
|
||||
<div className={cn(s.wrap)}>
|
||||
@ -81,7 +95,7 @@ const InviteModal = ({
|
||||
|
||||
<div>
|
||||
<div className='mb-2 text-sm font-medium text-gray-900'>{t('common.members.email')}</div>
|
||||
<div className='mb-8 h-36 flex items-stretch'>
|
||||
<div className='mb-8 h-36 flex flex-col items-stretch'>
|
||||
<ReactMultiEmail
|
||||
className={cn('w-full pt-2 px-3 outline-none border-none',
|
||||
'appearance-none text-sm text-gray-900 rounded-lg overflow-y-auto',
|
||||
@ -101,6 +115,14 @@ const InviteModal = ({
|
||||
}
|
||||
placeholder={t('common.members.emailPlaceholder') || ''}
|
||||
/>
|
||||
<div className={
|
||||
cn('flex items-center justify-end system-xs-regular text-text-tertiary',
|
||||
(isLimited && usedSize > licenseLimit.workspace_members.limit) ? 'text-text-destructive' : '')}
|
||||
>
|
||||
<span>{usedSize}</span>
|
||||
<span>/</span>
|
||||
<span>{isLimited ? licenseLimit.workspace_members.limit : t('common.license.unlimited')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-6'>
|
||||
<RoleSelector value={role} onChange={setRole} />
|
||||
@ -109,7 +131,7 @@ const InviteModal = ({
|
||||
tabIndex={0}
|
||||
className='w-full'
|
||||
onClick={handleSend}
|
||||
disabled={!emails.length}
|
||||
disabled={!emails.length || isLimitExceeded}
|
||||
variant='primary'
|
||||
>
|
||||
{t('common.members.sendInvite')}
|
||||
|
||||
@ -11,9 +11,11 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import TabHeader from '../../base/tab-header'
|
||||
import Button from '../../base/button'
|
||||
import { checkOrSetAccessToken } from '../utils'
|
||||
import AppUnavailable from '../../base/app-unavailable'
|
||||
import s from './style.module.css'
|
||||
import RunBatch from './run-batch'
|
||||
import ResDownload from './run-batch/res-download'
|
||||
import MenuDropdown from './menu-dropdown'
|
||||
import cn from '@/utils/classnames'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import RunOnce from '@/app/components/share/text-generation/run-once'
|
||||
@ -37,6 +39,8 @@ import Toast from '@/app/components/base/toast'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
|
||||
enum TaskStatus {
|
||||
@ -106,6 +110,9 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
|
||||
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
|
||||
|
||||
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ appId, isInstalledApp })
|
||||
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId, isInstalledApp })
|
||||
|
||||
// save message
|
||||
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
||||
const fetchSavedMessage = async () => {
|
||||
@ -537,12 +544,14 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!appId || !siteInfo || !promptConfig) {
|
||||
if (!appId || !siteInfo || !promptConfig || isGettingAccessMode || isCheckingPermission) {
|
||||
return (
|
||||
<div className='flex items-center h-screen'>
|
||||
<Loading type='app' />
|
||||
</div>)
|
||||
}
|
||||
if (!userCanAccessResult?.result)
|
||||
return <AppUnavailable code={403} unknownReason='no permission.' />
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -558,16 +567,19 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
|
||||
)}>
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<AppIcon
|
||||
size="small"
|
||||
iconType={siteInfo.icon_type}
|
||||
icon={siteInfo.icon}
|
||||
background={siteInfo.icon_background || appDefaultIconBackground}
|
||||
imageUrl={siteInfo.icon_url}
|
||||
/>
|
||||
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex grow'>
|
||||
<div className='flex items-center space-x-3 grow'>
|
||||
<AppIcon
|
||||
size="small"
|
||||
iconType={siteInfo.icon_type}
|
||||
icon={siteInfo.icon}
|
||||
background={siteInfo.icon_background || appDefaultIconBackground}
|
||||
imageUrl={siteInfo.icon_url}
|
||||
/>
|
||||
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
|
||||
</div>
|
||||
<MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} />
|
||||
</div>
|
||||
{!isPC && (
|
||||
<Button
|
||||
|
||||
49
web/app/components/share/text-generation/info-modal.tsx
Normal file
49
web/app/components/share/text-generation/info-modal.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
|
||||
type Props = {
|
||||
data?: SiteInfo
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const InfoModal = ({
|
||||
isShow,
|
||||
onClose,
|
||||
data,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='min-w-[400px] max-w-[400px] !p-0'
|
||||
closable
|
||||
>
|
||||
<div className={cn('flex flex-col items-center gap-4 px-4 pb-8 pt-10')}>
|
||||
<AppIcon
|
||||
size='xxl'
|
||||
iconType={data?.icon_type}
|
||||
icon={data?.icon}
|
||||
background={data?.icon_background || appDefaultIconBackground}
|
||||
imageUrl={data?.icon_url}
|
||||
/>
|
||||
<div className='system-xl-semibold text-text-secondary'>{data?.title}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{/* copyright */}
|
||||
{data?.copyright && (
|
||||
<div>© {(new Date()).getFullYear()} {data?.copyright}</div>
|
||||
)}
|
||||
{data?.custom_disclaimer && (
|
||||
<div className='mt-2'>{data.custom_disclaimer}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoModal
|
||||
113
web/app/components/share/text-generation/menu-dropdown.tsx
Normal file
113
web/app/components/share/text-generation/menu-dropdown.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Divider from '../../base/divider'
|
||||
import { removeAccessToken } from '../utils'
|
||||
import InfoModal from './info-modal'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
data?: SiteInfo
|
||||
placement?: Placement
|
||||
hideLogout?: boolean
|
||||
}
|
||||
|
||||
const MenuDropdown: FC<Props> = ({
|
||||
data,
|
||||
placement,
|
||||
hideLogout,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const [open, doSetOpen] = useState(false)
|
||||
const openRef = useRef(open)
|
||||
const setOpen = useCallback((v: boolean) => {
|
||||
doSetOpen(v)
|
||||
openRef.current = v
|
||||
}, [doSetOpen])
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
setOpen(!openRef.current)
|
||||
}, [setOpen])
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
removeAccessToken()
|
||||
router.replace(`/webapp-signin?redirect_url=${window.location.href}`)
|
||||
}, [router])
|
||||
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement={placement || 'bottom-end'}
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<div>
|
||||
<ActionButton size='l' className={cn(open && 'bg-state-base-hover')}>
|
||||
<RiEqualizer2Line className='h-[18px] w-[18px]' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-50'>
|
||||
<div className='w-[224px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
|
||||
<div className='p-1'>
|
||||
{data?.privacy_policy && (
|
||||
<a href={data.privacy_policy} target='_blank' className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
|
||||
<span className='grow'>{t('share.chat.privacyPolicyMiddle')}</span>
|
||||
</a>
|
||||
)}
|
||||
<div
|
||||
onClick={() => {
|
||||
handleTrigger()
|
||||
setShow(true)
|
||||
}}
|
||||
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
|
||||
>{t('common.userProfile.about')}</div>
|
||||
{false && (
|
||||
<>
|
||||
<Divider />
|
||||
<div
|
||||
onClick={() => {
|
||||
handleLogout()
|
||||
}}
|
||||
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-destructive hover:bg-state-base-hover'
|
||||
>{t('common.userProfile.logout')}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
{show && (
|
||||
<InfoModal
|
||||
isShow={show}
|
||||
onClose={() => {
|
||||
setShow(false)
|
||||
}}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(MenuDropdown)
|
||||
34
web/context/access-control-store.ts
Normal file
34
web/context/access-control-store.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { create } from 'zustand'
|
||||
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import type { App } from '@/types/app'
|
||||
|
||||
type AccessControlStore = {
|
||||
appId: App['id']
|
||||
setAppId: (appId: App['id']) => void
|
||||
specificGroups: AccessControlGroup[]
|
||||
setSpecificGroups: (specificGroups: AccessControlGroup[]) => void
|
||||
specificMembers: AccessControlAccount[]
|
||||
setSpecificMembers: (specificMembers: AccessControlAccount[]) => void
|
||||
currentMenu: AccessMode
|
||||
setCurrentMenu: (currentMenu: AccessMode) => void
|
||||
selectedGroupsForBreadcrumb: AccessControlGroup[]
|
||||
setSelectedGroupsForBreadcrumb: (selectedGroupsForBreadcrumb: AccessControlGroup[]) => void
|
||||
}
|
||||
|
||||
const useAccessControlStore = create<AccessControlStore>((set) => {
|
||||
return {
|
||||
appId: '',
|
||||
setAppId: appId => set({ appId }),
|
||||
specificGroups: [],
|
||||
setSpecificGroups: specificGroups => set({ specificGroups }),
|
||||
specificMembers: [],
|
||||
setSpecificMembers: specificMembers => set({ specificMembers }),
|
||||
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
setCurrentMenu: currentMenu => set({ currentMenu }),
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
setSelectedGroupsForBreadcrumb: selectedGroupsForBreadcrumb => set({ selectedGroupsForBreadcrumb }),
|
||||
}
|
||||
})
|
||||
|
||||
export default useAccessControlStore
|
||||
@ -36,6 +36,12 @@ type ProviderContextState = {
|
||||
modelLoadBalancingEnabled: boolean
|
||||
datasetOperatorEnabled: boolean
|
||||
webappCopyrightEnabled: boolean
|
||||
licenseLimit: {
|
||||
workspace_members: {
|
||||
size: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
}
|
||||
const ProviderContext = createContext<ProviderContextState>({
|
||||
modelProviders: [],
|
||||
@ -66,6 +72,12 @@ const ProviderContext = createContext<ProviderContextState>({
|
||||
modelLoadBalancingEnabled: false,
|
||||
datasetOperatorEnabled: false,
|
||||
webappCopyrightEnabled: false,
|
||||
licenseLimit: {
|
||||
workspace_members: {
|
||||
size: 0,
|
||||
limit: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const useProviderContext = () => useContext(ProviderContext)
|
||||
@ -94,6 +106,12 @@ export const ProviderContextProvider = ({
|
||||
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
|
||||
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
|
||||
const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false)
|
||||
const [licenseLimit, setLicenseLimit] = useState({
|
||||
workspace_members: {
|
||||
size: 0,
|
||||
limit: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const fetchPlan = async () => {
|
||||
const data = await fetchCurrentPlanInfo()
|
||||
@ -110,6 +128,8 @@ export const ProviderContextProvider = ({
|
||||
setDatasetOperatorEnabled(true)
|
||||
if (data.webapp_copyright_enabled)
|
||||
setWebappCopyrightEnabled(true)
|
||||
if (data.workspace_members)
|
||||
setLicenseLimit({ workspace_members: data.workspace_members })
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchPlan()
|
||||
@ -129,6 +149,7 @@ export const ProviderContextProvider = ({
|
||||
modelLoadBalancingEnabled,
|
||||
datasetOperatorEnabled,
|
||||
webappCopyrightEnabled,
|
||||
licenseLimit,
|
||||
}}>
|
||||
{children}
|
||||
</ProviderContext.Provider>
|
||||
|
||||
@ -23,6 +23,7 @@ export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
|
||||
|
||||
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
|
||||
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
|
||||
export NEXT_PUBLIC_ALLOW_EMBED=${ALLOW_EMBED}
|
||||
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
|
||||
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
|
||||
|
||||
|
||||
@ -9,7 +9,8 @@ export default function useDocumentTitle(title: string) {
|
||||
if (systemFeatures.branding.enabled) {
|
||||
document.title = `${prefix}${systemFeatures.branding.application_title}`
|
||||
const faviconEle = document.querySelector('link[rel*=\'icon\']') as HTMLLinkElement
|
||||
faviconEle.href = systemFeatures.branding.favicon
|
||||
if (faviconEle)
|
||||
faviconEle.href = systemFeatures.branding.favicon
|
||||
}
|
||||
else {
|
||||
document.title = `${prefix}Dify`
|
||||
|
||||
158
web/hooks/use-import-dsl.ts
Normal file
158
web/hooks/use-import-dsl.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type {
|
||||
DSLImportMode,
|
||||
DSLImportResponse,
|
||||
} from '@/models/app'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import {
|
||||
importDSL,
|
||||
importDSLConfirm,
|
||||
} from '@/service/apps'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
|
||||
type DSLPayload = {
|
||||
mode: DSLImportMode
|
||||
yaml_content?: string
|
||||
yaml_url?: string
|
||||
name?: string
|
||||
icon_type?: AppIconType
|
||||
icon?: string
|
||||
icon_background?: string
|
||||
description?: string
|
||||
}
|
||||
type ResponseCallback = {
|
||||
onSuccess?: () => void
|
||||
onPending?: (payload: DSLImportResponse) => void
|
||||
onFailed?: () => void
|
||||
}
|
||||
export const useImportDSL = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const isCurrentWorkspaceEditor = useSelector(s => s.isCurrentWorkspaceEditor)
|
||||
const { push } = useRouter()
|
||||
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||
const importIdRef = useRef<string>('')
|
||||
|
||||
const handleImportDSL = useCallback(async (
|
||||
payload: DSLPayload,
|
||||
{
|
||||
onSuccess,
|
||||
onPending,
|
||||
onFailed,
|
||||
}: ResponseCallback,
|
||||
) => {
|
||||
if (isFetching)
|
||||
return
|
||||
setIsFetching(true)
|
||||
|
||||
try {
|
||||
const response = await importDSL(payload)
|
||||
|
||||
if (!response)
|
||||
return
|
||||
|
||||
const {
|
||||
id,
|
||||
status,
|
||||
app_id,
|
||||
imported_dsl_version,
|
||||
current_dsl_version,
|
||||
} = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
if (!app_id)
|
||||
return
|
||||
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
|
||||
})
|
||||
onSuccess?.()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
|
||||
}
|
||||
else if (status === DSLImportStatus.PENDING) {
|
||||
setVersions({
|
||||
importedVersion: imported_dsl_version ?? '',
|
||||
systemVersion: current_dsl_version ?? '',
|
||||
})
|
||||
importIdRef.current = id
|
||||
onPending?.(response)
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
onFailed?.()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
onFailed?.()
|
||||
}
|
||||
finally {
|
||||
setIsFetching(false)
|
||||
}
|
||||
}, [t, notify, isCurrentWorkspaceEditor, push, isFetching])
|
||||
|
||||
const handleImportDSLConfirm = useCallback(async (
|
||||
{
|
||||
onSuccess,
|
||||
onFailed,
|
||||
}: Pick<ResponseCallback, 'onSuccess' | 'onFailed'>,
|
||||
) => {
|
||||
if (isFetching)
|
||||
return
|
||||
setIsFetching(true)
|
||||
if (!importIdRef.current)
|
||||
return
|
||||
|
||||
try {
|
||||
const response = await importDSLConfirm({
|
||||
import_id: importIdRef.current,
|
||||
})
|
||||
|
||||
const { status, app_id } = response
|
||||
if (!app_id)
|
||||
return
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED) {
|
||||
onSuccess?.()
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id! }, push)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
onFailed?.()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
onFailed?.()
|
||||
}
|
||||
finally {
|
||||
setIsFetching(false)
|
||||
}
|
||||
}, [t, notify, isCurrentWorkspaceEditor, push, isFetching])
|
||||
|
||||
return {
|
||||
handleImportDSL,
|
||||
handleImportDSLConfirm,
|
||||
versions,
|
||||
isFetching,
|
||||
}
|
||||
}
|
||||
@ -30,26 +30,26 @@ const translation = {
|
||||
overview: {
|
||||
title: 'Übersicht',
|
||||
appInfo: {
|
||||
explanation: 'Einsatzbereite AI-WebApp',
|
||||
explanation: 'Einsatzbereite AI-web app',
|
||||
accessibleAddress: 'Öffentliche URL',
|
||||
preview: 'Vorschau',
|
||||
regenerate: 'Regenerieren',
|
||||
regenerateNotice: 'Möchten Sie die öffentliche URL neu generieren?',
|
||||
preUseReminder: 'Bitte aktivieren Sie WebApp, bevor Sie fortfahren.',
|
||||
preUseReminder: 'Bitte aktivieren Sie web app, bevor Sie fortfahren.',
|
||||
settings: {
|
||||
entry: 'Einstellungen',
|
||||
title: 'WebApp-Einstellungen',
|
||||
webName: 'WebApp-Name',
|
||||
webDesc: 'WebApp-Beschreibung',
|
||||
title: 'web app Einstellungen',
|
||||
webName: 'web app Name',
|
||||
webDesc: 'web app Beschreibung',
|
||||
webDescTip: 'Dieser Text wird auf der Clientseite angezeigt und bietet grundlegende Anleitungen zur Verwendung der Anwendung',
|
||||
webDescPlaceholder: 'Geben Sie die Beschreibung der WebApp ein',
|
||||
webDescPlaceholder: 'Geben Sie die Beschreibung der web app ein',
|
||||
language: 'Sprache',
|
||||
workflow: {
|
||||
title: 'Workflow-Schritte',
|
||||
show: 'Anzeigen',
|
||||
hide: 'Verbergen',
|
||||
subTitle: 'Details zum Arbeitsablauf',
|
||||
showDesc: 'Ein- oder Ausblenden von Workflow-Details in der WebApp',
|
||||
showDesc: 'Ein- oder Ausblenden von Workflow-Details in der web app',
|
||||
},
|
||||
chatColorTheme: 'Chat-Farbschema',
|
||||
chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest',
|
||||
@ -69,10 +69,10 @@ const translation = {
|
||||
copyrightTooltip: 'Bitte führen Sie ein Upgrade auf den Professional-Plan oder höher durch',
|
||||
},
|
||||
sso: {
|
||||
title: 'WebApp-SSO',
|
||||
description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie WebApp verwenden können',
|
||||
title: 'web app SSO',
|
||||
description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie web app verwenden können',
|
||||
label: 'SSO-Authentifizierung',
|
||||
tooltip: 'Wenden Sie sich an den Administrator, um WebApp-SSO zu aktivieren',
|
||||
tooltip: 'Wenden Sie sich an den Administrator, um web app SSO zu aktivieren',
|
||||
},
|
||||
modalTip: 'Einstellungen für clientseitige Web-Apps.',
|
||||
},
|
||||
@ -94,7 +94,7 @@ const translation = {
|
||||
customize: {
|
||||
way: 'Art',
|
||||
entry: 'Anpassen',
|
||||
title: 'AI-WebApp anpassen',
|
||||
title: 'AI-web app anpassen',
|
||||
explanation: 'Sie können das Frontend der Web-App an Ihre Szenarien und Stilbedürfnisse anpassen.',
|
||||
way1: {
|
||||
name: 'Forken Sie den Client-Code, ändern Sie ihn und deployen Sie ihn auf Vercel (empfohlen)',
|
||||
|
||||
@ -163,9 +163,9 @@ const translation = {
|
||||
},
|
||||
},
|
||||
answerIcon: {
|
||||
descriptionInExplore: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in Explore verwendet werden soll',
|
||||
title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖',
|
||||
description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll',
|
||||
descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll',
|
||||
title: 'Verwenden Sie das web app Symbol, um es zu ersetzen 🤖',
|
||||
description: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll',
|
||||
},
|
||||
importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen',
|
||||
duplicate: 'Duplikat',
|
||||
|
||||
@ -5,7 +5,7 @@ const translation = {
|
||||
suffix: 'um deine Marke anzupassen.',
|
||||
},
|
||||
webapp: {
|
||||
title: 'WebApp Marke anpassen',
|
||||
title: 'web app Marke anpassen',
|
||||
removeBrand: 'Entferne Powered by Dify',
|
||||
changeLogo: 'Ändere Powered by Markenbild',
|
||||
changeLogoTip: 'SVG oder PNG Format mit einer Mindestgröße von 40x40px',
|
||||
|
||||
@ -30,27 +30,27 @@ const translation = {
|
||||
overview: {
|
||||
title: 'Overview',
|
||||
appInfo: {
|
||||
explanation: 'Ready-to-use AI WebApp',
|
||||
explanation: 'Ready-to-use AI web app',
|
||||
accessibleAddress: 'Public URL',
|
||||
preview: 'Preview',
|
||||
regenerate: 'Regenerate',
|
||||
regenerateNotice: 'Do you want to regenerate the public URL?',
|
||||
preUseReminder: 'Please enable WebApp before continuing.',
|
||||
preUseReminder: 'Please enable web app before continuing.',
|
||||
settings: {
|
||||
entry: 'Settings',
|
||||
title: 'Web App Settings',
|
||||
modalTip: 'Client-side web app settings. ',
|
||||
webName: 'WebApp Name',
|
||||
webDesc: 'WebApp Description',
|
||||
webName: 'web app Name',
|
||||
webDesc: 'web app Description',
|
||||
webDescTip: 'This text will be displayed on the client side, providing basic guidance on how to use the application',
|
||||
webDescPlaceholder: 'Enter the description of the WebApp',
|
||||
webDescPlaceholder: 'Enter the description of the web app',
|
||||
language: 'Language',
|
||||
workflow: {
|
||||
title: 'Workflow',
|
||||
subTitle: 'Workflow Details',
|
||||
show: 'Show',
|
||||
hide: 'Hide',
|
||||
showDesc: 'Show or hide workflow details in WebApp',
|
||||
showDesc: 'Show or hide workflow details in web app',
|
||||
},
|
||||
chatColorTheme: 'Chat color theme',
|
||||
chatColorThemeDesc: 'Set the color theme of the chatbot',
|
||||
@ -58,14 +58,14 @@ const translation = {
|
||||
invalidHexMessage: 'Invalid hex value',
|
||||
sso: {
|
||||
label: 'SSO Enforcement',
|
||||
title: 'WebApp SSO',
|
||||
description: 'All users are required to login with SSO before using WebApp',
|
||||
tooltip: 'Contact the administrator to enable WebApp SSO',
|
||||
title: 'web app SSO',
|
||||
description: 'All users are required to login with SSO before using web app',
|
||||
tooltip: 'Contact the administrator to enable web app SSO',
|
||||
},
|
||||
more: {
|
||||
entry: 'Show more settings',
|
||||
copyright: 'Copyright',
|
||||
copyrightTip: 'Display copyright information in the webapp',
|
||||
copyrightTip: 'Display copyright information in the web app',
|
||||
copyrightTooltip: 'Please upgrade to Professional plan or above',
|
||||
copyRightPlaceholder: 'Enter the name of the author or organization',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
@ -94,7 +94,7 @@ const translation = {
|
||||
customize: {
|
||||
way: 'way',
|
||||
entry: 'Customize',
|
||||
title: 'Customize AI WebApp',
|
||||
title: 'Customize AI web app',
|
||||
explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.',
|
||||
way1: {
|
||||
name: 'Fork the client code, modify it and deploy to Vercel (recommended)',
|
||||
|
||||
@ -112,9 +112,9 @@ const translation = {
|
||||
image: 'Image',
|
||||
},
|
||||
answerIcon: {
|
||||
title: 'Use WebApp icon to replace 🤖',
|
||||
description: 'Whether to use the WebApp icon to replace 🤖 in the shared application',
|
||||
descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore',
|
||||
title: 'Use web app icon to replace 🤖',
|
||||
description: 'Whether to use the web app icon to replace 🤖 in the shared application',
|
||||
descriptionInExplore: 'Whether to use the web app icon to replace 🤖 in Explore',
|
||||
},
|
||||
switch: 'Switch to Workflow Orchestrate',
|
||||
switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ',
|
||||
@ -174,6 +174,41 @@ const translation = {
|
||||
},
|
||||
},
|
||||
showMyCreatedAppsOnly: 'Created by me',
|
||||
accessControl: 'Web App Access Control',
|
||||
accessItemsDescription: {
|
||||
anyone: 'Anyone can access the web app',
|
||||
specific: 'Only specific groups or members can access the web app',
|
||||
organization: 'Anyone in the organization can access the web app',
|
||||
},
|
||||
accessControlDialog: {
|
||||
title: 'Web App Access Control',
|
||||
description: 'Set web app access permissions',
|
||||
accessLabel: 'Who has access',
|
||||
accessItems: {
|
||||
anyone: 'Anyone with the link',
|
||||
specific: 'Specific groups or members',
|
||||
organization: 'Only members within the enterprise',
|
||||
},
|
||||
groups_one: '{{count}} GROUP',
|
||||
groups_other: '{{count}} GROUPS',
|
||||
members_one: '{{count}} MEMBER',
|
||||
members_other: '{{count}} MEMBERS',
|
||||
noGroupsOrMembers: 'No groups or members selected',
|
||||
webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.',
|
||||
operateGroupAndMember: {
|
||||
searchPlaceholder: 'Search groups and members',
|
||||
allMembers: 'All members',
|
||||
expand: 'Expand',
|
||||
noResult: 'No result',
|
||||
},
|
||||
updateSuccess: 'Update successfully',
|
||||
},
|
||||
publishApp: {
|
||||
title: 'Who can access web app',
|
||||
notSet: 'Not set',
|
||||
notSetDesc: 'Currently nobody can access the web app. Please set permissions.',
|
||||
},
|
||||
noAccessPermission: 'No permission to access web app',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@ -617,10 +617,12 @@ const translation = {
|
||||
license: {
|
||||
expiring: 'Expiring in one day',
|
||||
expiring_plural: 'Expiring in {{count}} days',
|
||||
unlimited: 'Unlimited',
|
||||
},
|
||||
pagination: {
|
||||
perPage: 'Items per page',
|
||||
},
|
||||
you: 'You',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@ -5,7 +5,7 @@ const translation = {
|
||||
suffix: 'customize your brand.',
|
||||
},
|
||||
webapp: {
|
||||
title: 'Customize WebApp brand',
|
||||
title: 'Customize web app brand',
|
||||
removeBrand: 'Remove Powered by Dify',
|
||||
changeLogo: 'Change Powered by Brand Image',
|
||||
changeLogoTip: 'SVG or PNG format with a minimum size of 40x40px',
|
||||
|
||||
@ -104,6 +104,11 @@ const translation = {
|
||||
licenseLostTip: 'Failed to connect Dify license server. Please contact your administrator to continue using Dify.',
|
||||
licenseInactive: 'License Inactive',
|
||||
licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.',
|
||||
webapp: {
|
||||
noLoginMethod: 'Authentication method not configured for web app',
|
||||
noLoginMethodTip: 'Please contact the system admin to add an authentication method.',
|
||||
disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@ -49,7 +49,7 @@ const translation = {
|
||||
show: 'Mostrar',
|
||||
hide: 'Ocultar',
|
||||
subTitle: 'Detalles del flujo de trabajo',
|
||||
showDesc: 'Mostrar u ocultar detalles del flujo de trabajo en WebApp',
|
||||
showDesc: 'Mostrar u ocultar detalles del flujo de trabajo en web app',
|
||||
},
|
||||
chatColorTheme: 'Tema de color del chat',
|
||||
chatColorThemeDesc: 'Establece el tema de color del chatbot',
|
||||
@ -69,10 +69,10 @@ const translation = {
|
||||
copyrightTooltip: 'Actualice al plan Profesional o superior',
|
||||
},
|
||||
sso: {
|
||||
description: 'Todos los usuarios deben iniciar sesión con SSO antes de usar WebApp',
|
||||
tooltip: 'Póngase en contacto con el administrador para habilitar el inicio de sesión único de WebApp',
|
||||
description: 'Todos los usuarios deben iniciar sesión con SSO antes de usar web app',
|
||||
tooltip: 'Póngase en contacto con el administrador para habilitar el inicio de sesión único de web app',
|
||||
label: 'Autenticación SSO',
|
||||
title: 'WebApp SSO',
|
||||
title: 'web app SSO',
|
||||
},
|
||||
modalTip: 'Configuración de la aplicación web del lado del cliente.',
|
||||
},
|
||||
|
||||
@ -5,7 +5,7 @@ const translation = {
|
||||
suffix: 'personalizar tu marca.',
|
||||
},
|
||||
webapp: {
|
||||
title: 'Personalizar marca de WebApp',
|
||||
title: 'Personalizar marca de web app',
|
||||
removeBrand: 'Eliminar Powered by Dify',
|
||||
changeLogo: 'Cambiar Imagen de Marca Powered by',
|
||||
changeLogoTip: 'Formato SVG o PNG con un tamaño mínimo de 40x40px',
|
||||
|
||||
@ -35,20 +35,20 @@ const translation = {
|
||||
preview: 'پیشنمایش',
|
||||
regenerate: 'تولید مجدد',
|
||||
regenerateNotice: 'آیا میخواهید آدرس عمومی را دوباره تولید کنید؟',
|
||||
preUseReminder: 'لطفاً قبل از ادامه، WebApp را فعال کنید.',
|
||||
preUseReminder: 'لطفاً قبل از ادامه، web app را فعال کنید.',
|
||||
settings: {
|
||||
entry: 'تنظیمات',
|
||||
title: 'تنظیمات WebApp',
|
||||
webName: 'نام WebApp',
|
||||
webDesc: 'توضیحات WebApp',
|
||||
title: 'تنظیمات web app',
|
||||
webName: 'نام web app',
|
||||
webDesc: 'توضیحات web app',
|
||||
webDescTip: 'این متن در سمت مشتری نمایش داده میشود و راهنماییهای اولیه در مورد نحوه استفاده از برنامه را ارائه میدهد',
|
||||
webDescPlaceholder: 'توضیحات WebApp را وارد کنید',
|
||||
webDescPlaceholder: 'توضیحات web app را وارد کنید',
|
||||
language: 'زبان',
|
||||
workflow: {
|
||||
title: 'مراحل کاری',
|
||||
show: 'نمایش',
|
||||
hide: 'مخفی کردن',
|
||||
showDesc: 'نمایش یا پنهان کردن جزئیات گردش کار در WebApp',
|
||||
showDesc: 'نمایش یا پنهان کردن جزئیات گردش کار در web app',
|
||||
subTitle: 'جزئیات گردش کار',
|
||||
},
|
||||
chatColorTheme: 'تم رنگی چت',
|
||||
@ -69,10 +69,10 @@ const translation = {
|
||||
copyrightTooltip: 'لطفا به طرح حرفه ای یا بالاتر ارتقا دهید',
|
||||
},
|
||||
sso: {
|
||||
title: 'WebApp SSO',
|
||||
title: 'web app SSO',
|
||||
label: 'احراز هویت SSO',
|
||||
description: 'همه کاربران باید قبل از استفاده از WebApp با SSO وارد شوند',
|
||||
tooltip: 'برای فعال کردن WebApp SSO با سرپرست تماس بگیرید',
|
||||
description: 'همه کاربران باید قبل از استفاده از web app با SSO وارد شوند',
|
||||
tooltip: 'برای فعال کردن web app SSO با سرپرست تماس بگیرید',
|
||||
},
|
||||
modalTip: 'تنظیمات برنامه وب سمت مشتری.',
|
||||
},
|
||||
@ -94,7 +94,7 @@ const translation = {
|
||||
customize: {
|
||||
way: 'راه',
|
||||
entry: 'سفارشیسازی',
|
||||
title: 'سفارشیسازی WebApp AI',
|
||||
title: 'سفارشیسازی web app AI',
|
||||
explanation: 'شما میتوانید ظاهر جلویی برنامه وب را برای برآوردن نیازهای سناریو و سبک خود سفارشی کنید.',
|
||||
way1: {
|
||||
name: 'کلاینت را شاخه کنید، آن را تغییر دهید و در Vercel مستقر کنید (توصیه میشود)',
|
||||
|
||||
@ -165,9 +165,9 @@ const translation = {
|
||||
},
|
||||
},
|
||||
answerIcon: {
|
||||
descriptionInExplore: 'آیا از نماد WebApp برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر',
|
||||
description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر',
|
||||
title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید',
|
||||
descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر',
|
||||
description: 'آیا از نماد web app برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر',
|
||||
title: 'از نماد web app برای جایگزینی 🤖 استفاده کنید',
|
||||
},
|
||||
mermaid: {
|
||||
handDrawn: 'دست کشیده شده',
|
||||
|
||||
@ -30,12 +30,12 @@ const translation = {
|
||||
overview: {
|
||||
title: 'Aperçu',
|
||||
appInfo: {
|
||||
explanation: 'WebApp AI prête à l\'emploi',
|
||||
explanation: 'web app AI prête à l\'emploi',
|
||||
accessibleAddress: 'URL publique',
|
||||
preview: 'Aperçu',
|
||||
regenerate: 'Regénérer',
|
||||
regenerateNotice: 'Voulez-vous régénérer l\'URL publique ?',
|
||||
preUseReminder: 'Veuillez activer WebApp avant de continuer.',
|
||||
preUseReminder: 'Veuillez activer web app avant de continuer.',
|
||||
settings: {
|
||||
entry: 'Paramètres',
|
||||
title: 'Paramètres de l\'application Web',
|
||||
@ -48,7 +48,7 @@ const translation = {
|
||||
title: 'Étapes du workflow',
|
||||
show: 'Afficher',
|
||||
hide: 'Masquer',
|
||||
showDesc: 'Afficher ou masquer les détails du flux de travail dans WebApp',
|
||||
showDesc: 'Afficher ou masquer les détails du flux de travail dans web app',
|
||||
subTitle: 'Détails du flux de travail',
|
||||
},
|
||||
chatColorTheme: 'Thème de couleur du chatbot',
|
||||
@ -70,9 +70,9 @@ const translation = {
|
||||
},
|
||||
sso: {
|
||||
label: 'Authentification SSO',
|
||||
title: 'WebApp SSO',
|
||||
tooltip: 'Contactez l’administrateur pour activer l’authentification unique WebApp',
|
||||
description: 'Tous les utilisateurs doivent se connecter avec l’authentification unique avant d’utiliser WebApp',
|
||||
title: 'web app SSO',
|
||||
tooltip: 'Contactez l’administrateur pour activer l’authentification unique web app',
|
||||
description: 'Tous les utilisateurs doivent se connecter avec l’authentification unique avant d’utiliser web app',
|
||||
},
|
||||
modalTip: 'Paramètres de l’application web côté client.',
|
||||
},
|
||||
|
||||
@ -161,9 +161,9 @@ const translation = {
|
||||
},
|
||||
},
|
||||
answerIcon: {
|
||||
description: 'S’il faut utiliser l’icône WebApp pour remplacer 🤖 dans l’application partagée',
|
||||
title: 'Utiliser l’icône WebApp pour remplacer 🤖',
|
||||
descriptionInExplore: 'Utilisation de l’icône WebApp pour remplacer 🤖 dans Explore',
|
||||
description: 'S’il faut utiliser l’icône web app pour remplacer 🤖 dans l’application partagée',
|
||||
title: 'Utiliser l’icône web app pour remplacer 🤖',
|
||||
descriptionInExplore: 'Utilisation de l’icône web app pour remplacer 🤖 dans Explore',
|
||||
},
|
||||
importFromDSLUrlPlaceholder: 'Collez le lien DSL ici',
|
||||
importFromDSL: 'Importation à partir d’une DSL',
|
||||
|
||||
@ -5,7 +5,7 @@ const translation = {
|
||||
suffix: 'personnalisez votre marque.',
|
||||
},
|
||||
webapp: {
|
||||
title: 'Personnalisez la marque WebApp',
|
||||
title: 'Personnalisez la marque web app',
|
||||
removeBrand: 'Supprimer Propulsé par Dify',
|
||||
changeLogo: 'Changer Propulsé par l\'Image de Marque',
|
||||
changeLogoTip: 'Format SVG ou PNG avec une taille minimum de 40x40px',
|
||||
|
||||
@ -53,7 +53,7 @@ const translation = {
|
||||
show: 'दिखाएं',
|
||||
hide: 'छुपाएं',
|
||||
subTitle: 'कार्यप्रवाह विवरण',
|
||||
showDesc: 'WebApp में वर्कफ़्लो विवरण दिखाएँ या छुपाएँ',
|
||||
showDesc: 'web app में वर्कफ़्लो विवरण दिखाएँ या छुपाएँ',
|
||||
},
|
||||
chatColorTheme: 'चैटबॉट का रंग थीम',
|
||||
chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें',
|
||||
@ -77,8 +77,8 @@ const translation = {
|
||||
sso: {
|
||||
title: 'वेबएप एसएसओ',
|
||||
label: 'SSO प्रमाणीकरण',
|
||||
description: 'WebApp का उपयोग करने से पहले सभी उपयोगकर्ताओं को SSO के साथ लॉगिन करना आवश्यक है',
|
||||
tooltip: 'WebApp SSO को सक्षम करने के लिए व्यवस्थापक से संपर्क करें',
|
||||
description: 'web app का उपयोग करने से पहले सभी उपयोगकर्ताओं को SSO के साथ लॉगिन करना आवश्यक है',
|
||||
tooltip: 'web app SSO को सक्षम करने के लिए व्यवस्थापक से संपर्क करें',
|
||||
},
|
||||
modalTip: 'क्लाइंट-साइड वेब अनुप्रयोग सेटिंग्स.',
|
||||
},
|
||||
|
||||
@ -161,9 +161,9 @@ const translation = {
|
||||
},
|
||||
},
|
||||
answerIcon: {
|
||||
title: 'बदलने 🤖 के लिए WebApp चिह्न का उपयोग करें',
|
||||
title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें',
|
||||
descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं',
|
||||
description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं',
|
||||
description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए web app चिह्न का उपयोग करना है या नहीं',
|
||||
},
|
||||
importFromDSLFile: 'डीएसएल फ़ाइल से',
|
||||
importFromDSLUrl: 'यूआरएल से',
|
||||
|
||||
@ -5,7 +5,7 @@ const translation = {
|
||||
suffix: 'स्वयं अपना ब्रांड चुनना।',
|
||||
},
|
||||
webapp: {
|
||||
title: 'WebApp का ब्रांड व्यक्तिकरण करें',
|
||||
title: 'web app का ब्रांड व्यक्तिकरण करें',
|
||||
removeBrand: 'पावर्ड द्वारा डिफी हटाएं',
|
||||
changeLogo: 'पावर्ड द्वारा ब्रांड छवि बदले',
|
||||
changeLogoTip: 'SVG या PNG प्रारूप के साथ न्यूनतम आकार 40x40px होना चाहिए',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user