Compare commits

...

70 Commits

Author SHA1 Message Date
Garfield Dai
7f63cd52a2 update. 2025-03-24 23:08:54 +08:00
NFish
5b357fdbf0 Merge branch 'release/0.15.5' into e-0154 2025-03-24 16:42:11 +08:00
NFish
9283a5414f fix: update yarn.lock 2025-03-24 16:41:07 +08:00
NFish
8923e64b8d Merge branch 'release/0.15.5' into e-0154 2025-03-24 15:40:32 +08:00
-LAN-
2a2a0e9be9 fix: update DifySandbox image version to 0.2.11 in docker-compose files
Sgned-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 15:37:55 +08:00
Joel
061a765b7d fix: sanitizer svg to avoid xss (#16608) 2025-03-24 14:48:40 +08:00
-LAN-
acd7fead87 feat: remove Vanna provider and associated assets from the project
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 14:34:03 +08:00
KVOJJJin
64e9d96d84 chore: compatible with es5 (#14268) 2025-03-24 13:17:48 +08:00
NFish
d27de3818c Merge branch 'release/0.15.5' into e-0154 2025-03-24 11:46:30 +08:00
NFish
bbb080d5b2 fix: update chatbot help doc link on the create app form 2025-03-24 11:28:35 +08:00
NFish
8c025abb3b Merge branch 'release/0.15.5' into e-0154 2025-03-24 10:32:56 +08:00
NFish
c01d8a70f3 fix: upgrade nextjs to v14.2.25. a security patch for CVE-2025-29927. 2025-03-24 10:32:18 +08:00
NFish
98606ca558 fix: upgrade nextjs to v14.2.25 2025-03-24 10:12:21 +08:00
Garfield Dai
adf3e18ebd Merge tag '0.15.4' into e-0154 2025-03-21 18:29:43 +08:00
-LAN-
1ca15989e0 chore: update version to 0.15.4 in configuration and docker files
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 16:39:06 +08:00
-LAN-
8b5a3a9424 Merge branch 'release/0.15.4' of github.com:langgenius/dify into release/0.15.4 2025-03-21 16:31:06 +08:00
-LAN-
42ddcf1edd chore: remove 0.15.3 branch config in the build action
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 16:30:33 +08:00
Joel
21561df10f fix: xss in render svg (#16437) 2025-03-21 15:24:58 +08:00
Byron.wang
4327ec8c4c fix license expireAt field typo (#16428) 2025-03-21 13:43:43 +08:00
NFish
bbc5ec8301 fix: expired date calc error 2025-03-21 11:00:07 +08:00
NFish
4a51a72c1d Merge branch 'e-0154' into deploy/enterprise 2025-03-20 17:34:52 +08:00
NFish
4b6adffa8e fix: hide copyright on forgot-password/install/reset-password page 2025-03-20 17:34:19 +08:00
NFish
c7fd73d330 Merge branch 'e-0154' into deploy/enterprise 2025-03-20 10:13:09 +08:00
NFish
8a709e445a fix: remove Dify from Service API doc 2025-03-20 10:12:27 +08:00
NFish
f02b77b99f fix: Decouple login page logo component to avoid conflict with internal logo 2025-03-20 10:11:26 +08:00
GareArc
abc625bcce Merge branch 'e-0154' into deploy/enterprise 2025-03-18 22:35:39 -04:00
GareArc
b6bc1f8bc4 fix: adjust logic for branding toggle 2025-03-18 22:35:27 -04:00
NFish
b8f9037cd3 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 16:13:14 +08:00
NFish
02606ba3c7 fix: cannot update webapp copyright info 2025-03-18 16:12:52 +08:00
GareArc
79311d3fb5 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 03:53:18 -04:00
GareArc
31086a1fbf feat: add webapp copyright feature 2025-03-18 03:53:07 -04:00
NFish
6ae5d052e5 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 14:55:36 +08:00
NFish
c794ecf101 fix: user can edit webapp copyright info only if webapp_copyright_enabled is true 2025-03-18 14:54:34 +08:00
GareArc
d887aae012 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 01:55:38 -04:00
GareArc
1b1e96eff7 fix: typo 2025-03-18 01:55:27 -04:00
GareArc
eecd091063 Merge branch 'e-0154' into deploy/enterprise 2025-03-17 15:34:49 -04:00
GareArc
d38f2cb380 fix: change subject title 2025-03-17 15:34:28 -04:00
GareArc
56aaee5558 fix: wrong branding title 2025-03-17 15:01:31 -04:00
GareArc
d72b4752c9 fix: wrong title location 2025-03-17 15:00:04 -04:00
GareArc
ea769c6483 Merge branch 'e-0154' into deploy/enterprise 2025-03-17 14:24:00 -04:00
GareArc
ec194fa3d4 fix: invalid email template variables 2025-03-17 14:23:46 -04:00
NFish
b877039859 Merge branch 'e-0154' into deploy/enterprise 2025-03-17 10:37:20 +08:00
NFish
54634f26d2 fix: show copyright in webapp 2025-03-17 10:36:51 +08:00
NFish
3bef91a2cd fix: show loading icon when fetching system features 2025-03-15 12:01:30 +08:00
NFish
7da45ba589 fix: show loading icon when fetching system features 2025-03-15 12:00:22 +08:00
NFish
e0232c67cc fix: update document title and favicon in client side 2025-03-15 12:00:22 +08:00
GareArc
1dc4a229d4 Merge branch 'e-0154' into deploy/enterprise 2025-03-14 16:37:02 -04:00
GareArc
0e0bada1f3 fix: missing json keys 2025-03-14 16:36:49 -04:00
GareArc
5366a814f9 fix: update json keys 2025-03-14 16:35:05 -04:00
GareArc
f1240a22db fix: remove default value 2025-03-14 13:26:44 -04:00
NFish
66f35c2b7e Merge branch 'e-0154' into deploy/enterprise 2025-03-15 01:25:15 +08:00
NFish
766ee48531 fix: update document title and favicon in client side 2025-03-15 01:25:04 +08:00
NFish
083045f45c Merge branch 'e-0154' into deploy/enterprise 2025-03-14 20:49:17 +08:00
NFish
fe237802c9 fix: update Dify text 2025-03-14 19:10:03 +08:00
NFish
00b923651f fix: update document title with system features config 2025-03-14 19:10:03 +08:00
NFish
24fce3cc64 chore: use global zustand manage systemFeatures and share between all pages 2025-03-14 19:10:03 +08:00
GareArc
8ba969f67d fix: add ci workflow 2025-03-13 17:15:11 -04:00
GareArc
6844d59371 fix: add default title name 2025-03-13 17:07:45 -04:00
GareArc
fe5529db85 Trigger workflow 2025-03-13 17:04:13 -04:00
GareArc
d89034d913 feat: add application title 2025-03-13 15:49:04 -04:00
NFish
360fbeb108 fix: update email template, add application_title 2025-03-13 17:28:49 +08:00
GareArc
e7c2fa1cfa fix: remove system feature is_branding 2025-03-12 10:48:58 -04:00
Hash Brown
735f09d977 fix: build failed due to getPrevChatList no longer exists (#13383) 2025-03-12 10:22:33 +08:00
GareArc
f83a5e3e49 fix: wrong type 2025-03-11 07:46:48 -04:00
NFish
01a8d4efcc fix: remove dify from invite template 2025-03-11 19:25:30 +08:00
GareArc
fdb1e649d4 feat: add branding support 2025-03-11 07:14:52 -04:00
NFish
0856792a57 fix: add email templates that are no brands or logo 2025-03-11 16:03:15 +08:00
crazywoola
0e33a3aa5f chore: add ci 2025-02-19 14:34:36 +08:00
Hash Brown
d3895bcd6b revert 2025-02-19 14:32:28 +08:00
Hash Brown
eeb390650b fix: build failed 2025-02-19 14:32:28 +08:00
87 changed files with 1105 additions and 809 deletions

View File

@@ -5,6 +5,8 @@ on:
branches:
- "main"
- "deploy/dev"
- "deploy/enterprise"
- "e-0154"
release:
types: [published]

29
.github/workflows/deploy-enterprise.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Deploy Enterprise
permissions:
contents: read
on:
workflow_run:
workflows: ["Build and Push API & Web"]
branches:
- "deploy/enterprise"
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/enterprise'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
with:
host: ${{ secrets.ENTERPRISE_SSH_HOST }}
username: ${{ secrets.ENTERPRISE_SSH_USER }}
password: ${{ secrets.ENTERPRISE_SSH_PASSWORD }}
script: |
${{ vars.ENTERPRISE_SSH_SCRIPT || secrets.ENTERPRISE_SSH_SCRIPT }}

View File

@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
default="0.15.3",
default="0.15.4",
)
COMMIT_SHA: str = Field(

View File

@@ -39,6 +39,17 @@ def only_edition_cloud(view):
return decorated
def only_enterprise_edition(view):
@wraps(view)
def decorated(*args, **kwargs):
if not dify_config.ENTERPRISE_ENABLED:
abort(404)
return view(*args, **kwargs)
return decorated
def only_edition_self_hosted(view):
@wraps(view)
def decorated(*args, **kwargs):

View File

@@ -77,5 +77,4 @@
- onebot
- regex
- trello
- vanna
- fal

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,134 +0,0 @@
from typing import Any, Union
from vanna.remote import VannaDefault # type: ignore
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.tool.builtin_tool import BuiltinTool
class VannaTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
"""
invoke tools
"""
# Ensure runtime and credentials
if not self.runtime or not self.runtime.credentials:
raise ToolProviderCredentialValidationError("Tool runtime or credentials are missing")
api_key = self.runtime.credentials.get("api_key", None)
if not api_key:
raise ToolProviderCredentialValidationError("Please input api key")
model = tool_parameters.get("model", "")
if not model:
return self.create_text_message("Please input RAG model")
prompt = tool_parameters.get("prompt", "")
if not prompt:
return self.create_text_message("Please input prompt")
url = tool_parameters.get("url", "")
if not url:
return self.create_text_message("Please input URL/Host/DSN")
db_name = tool_parameters.get("db_name", "")
username = tool_parameters.get("username", "")
password = tool_parameters.get("password", "")
port = tool_parameters.get("port", 0)
base_url = self.runtime.credentials.get("base_url", None)
vn = VannaDefault(model=model, api_key=api_key, config={"endpoint": base_url})
db_type = tool_parameters.get("db_type", "")
if db_type in {"Postgres", "MySQL", "Hive", "ClickHouse"}:
if not db_name:
return self.create_text_message("Please input database name")
if not username:
return self.create_text_message("Please input username")
if port < 1:
return self.create_text_message("Please input port")
schema_sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS"
match db_type:
case "SQLite":
schema_sql = "SELECT type, sql FROM sqlite_master WHERE sql is not null"
vn.connect_to_sqlite(url)
case "Postgres":
vn.connect_to_postgres(host=url, dbname=db_name, user=username, password=password, port=port)
case "DuckDB":
vn.connect_to_duckdb(url=url)
case "SQLServer":
vn.connect_to_mssql(url)
case "MySQL":
vn.connect_to_mysql(host=url, dbname=db_name, user=username, password=password, port=port)
case "Oracle":
vn.connect_to_oracle(user=username, password=password, dsn=url)
case "Hive":
vn.connect_to_hive(host=url, dbname=db_name, user=username, password=password, port=port)
case "ClickHouse":
vn.connect_to_clickhouse(host=url, dbname=db_name, user=username, password=password, port=port)
enable_training = tool_parameters.get("enable_training", False)
reset_training_data = tool_parameters.get("reset_training_data", False)
if enable_training:
if reset_training_data:
existing_training_data = vn.get_training_data()
if len(existing_training_data) > 0:
for _, training_data in existing_training_data.iterrows():
vn.remove_training_data(training_data["id"])
ddl = tool_parameters.get("ddl", "")
question = tool_parameters.get("question", "")
sql = tool_parameters.get("sql", "")
memos = tool_parameters.get("memos", "")
training_metadata = tool_parameters.get("training_metadata", False)
if training_metadata:
if db_type == "SQLite":
df_ddl = vn.run_sql(schema_sql)
for ddl in df_ddl["sql"].to_list():
vn.train(ddl=ddl)
else:
df_information_schema = vn.run_sql(schema_sql)
plan = vn.get_training_plan_generic(df_information_schema)
vn.train(plan=plan)
if ddl:
vn.train(ddl=ddl)
if sql:
if question:
vn.train(question=question, sql=sql)
else:
vn.train(sql=sql)
if memos:
vn.train(documentation=memos)
#########################################################################################
# Due to CVE-2024-5565, we have to disable the chart generation feature
# The Vanna library uses a prompt function to present the user with visualized results,
# it is possible to alter the prompt using prompt injection and run arbitrary Python code
# instead of the intended visualization code.
# Specifically - allowing external input to the librarys “ask” method
# with "visualize" set to True (default behavior) leads to remote code execution.
# Affected versions: <= 0.5.5
#########################################################################################
allow_llm_to_see_data = tool_parameters.get("allow_llm_to_see_data", False)
res = vn.ask(
prompt, print_results=False, auto_train=True, visualize=False, allow_llm_to_see_data=allow_llm_to_see_data
)
result = []
if res is not None:
result.append(self.create_text_message(res[0]))
if len(res) > 1 and res[1] is not None:
result.append(self.create_text_message(res[1].to_markdown()))
if len(res) > 2 and res[2] is not None:
result.append(
self.create_blob_message(blob=res[2].to_image(format="svg"), meta={"mime_type": "image/svg+xml"})
)
return result

View File

@@ -1,213 +0,0 @@
identity:
name: vanna
author: QCTC
label:
en_US: Vanna.AI
zh_Hans: Vanna.AI
description:
human:
en_US: The fastest way to get actionable insights from your database just by asking questions.
zh_Hans: 一个基于大模型和RAG的Text2SQL工具。
llm: A tool for converting text to SQL.
parameters:
- name: prompt
type: string
required: true
label:
en_US: Prompt
zh_Hans: 提示词
pt_BR: Prompt
human_description:
en_US: used for generating SQL
zh_Hans: 用于生成SQL
llm_description: key words for generating SQL
form: llm
- name: model
type: string
required: true
label:
en_US: RAG Model
zh_Hans: RAG模型
human_description:
en_US: RAG Model for your database DDL
zh_Hans: 存储数据库训练数据的RAG模型
llm_description: RAG Model for generating SQL
form: llm
- name: db_type
type: select
required: true
options:
- value: SQLite
label:
en_US: SQLite
zh_Hans: SQLite
- value: Postgres
label:
en_US: Postgres
zh_Hans: Postgres
- value: DuckDB
label:
en_US: DuckDB
zh_Hans: DuckDB
- value: SQLServer
label:
en_US: Microsoft SQL Server
zh_Hans: 微软 SQL Server
- value: MySQL
label:
en_US: MySQL
zh_Hans: MySQL
- value: Oracle
label:
en_US: Oracle
zh_Hans: Oracle
- value: Hive
label:
en_US: Hive
zh_Hans: Hive
- value: ClickHouse
label:
en_US: ClickHouse
zh_Hans: ClickHouse
default: SQLite
label:
en_US: DB Type
zh_Hans: 数据库类型
human_description:
en_US: Database type.
zh_Hans: 选择要链接的数据库类型。
form: form
- name: url
type: string
required: true
label:
en_US: URL/Host/DSN
zh_Hans: URL/Host/DSN
human_description:
en_US: Please input depending on DB type, visit https://vanna.ai/docs/ for more specification
zh_Hans: 请根据数据库类型填入对应值详情参考https://vanna.ai/docs/
form: form
- name: db_name
type: string
required: false
label:
en_US: DB name
zh_Hans: 数据库名
human_description:
en_US: Database name
zh_Hans: 数据库名
form: form
- name: username
type: string
required: false
label:
en_US: Username
zh_Hans: 用户名
human_description:
en_US: Username
zh_Hans: 用户名
form: form
- name: password
type: secret-input
required: false
label:
en_US: Password
zh_Hans: 密码
human_description:
en_US: Password
zh_Hans: 密码
form: form
- name: port
type: number
required: false
label:
en_US: Port
zh_Hans: 端口
human_description:
en_US: Port
zh_Hans: 端口
form: form
- name: ddl
type: string
required: false
label:
en_US: Training DDL
zh_Hans: 训练DDL
human_description:
en_US: DDL statements for training data
zh_Hans: 用于训练RAG Model的建表语句
form: llm
- name: question
type: string
required: false
label:
en_US: Training Question
zh_Hans: 训练问题
human_description:
en_US: Question-SQL Pairs
zh_Hans: Question-SQL中的问题
form: llm
- name: sql
type: string
required: false
label:
en_US: Training SQL
zh_Hans: 训练SQL
human_description:
en_US: SQL queries to your training data
zh_Hans: 用于训练RAG Model的SQL语句
form: llm
- name: memos
type: string
required: false
label:
en_US: Training Memos
zh_Hans: 训练说明
human_description:
en_US: Sometimes you may want to add documentation about your business terminology or definitions
zh_Hans: 添加更多关于数据库的业务说明
form: llm
- name: enable_training
type: boolean
required: false
default: false
label:
en_US: Training Data
zh_Hans: 训练数据
human_description:
en_US: You only need to train once. Do not train again unless you want to add more training data
zh_Hans: 训练数据无更新时,训练一次即可
form: form
- name: reset_training_data
type: boolean
required: false
default: false
label:
en_US: Reset Training Data
zh_Hans: 重置训练数据
human_description:
en_US: Remove all training data in the current RAG Model
zh_Hans: 删除当前RAG Model中的所有训练数据
form: form
- name: training_metadata
type: boolean
required: false
default: false
label:
en_US: Training Metadata
zh_Hans: 训练元数据
human_description:
en_US: If enabled, it will attempt to train on the metadata of that database
zh_Hans: 是否自动从数据库获取元数据来训练
form: form
- name: allow_llm_to_see_data
type: boolean
required: false
default: false
label:
en_US: Whether to allow the LLM to see the data
zh_Hans: 是否允许LLM查看数据
human_description:
en_US: Whether to allow the LLM to see the data
zh_Hans: 是否允许LLM查看数据
form: form

View File

@@ -1,46 +0,0 @@
import re
from typing import Any
from urllib.parse import urlparse
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.vanna.tools.vanna import VannaTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class VannaProvider(BuiltinToolProviderController):
def _get_protocol_and_main_domain(self, url):
parsed_url = urlparse(url)
protocol = parsed_url.scheme
hostname = parsed_url.hostname
port = f":{parsed_url.port}" if parsed_url.port else ""
# Check if the hostname is an IP address
is_ip = re.match(r"^\d{1,3}(\.\d{1,3}){3}$", hostname) is not None
# Return the full hostname (with port if present) for IP addresses, otherwise return the main domain
main_domain = f"{hostname}{port}" if is_ip else ".".join(hostname.split(".")[-2:]) + port
return f"{protocol}://{main_domain}"
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
base_url = credentials.get("base_url")
if not base_url:
base_url = "https://ask.vanna.ai/rpc"
else:
base_url = base_url.removesuffix("/")
credentials["base_url"] = base_url
try:
VannaTool().fork_tool_runtime(
runtime={
"credentials": credentials,
}
).invoke(
user_id="",
tool_parameters={
"model": "chinook",
"db_type": "SQLite",
"url": f"{self._get_protocol_and_main_domain(credentials['base_url'])}/Chinook.sqlite",
"query": "What are the top 10 customers by sales?",
},
)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

View File

@@ -1,35 +0,0 @@
identity:
author: QCTC
name: vanna
label:
en_US: Vanna.AI
zh_Hans: Vanna.AI
description:
en_US: The fastest way to get actionable insights from your database just by asking questions.
zh_Hans: 一个基于大模型和RAG的Text2SQL工具。
icon: icon.png
tags:
- utilities
- productivity
credentials_for_provider:
api_key:
type: secret-input
required: true
label:
en_US: API key
zh_Hans: API key
placeholder:
en_US: Please input your API key
zh_Hans: 请输入你的 API key
pt_BR: Please input your API key
help:
en_US: Get your API key from Vanna.AI
zh_Hans: 从 Vanna.AI 获取你的 API key
url: https://vanna.ai/account/profile
base_url:
type: text-input
required: false
label:
en_US: Vanna.AI Endpoint Base URL
placeholder:
en_US: https://ask.vanna.ai/rpc

View File

@@ -36,6 +36,14 @@ class LicenseModel(BaseModel):
expired_at: str = ""
class BrandingModel(BaseModel):
enabled: bool = False
application_title: str = ""
login_page_logo: str = ""
workspace_logo: str = ""
favicon: str = ""
class FeatureModel(BaseModel):
billing: BillingModel = BillingModel()
members: LimitationModel = LimitationModel(size=0, limit=1)
@@ -47,6 +55,7 @@ class FeatureModel(BaseModel):
can_replace_logo: bool = False
model_load_balancing_enabled: bool = False
dataset_operator_enabled: bool = False
webapp_copyright_enabled: bool = False
# pydantic configs
model_config = ConfigDict(protected_namespaces=())
@@ -65,6 +74,7 @@ class SystemFeatureModel(BaseModel):
is_allow_create_workspace: bool = False
is_email_setup: bool = False
license: LicenseModel = LicenseModel()
branding: BrandingModel = BrandingModel()
class FeatureService:
@@ -77,6 +87,9 @@ class FeatureService:
if dify_config.BILLING_ENABLED and tenant_id:
cls._fulfill_params_from_billing_api(features, tenant_id)
if dify_config.ENTERPRISE_ENABLED:
features.webapp_copyright_enabled = True
return features
@classmethod
@@ -87,7 +100,7 @@ class FeatureService:
if dify_config.ENTERPRISE_ENABLED:
system_features.enable_web_sso_switch_component = True
system_features.branding.enabled = True
cls._fulfill_params_from_enterprise(system_features)
return system_features
@@ -115,6 +128,9 @@ class FeatureService:
features.billing.subscription.plan = billing_info["subscription"]["plan"]
features.billing.subscription.interval = billing_info["subscription"]["interval"]
if features.billing.subscription.plan != "sandbox":
features.webapp_copyright_enabled = True
if "members" in billing_info:
features.members.size = billing_info["members"]["size"]
features.members.limit = billing_info["members"]["limit"]
@@ -148,35 +164,41 @@ class FeatureService:
def _fulfill_params_from_enterprise(cls, features):
enterprise_info = EnterpriseService.get_info()
if "sso_enforced_for_signin" in enterprise_info:
features.sso_enforced_for_signin = enterprise_info["sso_enforced_for_signin"]
if "SSOEnforcedForSignin" in enterprise_info:
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
if "sso_enforced_for_signin_protocol" in enterprise_info:
features.sso_enforced_for_signin_protocol = enterprise_info["sso_enforced_for_signin_protocol"]
if "SSOEnforcedForSigninProtocol" in enterprise_info:
features.sso_enforced_for_signin_protocol = enterprise_info["SSOEnforcedForSigninProtocol"]
if "sso_enforced_for_web" in enterprise_info:
features.sso_enforced_for_web = enterprise_info["sso_enforced_for_web"]
if "SSOEnforcedForWeb" in enterprise_info:
features.sso_enforced_for_web = enterprise_info["SSOEnforcedForWeb"]
if "sso_enforced_for_web_protocol" in enterprise_info:
features.sso_enforced_for_web_protocol = enterprise_info["sso_enforced_for_web_protocol"]
if "SSOEnforcedForWebProtocol" in enterprise_info:
features.sso_enforced_for_web_protocol = enterprise_info["SSOEnforcedForWebProtocol"]
if "enable_email_code_login" in enterprise_info:
features.enable_email_code_login = enterprise_info["enable_email_code_login"]
if "EnableEmailCodeLogin" in enterprise_info:
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
if "enable_email_password_login" in enterprise_info:
features.enable_email_password_login = enterprise_info["enable_email_password_login"]
if "EnableEmailPasswordLogin" in enterprise_info:
features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
if "is_allow_register" in enterprise_info:
features.is_allow_register = enterprise_info["is_allow_register"]
if "IsAllowRegister" in enterprise_info:
features.is_allow_register = enterprise_info["IsAllowRegister"]
if "is_allow_create_workspace" in enterprise_info:
features.is_allow_create_workspace = enterprise_info["is_allow_create_workspace"]
if "IsAllowCreateWorkspace" in enterprise_info:
features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"]
if "license" in enterprise_info:
license_info = enterprise_info["license"]
if "Branding" in enterprise_info:
features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "")
features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "")
features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "")
features.branding.favicon = enterprise_info["Branding"].get("favicon", "")
if "License" in enterprise_info:
license_info = enterprise_info["License"]
if "status" in license_info:
features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE))
if "expired_at" in license_info:
features.license.expired_at = license_info["expired_at"]
if "expiredAt" in license_info:
features.license.expired_at = license_info["expiredAt"]

View File

@@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
from flask import render_template
from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail")
@@ -25,10 +26,24 @@ def send_email_code_login_mail_task(language: str, to: str, code: str):
# send email code login mail using different languages
try:
if language == "zh-Hans":
html_content = render_template("email_code_login_mail_template_zh-CN.html", to=to, code=code)
template = "email_code_login_mail_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/email_code_login_mail_template_zh-CN.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="邮箱验证码", html=html_content)
else:
html_content = render_template("email_code_login_mail_template_en-US.html", to=to, code=code)
template = "email_code_login_mail_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/email_code_login_mail_template_en-US.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="Email Code", html=html_content)
end_at = time.perf_counter()

View File

@@ -7,6 +7,7 @@ from flask import render_template
from configs import dify_config
from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail")
@@ -33,23 +34,45 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam
try:
url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}"
if language == "zh-Hans":
html_content = render_template(
"invite_member_mail_template_zh-CN.html",
to=to,
inviter_name=inviter_name,
workspace_name=workspace_name,
url=url,
)
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
template = "invite_member_mail_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/invite_member_mail_template_zh-CN.html"
html_content = render_template(
template,
to=to,
inviter_name=inviter_name,
workspace_name=workspace_name,
url=url,
application_title=application_title,
)
mail.send(to=to, subject=f"立即加入 {application_title} 工作空间", html=html_content)
else:
html_content = render_template(
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
)
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
else:
html_content = render_template(
"invite_member_mail_template_en-US.html",
to=to,
inviter_name=inviter_name,
workspace_name=workspace_name,
url=url,
)
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content)
template = "invite_member_mail_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/invite_member_mail_template_en-US.html"
html_content = render_template(
template,
to=to,
inviter_name=inviter_name,
workspace_name=workspace_name,
url=url,
application_title=application_title,
)
mail.send(to=to, subject=f"Join {application_title} Workspace Now", html=html_content)
else:
html_content = render_template(
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
)
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content)
end_at = time.perf_counter()
logging.info(

View File

@@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
from flask import render_template
from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail")
@@ -25,11 +26,27 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
# send reset password mail using different languages
try:
if language == "zh-Hans":
html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code)
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
template = "reset_password_mail_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/reset_password_mail_template_zh-CN.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
mail.send(to=to, subject=f"设置您的 {application_title} 密码", html=html_content)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
else:
html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code)
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
template = "reset_password_mail_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/reset_password_mail_template_en-US.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
mail.send(to=to, subject=f"Set Your {application_title} Password", html=html_content)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
end_at = time.perf_counter()
logging.info(

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">Your login code for {{application_title}}</p>
<p class="description">Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didn't request a login, don't worry. You can safely ignore this email.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">{{application_title}} 的登录验证码</p>
<p class="description">复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求登录,请不要担心。您可以安全地忽略此电子邮件。</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>Dear {{ to }},</p>
<p>{{ inviter_name }} is pleased to invite you to join our workspace on {{application_title}}, a platform specifically designed for LLM application development. On {{application_title}}, you can explore, create, and collaborate to build and operate AI applications.</p>
<p>Click the button below to log in to {{application_title}} and join the workspace.</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p>
</div>
<div class="footer">
<p>Best regards,</p>
<p>{{application_title}} Team</p>
<p>Please do not reply directly to this email; it is automatically sent by the system.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>尊敬的 {{ to }}</p>
<p>{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
<p>点击下方按钮即可登录 {{application_title}} 并且加入空间。</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
</div>
<div class="footer">
<p>此致,</p>
<p>{{application_title}} 团队</p>
<p>请不要直接回复此电子邮件;由系统自动发送。</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">Set your {{application_title}} password</p>
<p class="description">Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didn't request, don't worry. You can safely ignore this email.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">设置您的 {{application_title}} 账户密码</p>
<p class="description">复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求,请不要担心。您可以安全地忽略此电子邮件。</p>
</div>
</body>
</html>

View File

@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:0.15.3
image: langgenius/dify-api:0.15.4
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.3
image: langgenius/dify-api:0.15.4
restart: always
environment:
# Use the shared environment variables.
@@ -47,7 +47,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:0.15.3
image: langgenius/dify-web:0.15.4
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -98,7 +98,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations

View File

@@ -43,7 +43,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations

View File

@@ -393,7 +393,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:0.15.3
image: langgenius/dify-api:0.15.4
restart: always
environment:
# Use the shared environment variables.
@@ -416,7 +416,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:0.15.3
image: langgenius/dify-api:0.15.4
restart: always
environment:
# Use the shared environment variables.
@@ -438,7 +438,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:0.15.3
image: langgenius/dify-web:0.15.4
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -489,7 +489,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations

View File

@@ -15,17 +15,17 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { useContextSelector } from 'use-context-selector'
import s from './style.module.css'
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 AppContext, { useAppContext } from '@/context/app-context'
import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { App } from '@/types/app'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@@ -56,7 +56,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [
@@ -98,7 +98,11 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
useEffect(() => {
if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify`
if (systemFeatures.branding.enabled)
document.title = `${(appDetail.name || 'App')} - ${systemFeatures.branding.application_title}`
else
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
@@ -106,7 +110,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
// setAppSiderbarExpand('collapse')
}
}, [appDetail, isMobile])
}, [appDetail, isMobile, pathname, setAppSiderbarExpand, systemFeatures])
useEffect(() => {
setAppDetail()

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
@@ -20,7 +20,7 @@ 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 AppContext from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ICardViewProps = {
appId: string
@@ -31,7 +31,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const updateAppDetail = async () => {
try {

View File

@@ -2,7 +2,9 @@
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetail = {
children: React.ReactNode
@@ -11,11 +13,13 @@ export type IAppDetail = {
const AppDetail: FC<IAppDetail> = ({ children }) => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useDocumentTitle(t('common.menus.appDetail'))
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator])
}, [isCurrentWorkspaceDatasetOperator, router])
return (
<>

View File

@@ -85,7 +85,6 @@ const Apps = () => {
]
useEffect(() => {
document.title = `${t('common.menus.apps')} - Dify`
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()

View File

@@ -1,21 +1,20 @@
'use client'
import { useContextSelector } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
import Link from 'next/link'
import style from '../list.module.css'
import Apps from './Apps'
import AppContext from '@/context/app-context'
import { LicenseStatus } from '@/types/feature'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
const AppList = () => {
const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle(t('common.menus.apps'))
return (
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
<Apps />
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='px-12 py-6 grow-0 shrink-0'>
{!systemFeatures.branding.enabled && <footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3>
<p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='flex items-center gap-2 mt-3'>

View File

@@ -31,6 +31,7 @@ import { getLocaleOnClient } from '@/i18n'
import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@@ -186,11 +187,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
}
return baseNavigation
}, [datasetRes?.provider, datasetId, t])
useEffect(() => {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
useDocumentTitle(`${datasetRes?.name || 'Dataset'}`)
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)

View File

@@ -29,9 +29,11 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Container = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter()
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
@@ -123,7 +125,7 @@ const Container = () => {
{activeTab === 'dataset' && (
<>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
<DatasetFooter />
{!systemFeatures.branding.enabled && <DatasetFooter />}
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}

View File

@@ -3,7 +3,6 @@
import { useEffect, useRef } from 'react'
import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import NewDatasetCard from './NewDatasetCard'
import DatasetCard from './DatasetCard'
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
@@ -57,11 +56,8 @@ const Datasets = ({
const loadingStateRef = useRef(false)
const anchorRef = useRef<HTMLAnchorElement>(null)
const { t } = useTranslation()
useEffect(() => {
loadingStateRef.current = isLoading
document.title = `${t('dataset.knowledge')} - Dify`
}, [isLoading])
useEffect(() => {
@@ -80,7 +76,7 @@ const Datasets = ({
return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
{isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} />}
{data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
))}

View File

@@ -1,11 +1,12 @@
'use client'
import { useTranslation } from 'react-i18next'
import Container from './Container'
import useDocumentTitle from '@/hooks/use-document-title'
const AppList = async () => {
const AppList = () => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.datasets'))
return <Container />
}
export const metadata = {
title: 'Datasets - Dify',
}
export default AppList

View File

@@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div>
### Authentication
Service API of Dify authenticates using an `API-Key`.
Service API authenticates using an `API-Key`.
It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss.

View File

@@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div>
### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。
Service API 使用 `API-Key` 进行鉴权。
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。

View File

@@ -1,11 +1,13 @@
import type { FC } from 'react'
'use client'
import type { FC, PropsWithChildren } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import ExploreClient from '@/app/components/explore'
export type IAppDetail = {
children: React.ReactNode
}
import useDocumentTitle from '@/hooks/use-document-title'
const AppDetail: FC<IAppDetail> = ({ children }) => {
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.explore'))
return (
<ExploreClient>
{children}
@@ -13,4 +15,4 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
)
}
export default React.memo(AppDetail)
export default React.memo(ExploreLayout)

View File

@@ -30,9 +30,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
</>
)
}
export const metadata = {
title: 'Dify',
}
export default Layout

View File

@@ -1,22 +1,16 @@
'use client'
import type { FC } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ToolProviderList from '@/app/components/tools/provider-list'
import { useAppContext } from '@/context/app-context'
const Layout: FC = () => {
const { t } = useTranslation()
import useDocumentTitle from '@/hooks/use-document-title'
const ToolsList: FC = () => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
useEffect(() => {
if (typeof window !== 'undefined')
document.title = `${t('tools.title')} - Dify`
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator, router, t])
const { t } = useTranslation()
useDocumentTitle(t('common.menus.tools'))
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
@@ -25,4 +19,4 @@ const Layout: FC = () => {
return <ToolProviderList />
}
export default React.memo(Layout)
export default React.memo(ToolsList)

View File

@@ -16,6 +16,7 @@ import { ToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input'
import { useGlobalPublicStore } from '@/context/global-public-context'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -28,7 +29,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useAppContext()
const { systemFeatures } = useGlobalPublicStore()
const { mutateUserProfile, userProfile, apps } = useAppContext()
const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
@@ -133,7 +134,7 @@ export default function AccountPage() {
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
</div>
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} />
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
<div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>

View File

@@ -5,9 +5,11 @@ import { useRouter } from 'next/navigation'
import Button from '../components/base/button'
import Avatar from './avatar'
import LogoSite from '@/app/components/base/logo/logo-site'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Header = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter()
const back = () => {
@@ -25,7 +27,7 @@ const Header = () => {
<div className='flex items-center flex-shrink-0 gap-3'>
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
<RiRobot2Line className='w-4 h-4' />
<p>{t('common.account.studio')}</p>
<p>{!systemFeatures.branding.enabled && 'Dify '}{t('common.account.studio')}</p>
<RiArrowRightUpLine className='w-4 h-4' />
</Button>
<div className='w-[1px] h-4 bg-divider-regular' />

View File

@@ -32,9 +32,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
</>
)
}
export const metadata = {
title: 'Dify',
}
export default Layout

View File

@@ -1,6 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import AccountPage from './account-page'
import useDocumentTitle from '@/hooks/use-document-title'
export default function Account() {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.account'))
return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'>
<AccountPage />
</div>

View File

@@ -7,8 +7,10 @@ import Button from '@/app/components/base/button'
import { invitationCheck } from '@/service/common'
import Loading from '@/app/components/base/loading'
import useDocumentTitle from '@/hooks/use-document-title'
const ActivateForm = () => {
useDocumentTitle('')
const router = useRouter()
const { t } = useTranslation()
const searchParams = useSearchParams()

View File

@@ -1,10 +1,13 @@
'use client'
import React from 'react'
import Header from '../signin/_header'
import style from '../signin/page.module.css'
import ActivateForm from './activateForm'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Activate = () => {
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={cn(
style.background,
@@ -21,9 +24,9 @@ const Activate = () => {
}>
<Header />
<ActivateForm />
<div className='px-8 py-6 text-sm font-normal text-gray-500'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
)

View File

@@ -312,7 +312,7 @@ function AppPreview({ mode }: { mode: AppMode }) {
'chat': {
title: t('app.types.chatbot'),
description: t('app.newApp.chatbotUserDescription'),
link: 'https://docs.dify.ai/guides/application-orchestrate/conversation-application?fallback=true',
link: 'https://docs.dify.ai/guides/application-orchestrate#application_type',
},
'advanced-chat': {
title: t('app.types.advanced'),

View File

@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
import Link from 'next/link'
import { Trans, useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import Modal from '@/app/components/base/modal'
import ActionButton from '@/app/components/base/action-button'
@@ -21,13 +21,14 @@ 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 AppContext, { useAppContext } from '@/context/app-context'
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
@@ -65,7 +66,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose,
onSave,
}) => {
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const { isCurrentWorkspaceEditor } = useAppContext()
const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false)
@@ -110,7 +111,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
: { type: 'emoji', icon, background: icon_background! },
)
const { enableBilling, plan } = useProviderContext()
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const isFreePlan = plan.type === 'sandbox'
const handlePlanClick = useCallback(() => {
@@ -177,7 +178,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
chat_color_theme: inputInfo.chatColorTheme,
chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
prompt_public: false,
copyright: isFreePlan
copyright: !webappCopyrightEnabled
? ''
: inputInfo.copyrightSwitchValue
? inputInfo.copyright
@@ -354,7 +355,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.entry`)}</div>
<p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.more.copyRightPlaceholder`)} & {t(`${prefixSettings}.more.privacyPolicyPlaceholder`)}</p>
</div>
<RiArrowRightSLine className='shrink-0 ml-1 w-4 h-4 text-text-secondary'/>
<RiArrowRightSLine className='shrink-0 ml-1 w-4 h-4 text-text-secondary' />
</div>
)}
{/* more settings */}
@@ -380,14 +381,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
)}
</div>
<Tooltip
disabled={!isFreePlan}
disabled={webappCopyrightEnabled}
popupContent={
<div className='w-[260px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
<div className='w-[180px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
}
asChild={false}
>
<Switch
disabled={isFreePlan}
disabled={!webappCopyrightEnabled}
defaultValue={inputInfo.copyrightSwitchValue}
onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
@@ -439,20 +440,22 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
</div>
</Modal >
{showAppIconPicker && (
<AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(icon_type === 'image'
? { type: 'image', url: icon_url!, fileId: icon }
: { type: 'emoji', icon, background: icon_background! })
setShowAppIconPicker(false)
}}
/>
)}
{
showAppIconPicker && (
<AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(icon_type === 'image'
? { type: 'image', url: icon_url!, fileId: icon }
: { type: 'emoji', icon, background: icon_background! })
setShowAppIconPicker(false)
}}
/>
)
}
</>
)

View File

@@ -11,10 +11,12 @@ import { useLocalStorageState } from 'ahooks'
import produce from 'immer'
import type {
ChatConfig,
ChatItem,
Feedback,
} from '../types'
import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils'
import { buildChatItemTree, getProcessedInputsFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import {
fetchAppInfo,
fetchAppMeta,
@@ -32,6 +34,33 @@ import { useToastContext } from '@/app/components/base/toast'
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'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
messages.forEach((item) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: item.parent_message_id || undefined,
})
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
newChatList.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 }))),
parentMessageId: `question-${item.id}`,
})
})
return newChatList
}
export const useEmbeddedChatbot = () => {
const isInstalledApp = false
@@ -77,7 +106,7 @@ export const useEmbeddedChatbot = () => {
const appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data)
? buildChatItemTree(getFormattedChatList(appChatListData.data))
: [],
[appChatListData, currentConversationId],
)

View File

@@ -100,7 +100,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
let targetNode: ChatItemInTree | undefined
// find path to the target message
const stack = tree.toReversed().map(rootNode => ({
const stack = tree.slice().reverse().map(rootNode => ({
node: rootNode,
path: [rootNode],
}))

View File

@@ -2,6 +2,7 @@
import type { FC } from 'react'
import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type LogoSiteProps = {
className?: string
@@ -10,13 +11,17 @@ type LogoSiteProps = {
const LogoSite: FC<LogoSiteProps> = ({
className,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useSelector((s) => {
return {
theme: s.theme,
}
})
const src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.workspace_logo
return (
<img
src={src}

View File

@@ -10,6 +10,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter'
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import { Component, memo, useMemo, useRef, useState } from 'react'
import type { CodeComponent } from 'react-markdown/lib/ast-to-react'
import SVGRenderer from './svg-gallery'
import cn from '@/utils/classnames'
import CopyBtn from '@/app/components/base/copy-btn'
import SVGBtn from '@/app/components/base/svg'
@@ -18,7 +19,7 @@ import ImageGallery from '@/app/components/base/image-gallery'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import VideoGallery from '@/app/components/base/video-gallery'
import AudioGallery from '@/app/components/base/audio-gallery'
import SVGRenderer from '@/app/components/base/svg-gallery'
// import SVGRenderer from '@/app/components/base/svg-gallery'
import MarkdownButton from '@/app/components/base/markdown-blocks/button'
import MarkdownForm from '@/app/components/base/markdown-blocks/form'

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { SVG } from '@svgdotjs/svg.js'
import DOMPurify from 'dompurify'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
export const SVGRenderer = ({ content }: { content: string }) => {
@@ -44,7 +45,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
const rootElement = draw.svg(content)
const rootElement = draw.svg(DOMPurify.sanitize(content))
rootElement.click(() => {
setImagePreview(svgToDataURL(svgElement as Element))

View File

@@ -67,6 +67,7 @@ export type CurrentPlanInfoBackend = {
can_replace_logo: boolean
model_load_balancing_enabled: boolean
dataset_operator_enabled: boolean
webapp_copyright_enabled: boolean
}
export type SubscriptionItem = {

View File

@@ -15,7 +15,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。
Service API 使用 `API-Key` 进行鉴权。
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

@@ -14,7 +14,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
### Authentication
Dify Service API 使用 `API-Key` 进行鉴权。
Service API 使用 `API-Key` 进行鉴权。
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

@@ -2,7 +2,6 @@
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
@@ -16,7 +15,6 @@ export type IExploreProps = {
const Explore: FC<IExploreProps> = ({
children,
}) => {
const { t } = useTranslation()
const router = useRouter()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
@@ -24,7 +22,6 @@ const Explore: FC<IExploreProps> = ({
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
useEffect(() => {
document.title = `${t('explore.title')} - Dify`;
(async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)

View File

@@ -20,6 +20,7 @@ import { useModalContext } from '@/context/modal-context'
import { LanguagesSupported } from '@/i18n/language'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IAppSelector = {
isMobile: boolean
@@ -32,6 +33,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
`
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
const { systemFeatures } = useGlobalPublicStore()
const { locale } = useContext(I18n)
const { t } = useTranslation()
@@ -122,78 +124,80 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<div>{t('common.userProfile.settings')}</div>
</div>}
</Menu.Item>
{canEmailSupport && <Menu.Item>
{({ active }) => <a
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.emailSupport')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</a>}
</Menu.Item>}
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://github.com/langgenius/dify/discussions/categories/feedbacks'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.communityFeedback')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://discord.gg/5AEfbxcd9k'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.community')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href={
locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
}
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.helpCenter')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://roadmap.dify.ai'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.roadmap')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
{
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
<Menu.Item>
{({ active }) => <div className={classNames(itemClassName, 'justify-between',
{!systemFeatures.branding.enabled && <>
{canEmailSupport && <Menu.Item>
{({ active }) => <a
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}>
<div>{t('common.userProfile.about')}</div>
<div className='flex items-center'>
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
</div>}
</Menu.Item>
)
}
)}
href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.emailSupport')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</a>}
</Menu.Item>}
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://github.com/langgenius/dify/discussions/categories/feedbacks'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.communityFeedback')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://discord.gg/5AEfbxcd9k'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.community')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href={
locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
}
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.helpCenter')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <Link
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://roadmap.dify.ai'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.roadmap')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>}
</Menu.Item>
{
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
<Menu.Item>
{({ active }) => <div className={classNames(itemClassName, 'justify-between',
active && 'bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}>
<div>{t('common.userProfile.about')}</div>
<div className='flex items-center'>
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
</div>}
</Menu.Item>
)
}
</>}
</div>
<Menu.Item>
{({ active }) => <div className='p-1' onClick={() => handleLogout()}>

View File

@@ -21,6 +21,7 @@ import { Plan } from '@/app/components/billing/type'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { NUM_INFINITE } from '@/app/components/billing/config'
import { LanguagesSupported } from '@/i18n/language'
import { useGlobalPublicStore } from '@/context/global-public-context'
dayjs.extend(relativeTime)
const MembersPage = () => {
@@ -34,7 +35,8 @@ const MembersPage = () => {
}
const { locale } = useContext(I18n)
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
const { systemFeatures } = useGlobalPublicStore()
const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])

View File

@@ -4,7 +4,6 @@ import Link from 'next/link'
import { useBoolean } from 'ahooks'
import { useSelectedLayoutSegment } from 'next/navigation'
import { Bars3Icon } from '@heroicons/react/20/solid'
import { useContextSelector } from 'use-context-selector'
import HeaderBillingBtn from '../billing/header-billing-btn'
import AccountDropdown from './account-dropdown'
import AppNav from './app-nav'
@@ -15,12 +14,13 @@ import ToolsNav from './tools-nav'
import GithubStar from './github-star'
import LicenseNav from './license-env'
import { WorkspaceProvider } from '@/context/workspace-context'
import AppContext, { useAppContext } from '@/context/app-context'
import { useAppContext } from '@/context/app-context'
import LogoSite from '@/app/components/base/logo/logo-site'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { LicenseStatus } from '@/types/feature'
import { useGlobalPublicStore } from '@/context/global-public-context'
const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@@ -30,7 +30,7 @@ const navClassName = `
const Header = () => {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile

View File

@@ -5,14 +5,15 @@ import { LicenseStatus } from '@/types/feature'
import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import dayjs from 'dayjs'
import { useGlobalPublicStore } from '@/context/global-public-context'
const LicenseNav = () => {
const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
const expiredAt = systemFeatures.license?.expired_at
const count = dayjs(expiredAt).diff(dayjs(), 'days')
const count = dayjs(expiredAt).diff(dayjs(), 'day')
return <div className='px-2 py-1 mr-4 rounded-full bg-util-colors-orange-orange-50 border-util-colors-orange-orange-100 system-xs-medium text-util-colors-orange-orange-600'>
{count <= 1 && <span>{t('common.license.expiring', { count })}</span>}
{count > 1 && <span>{t('common.license.expiring_plural', { count })}</span>}

View File

@@ -17,9 +17,11 @@ import ProviderCard from '@/app/components/tools/provider/card'
import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { fetchCollectionList } from '@/service/tools'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ProviderList = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'builtin',
@@ -98,7 +100,7 @@ const ProviderList = () => {
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0',
currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
)}>
{activeTab === 'builtin' && <ContributeCard />}
{activeTab === 'builtin' && !systemFeatures.branding.enabled && <ContributeCard />}
{activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />}
{filteredCollectionList.map(collection => (
<ProviderCard

View File

@@ -21,8 +21,8 @@ const Contribute: FC = () => {
>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='relative shrink-0 flex items-center'>
<div className='z-10 flex p-3 rounded-[10px] bg-white border-[0.5px] border-primary-100 shadow-md'><RiHammerFill className='w-4 h-4 text-primary-600'/></div>
<div className='-translate-x-2 flex p-3 rounded-[10px] bg-[#FEF6FB] border-[0.5px] border-[#FCE7F6] shadow-md'><Heart02 className='w-4 h-4 text-[#EE46BC]'/></div>
<div className='z-10 flex p-3 rounded-[10px] bg-white border-[0.5px] border-primary-100 shadow-md'><RiHammerFill className='w-4 h-4 text-primary-600' /></div>
<div className='-translate-x-2 flex p-3 rounded-[10px] bg-[#FEF6FB] border-[0.5px] border-[#FCE7F6] shadow-md'><Heart02 className='w-4 h-4 text-[#EE46BC]' /></div>
</div>
</div>
<div className='mb-3 px-[14px] text-[15px] leading-5 font-semibold'>

View File

@@ -6,10 +6,14 @@ import Header from '../signin/_header'
import style from '../signin/page.module.css'
import ForgotPasswordForm from './ForgotPasswordForm'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams()
const token = searchParams.get('token')
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={classNames(
@@ -27,9 +31,9 @@ const ForgotPassword = () => {
}>
<Header />
{token ? <ChangePasswordForm /> : <ForgotPasswordForm />}
<div className='px-8 py-6 text-sm font-normal text-gray-500'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
)

View File

@@ -7,8 +7,10 @@ import Loading from '../components/base/loading'
import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, initValidate } from '@/service/common'
import type { InitValidateStatusResponse } from '@/models/common'
import useDocumentTitle from '@/hooks/use-document-title'
const InitPasswordPopup = () => {
useDocumentTitle('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(true)
const [validated, setValidated] = useState(false)

View File

@@ -15,6 +15,7 @@ import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import useDocumentTitle from '@/hooks/use-document-title'
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
@@ -32,6 +33,7 @@ const accountFormSchema = z.object({
type AccountFormValues = z.infer<typeof accountFormSchema>
const InstallForm = () => {
useDocumentTitle('')
const { t } = useTranslation()
const router = useRouter()
const [showPassword, setShowPassword] = React.useState(false)

View File

@@ -1,10 +1,13 @@
'use client'
import React from 'react'
import Header from '../signin/_header'
import style from '../signin/page.module.css'
import InstallForm from './installForm'
import classNames from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Install = () => {
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={classNames(
style.background,
@@ -21,9 +24,9 @@ const Install = () => {
}>
<Header />
<InstallForm />
<div className='px-8 py-6 text-sm font-normal text-gray-500'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
)

View File

@@ -1,4 +1,4 @@
import type { Viewport } from 'next'
import type { Metadata, Viewport } from 'next'
import I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor'
@@ -6,10 +6,7 @@ import { getLocaleOnServer } from '@/i18n/server'
import { TanstackQueryIniter } from '@/context/query-client'
import './styles/globals.css'
import './styles/markdown.scss'
export const metadata = {
title: 'Dify',
}
import GlobalPublicStoreProvider from '@/context/global-public-context'
export const viewport: Viewport = {
width: 'device-width',
@@ -18,6 +15,10 @@ export const viewport: Viewport = {
viewportFit: 'cover',
userScalable: false,
}
export const metadata: Metadata = {
title: ' ',
icons: 'data:',
}
const LocaleLayout = ({
children,
@@ -50,7 +51,11 @@ const LocaleLayout = ({
<BrowserInitor>
<SentryInitor>
<TanstackQueryIniter>
<I18nServer>{children}</I18nServer>
<I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryIniter>
</SentryInitor>
</BrowserInitor>

View File

@@ -1,9 +1,12 @@
'use client'
import Header from '../signin/_header'
import style from '../signin/page.module.css'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default async function SignInLayout({ children }: any) {
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
return <>
<div className={cn(
style.background,
@@ -30,9 +33,9 @@ export default async function SignInLayout({ children }: any) {
{children}
</div>
</div>
<div className='px-8 py-6 system-xs-regular text-text-tertiary'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 system-xs-regular text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
</>

View File

@@ -12,9 +12,11 @@ import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendResetPasswordCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import useDocumentTitle from '@/hooks/use-document-title'
export default function CheckCode() {
const { t } = useTranslation()
useDocumentTitle('')
const searchParams = useSearchParams()
const router = useRouter()
const [email, setEmail] = useState('')

View File

@@ -0,0 +1,34 @@
'use client'
import type { FC } from 'react'
import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type LoginLogoProps = {
className?: string
}
const LoginLogo: FC<LoginLogoProps> = ({
className,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useSelector((s) => {
return {
theme: s.theme,
}
})
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.login_page_logo
return (
<img
src={src}
className={classNames('block w-auto h-10', className)}
alt='logo'
/>
)
}
export default LoginLogo

View File

@@ -1,17 +1,17 @@
'use client'
import React from 'react'
import { useContext } from 'use-context-selector'
import LoginLogo from './LoginLogo'
import Select from '@/app/components/base/select/locale'
import { languages } from '@/i18n/language'
import { type Locale } from '@/i18n'
import I18n from '@/context/i18n'
import LogoSite from '@/app/components/base/logo/logo-site'
const Header = () => {
const { locale, setLocaleOnClient } = useContext(I18n)
return <div className='flex items-center justify-between p-6 w-full'>
<LogoSite />
<LoginLogo />
<Select
value={locale}
items={languages.filter(item => item.supported)}

View File

@@ -1,9 +1,14 @@
'use client'
import Header from './_header'
import style from './page.module.css'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
export default async function SignInLayout({ children }: any) {
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle('')
return <>
<div className={cn(
style.background,
@@ -30,9 +35,9 @@ export default async function SignInLayout({ children }: any) {
{children}
</div>
</div>
<div className='px-8 py-6 system-xs-regular text-text-tertiary'>
{systemFeatures.branding.enabled === false && <div className='px-8 py-6 system-xs-regular text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
</>

View File

@@ -9,10 +9,11 @@ import MailAndPasswordAuth from './components/mail-and-password-auth'
import SocialAuth from './components/social-auth'
import SSOAuth from './components/sso-auth'
import cn from '@/utils/classnames'
import { getSystemFeatures, invitationCheck } from '@/service/common'
import { LicenseStatus, defaultSystemFeatures } from '@/types/feature'
import { invitationCheck } from '@/service/common'
import { LicenseStatus } from '@/types/feature'
import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NormalForm = () => {
const { t } = useTranslation()
@@ -23,7 +24,7 @@ const NormalForm = () => {
const message = decodeURIComponent(searchParams.get('message') || '')
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
const [isLoading, setIsLoading] = useState(true)
const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
@@ -46,12 +47,9 @@ const NormalForm = () => {
message,
})
}
const features = await getSystemFeatures()
const allFeatures = { ...defaultSystemFeatures, ...features }
setSystemFeatures(allFeatures)
setAllMethodsAreDisabled(!allFeatures.enable_social_oauth_login && !allFeatures.enable_email_code_login && !allFeatures.enable_email_password_login && !allFeatures.sso_enforced_for_signin)
setShowORLine((allFeatures.enable_social_oauth_login || allFeatures.sso_enforced_for_signin) && (allFeatures.enable_email_code_login || allFeatures.enable_email_password_login))
updateAuthType(allFeatures.enable_email_password_login ? 'password' : 'code')
setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
if (isInviteLink) {
const checkRes = await invitationCheck({
url: '/activate/check',
@@ -65,10 +63,9 @@ const NormalForm = () => {
catch (error) {
console.error(error)
setAllMethodsAreDisabled(true)
setSystemFeatures(defaultSystemFeatures)
}
finally { setIsLoading(false) }
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink])
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, systemFeatures])
useEffect(() => {
init()
}, [init])
@@ -83,7 +80,7 @@ const NormalForm = () => {
<Loading type='area' />
</div>
}
if (systemFeatures.license?.status === LicenseStatus.LOST) {
if (systemFeatures.license.status === LicenseStatus.LOST) {
return <div className='w-full mx-auto mt-8'>
<div className='bg-white'>
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
@@ -97,7 +94,7 @@ const NormalForm = () => {
</div>
</div>
}
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) {
if (systemFeatures.license.status === LicenseStatus.EXPIRED) {
return <div className='w-full mx-auto mt-8'>
<div className='bg-white'>
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
@@ -111,7 +108,7 @@ const NormalForm = () => {
</div>
</div>
}
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) {
if (systemFeatures.license.status === LicenseStatus.INACTIVE) {
return <div className='w-full mx-auto mt-8'>
<div className='bg-white'>
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
@@ -132,11 +129,11 @@ const NormalForm = () => {
{isInviteLink
? <div className="w-full mx-auto">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.join')}{workspaceName}</h2>
<p className='mt-2 body-md-regular text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>
{!systemFeatures.branding.enabled && <p className='mt-2 body-md-regular text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>}
</div>
: <div className="w-full mx-auto">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2>
<p className='mt-2 body-md-regular text-text-tertiary'>{t('login.welcome')}</p>
{!systemFeatures.branding.enabled && <p className='mt-2 body-md-regular text-text-tertiary'>{t('login.welcome')}</p>}
</div>}
<div className="bg-white">
<div className="flex flex-col gap-3 mt-6">
@@ -184,29 +181,31 @@ const NormalForm = () => {
</div>
</div>
</>}
<div className="w-full block mt-2 system-xs-regular text-text-tertiary">
{t('login.tosDesc')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/terms'
>{t('login.tos')}</Link>
&nbsp;&&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/privacy'
>{t('login.pp')}</Link>
</div>
{IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary">
{t('login.goToInit')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
href='/install'
>{t('login.setAdminAccount')}</Link>
</div>}
{!systemFeatures.branding.enabled && <>
<div className="w-full block mt-2 system-xs-regular text-text-tertiary">
{t('login.tosDesc')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/terms'
>{t('login.tos')}</Link>
&nbsp;&&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/privacy'
>{t('login.pp')}</Link>
</div>
{IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary">
{t('login.goToInit')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
href='/install'
>{t('login.setAdminAccount')}</Link>
</div>}
</>}
</div>
</div>

View File

@@ -6,19 +6,16 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
import type { FC, ReactNode } from 'react'
import { fetchAppList } from '@/service/apps'
import Loading from '@/app/components/base/loading'
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common'
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
import type { App } from '@/types/app'
import { Theme } from '@/types/app'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
export type AppContextValue = {
theme: Theme
setTheme: (theme: Theme) => void
apps: App[]
systemFeatures: SystemFeatures
mutateApps: VoidFunction
userProfile: UserProfileResponse
mutateUserProfile: VoidFunction
@@ -57,7 +54,6 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
const AppContext = createContext<AppContextValue>({
theme: Theme.light,
systemFeatures: defaultSystemFeatures,
setTheme: () => { },
apps: [],
mutateApps: () => { },
@@ -96,10 +92,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
const { data: systemFeatures } = useSWR({ url: '/console/system-features' }, getSystemFeatures, {
fallbackData: defaultSystemFeatures,
})
const [userProfile, setUserProfile] = useState<UserProfileResponse>()
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
@@ -146,7 +138,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
theme,
setTheme: handleSetTheme,
apps: appList.data,
systemFeatures: { ...defaultSystemFeatures, ...systemFeatures },
mutateApps,
userProfile,
mutateUserProfile,

View File

@@ -0,0 +1,37 @@
'use client'
import { create } from 'zustand'
import { useQuery } from '@tanstack/react-query'
import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading'
type GlobalPublicStore = {
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
}
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
}))
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
children,
}) => {
const { isPending, data } = useQuery({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
const { setSystemFeatures } = useGlobalPublicStore()
useEffect(() => {
if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data })
}, [data, setSystemFeatures])
if (isPending)
return <div className='w-screen h-screen flex items-center justify-center'><Loading /></div>
return <>{children}</>
}
export default GlobalPublicStoreProvider

View File

@@ -35,6 +35,7 @@ type ProviderContextState = {
enableReplaceWebAppLogo: boolean
modelLoadBalancingEnabled: boolean
datasetOperatorEnabled: boolean
webappCopyrightEnabled: boolean
}
const ProviderContext = createContext<ProviderContextState>({
modelProviders: [],
@@ -64,6 +65,7 @@ const ProviderContext = createContext<ProviderContextState>({
enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false,
datasetOperatorEnabled: false,
webappCopyrightEnabled: false,
})
export const useProviderContext = () => useContext(ProviderContext)
@@ -91,6 +93,7 @@ export const ProviderContextProvider = ({
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false)
const fetchPlan = async () => {
const data = await fetchCurrentPlanInfo()
@@ -105,6 +108,8 @@ export const ProviderContextProvider = ({
setModelLoadBalancingEnabled(true)
if (data.dataset_operator_enabled)
setDatasetOperatorEnabled(true)
if (data.webapp_copyright_enabled)
setWebappCopyrightEnabled(true)
}
useEffect(() => {
fetchPlan()
@@ -123,6 +128,7 @@ export const ProviderContextProvider = ({
enableReplaceWebAppLogo,
modelLoadBalancingEnabled,
datasetOperatorEnabled,
webappCopyrightEnabled,
}}>
{children}
</ProviderContext.Provider>

View File

@@ -0,0 +1,18 @@
'use client'
import { useLayoutEffect } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default function useDocumentTitle(title: string) {
const { systemFeatures } = useGlobalPublicStore()
useLayoutEffect(() => {
const prefix = title ? `${title} - ` : ''
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
}
else {
document.title = `${prefix}Dify`
}
}, [systemFeatures, title])
}

View File

@@ -131,6 +131,8 @@ const translation = {
status: 'beta',
explore: 'Explore',
apps: 'Studio',
appDetail: 'App Detail',
account: 'Account',
plugins: 'Plugins',
pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.',
datasets: 'Knowledge',
@@ -167,7 +169,7 @@ const translation = {
account: {
account: 'Account',
myAccount: 'My Account',
studio: 'Dify Studio',
studio: 'Studio',
avatar: 'Avatar',
name: 'Name',
email: 'Email',
@@ -179,8 +181,8 @@ const translation = {
newPassword: 'New password',
confirmPassword: 'Confirm password',
notEqual: 'Two passwords are different.',
langGeniusAccount: 'Dify account',
langGeniusAccountTip: 'Your Dify account and associated user data.',
langGeniusAccount: 'Account\'s data',
langGeniusAccountTip: 'The user data of your account.',
editName: 'Edit Name',
showAppLength: 'Show {{length}} apps',
delete: 'Delete Account',

View File

@@ -16,7 +16,7 @@ const translation = {
},
},
apps: {
title: 'Explore Apps by Dify',
title: 'Explore Apps',
description: 'Use these template apps instantly or customize your own apps based on the templates.',
allCategories: 'Recommended',
},

View File

@@ -131,6 +131,8 @@ const translation = {
status: 'ベータ版',
explore: '探索',
apps: 'スタジオ',
appDetail: 'アプリの詳細',
account: 'アカウント',
plugins: 'プラグイン',
pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT互換のAIプラグインを作成します。',
datasets: 'ナレッジ',
@@ -176,8 +178,8 @@ const translation = {
newPassword: '新しいパスワード',
confirmPassword: 'パスワードを確認',
notEqual: '2つのパスワードが異なります。',
langGeniusAccount: 'Difyアカウント',
langGeniusAccountTip: 'Difyアカウント関連するユーザーデータ。',
langGeniusAccount: 'アカウント関連データ',
langGeniusAccountTip: 'アカウント関連するユーザーデータ。',
editName: '名前を編集',
showAppLength: '{{length}}アプリを表示',
delete: 'アカウントを削除',
@@ -185,7 +187,7 @@ const translation = {
deleteConfirmTip: '確認のため、登録したメールから次の内容をに送信してください ',
account: 'アカウント',
myAccount: 'マイアカウント',
studio: 'Difyスタジオ',
studio: 'スタジオ',
deletePrivacyLinkTip: 'お客様のデータの取り扱い方法の詳細については、当社の',
deletePrivacyLink: 'プライバシーポリシー。',
deleteSuccessTip: 'アカウントの削除が完了するまでに時間が必要です。すべて完了しましたら、メールでお知らせします。',

View File

@@ -16,7 +16,7 @@ const translation = {
},
},
apps: {
title: 'Difyによるアプリ探索',
title: 'アプリ探索',
description: 'これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。',
allCategories: '推奨',
},

View File

@@ -131,6 +131,8 @@ const translation = {
status: 'beta',
explore: '探索',
apps: '工作室',
appDetail: '应用详情',
account: '账户',
plugins: '插件',
pluginsTips: '集成第三方插件或创建与 ChatGPT 兼容的 AI 插件。',
datasets: '知识库',
@@ -167,7 +169,7 @@ const translation = {
account: {
account: '账户',
myAccount: '我的账户',
studio: 'Dify 工作室',
studio: '工作室',
avatar: '头像',
name: '用户名',
email: '邮箱',
@@ -179,8 +181,8 @@ const translation = {
newPassword: '新密码',
notEqual: '两个密码不相同',
confirmPassword: '确认密码',
langGeniusAccount: 'Dify 账号',
langGeniusAccountTip: '您的 Dify 账号相关的用户数据。',
langGeniusAccount: '账号关联数据',
langGeniusAccountTip: '您的账号相关的用户数据。',
editName: '编辑名字',
showAppLength: '显示 {{length}} 个应用',
delete: '删除账户',

View File

@@ -16,7 +16,7 @@ const translation = {
},
},
apps: {
title: '探索 Dify 的应用',
title: '探索应用',
description: '使用这些模板应用程序,或根据模板自定义您自己的应用程序。',
allCategories: '推荐',
},

View File

@@ -127,6 +127,8 @@ const translation = {
status: 'beta',
explore: '探索',
apps: '工作室',
appDetail: '應用詳情',
account: '我的帳戶',
plugins: '外掛',
pluginsTips: '整合第三方外掛或建立與 ChatGPT 相容的 AI 外掛。',
datasets: '知識庫',
@@ -172,8 +174,8 @@ const translation = {
newPassword: '新密碼',
notEqual: '兩個密碼不相同',
confirmPassword: '確認密碼',
langGeniusAccount: 'Dify 賬號',
langGeniusAccountTip: '您的 Dify 賬號和相關的使用者資料。',
langGeniusAccount: '賬號数据',
langGeniusAccountTip: '您的賬號和相關的使用者資料。',
editName: '編輯名字',
showAppLength: '顯示 {{length}} 個應用',
delete: '刪除帳戶',
@@ -181,7 +183,7 @@ const translation = {
deleteConfirmTip: '請將以下內容從您的註冊電子郵件發送至 ',
account: '帳戶',
myAccount: '我的帳戶',
studio: 'Dify 工作室',
studio: '工作室',
deletePrivacyLinkTip: '有關我們如何處理您的數據的更多資訊,請參閱我們的',
deletePrivacyLink: '隱私策略。',
deleteSuccessTip: '您的帳戶需要時間才能完成刪除。完成後,我們會給您發送電子郵件。',

View File

@@ -16,7 +16,7 @@ const translation = {
},
},
apps: {
title: '探索 Dify 的應用',
title: '探索應用',
description: '使用這些模板應用程式,或根據模板自定義您自己的應用程式。',
allCategories: '推薦',
},

View File

@@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "0.15.3",
"version": "0.15.4",
"private": true,
"engines": {
"node": ">=18.17.0"
@@ -51,6 +51,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.7",
"decimal.js": "^10.4.3",
"dompurify": "^3.2.4",
"echarts": "^5.5.1",
"echarts-for-react": "^3.0.2",
"elkjs": "^0.9.3",
@@ -70,7 +71,7 @@
"mermaid": "11.4.1",
"mime": "^4.0.4",
"negotiator": "^0.6.3",
"next": "^14.2.10",
"next": "^14.2.25",
"pinyin-pro": "^3.23.0",
"qrcode.react": "^3.1.0",
"qs": "^6.11.1",

View File

@@ -40,7 +40,7 @@ import type { SystemFeatures } from '@/types/feature'
type LoginSuccess = {
result: 'success'
data: { access_token: string;refresh_token: string }
data: { access_token: string; refresh_token: string }
}
type LoginFail = {
result: 'fail'
@@ -331,20 +331,20 @@ export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => {
export const sendEMailLoginCode = (email: string, language = 'en-US') =>
post<CommonResponse & { data: string }>('/email-code-login', { body: { email, language } })
export const emailLoginWithCode = (data: { email: string;code: string;token: string }) =>
export const emailLoginWithCode = (data: { email: string; code: string; token: string }) =>
post<LoginResponse>('/email-code-login/validity', { body: data })
export const sendResetPasswordCode = (email: string, language = 'en-US') =>
post<CommonResponse & { data: string;message?: string ;code?: string }>('/forgot-password', { body: { email, language } })
post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } })
export const verifyResetPasswordCode = (body: { email: string;code: string;token: string }) =>
export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean }>('/forgot-password/validity', { body })
export const sendDeleteAccountCode = () =>
get<CommonResponse & { data: string }>('/account/delete/verify')
export const verifyDeleteAccountCode = (body: { code: string;token: string }) =>
export const verifyDeleteAccountCode = (body: { code: string; token: string }) =>
post<CommonResponse & { is_valid: boolean }>('/account/delete', { body })
export const submitDeleteAccountFeedback = (body: { feedback: string;email: string }) =>
export const submitDeleteAccountFeedback = (body: { feedback: string; email: string }) =>
post<CommonResponse>('/account/delete/feedback', { body })

View File

@@ -31,6 +31,13 @@ export type SystemFeatures = {
is_allow_register: boolean
is_email_setup: boolean
license: License
branding: {
enabled: boolean
login_page_logo: string
workspace_logo: string
favicon: string
application_title: string
}
}
export const defaultSystemFeatures: SystemFeatures = {
@@ -49,4 +56,11 @@ export const defaultSystemFeatures: SystemFeatures = {
status: LicenseStatus.NONE,
expired_at: '',
},
branding: {
enabled: false,
login_page_logo: '',
workspace_logo: '',
favicon: '',
application_title: 'test title',
},
}

View File

@@ -2066,10 +2066,10 @@
dependencies:
"@monaco-editor/loader" "^1.4.0"
"@next/env@14.2.17":
version "14.2.17"
resolved "https://registry.npmjs.org/@next/env/-/env-14.2.17.tgz"
integrity sha512-MCgO7VHxXo8sYR/0z+sk9fGyJJU636JyRmkjc7ZJY8Hurl8df35qG5hoAh5KMs75FLjhlEo9bb2LGe89Y/scDA==
"@next/env@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.25.tgz#936d10b967e103e49a4bcea1e97292d5605278dd"
integrity sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w==
"@next/eslint-plugin-next@14.0.4":
version "14.0.4"
@@ -2085,50 +2085,50 @@
dependencies:
source-map "^0.7.0"
"@next/swc-darwin-arm64@14.2.17":
version "14.2.17"
resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.17.tgz"
integrity sha512-WiOf5nElPknrhRMTipXYTJcUz7+8IAjOYw3vXzj3BYRcVY0hRHKWgTgQ5439EvzQyHEko77XK+yN9x9OJ0oOog==
"@next/swc-darwin-arm64@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.25.tgz#7bcccfda0c0ff045c45fbe34c491b7368e373e3d"
integrity sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ==
"@next/swc-darwin-x64@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.17.tgz#e29a17ef28d97c347c7d021f391e13b6c8e4c813"
integrity sha512-29y425wYnL17cvtxrDQWC3CkXe/oRrdt8ie61S03VrpwpPRI0XsnTvtKO06XCisK4alaMnZlf8riwZIbJTaSHQ==
"@next/swc-darwin-x64@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.25.tgz#b489e209d7b405260b73f69a38186ed150fb7a08"
integrity sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug==
"@next/swc-linux-arm64-gnu@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.17.tgz#10e99c7aa60cc33f8b7633e045f74be9a43e7b0c"
integrity sha512-SSHLZls3ZwNEHsc+d0ynKS+7Af0Nr8+KTUBAy9pm6xz9SHkJ/TeuEg6W3cbbcMSh6j4ITvrjv3Oi8n27VR+IPw==
"@next/swc-linux-arm64-gnu@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.25.tgz#ba064fabfdce0190d9859493d8232fffa84ef2e2"
integrity sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg==
"@next/swc-linux-arm64-musl@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.17.tgz#9a5bb809d3c6aef96c409959aedae28b4e5db53d"
integrity sha512-VFge37us5LNPatB4F7iYeuGs9Dprqe4ZkW7lOEJM91r+Wf8EIdViWHLpIwfdDXinvCdLl6b4VyLpEBwpkctJHA==
"@next/swc-linux-arm64-musl@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.25.tgz#bf0018267e4e0fbfa1524750321f8cae855144a3"
integrity sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw==
"@next/swc-linux-x64-gnu@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.17.tgz#64e0ce01870e6dc45ae48f676d7cce82aedcdc62"
integrity sha512-aaQlpxUVb9RZ41adlTYVQ3xvYEfBPUC8+6rDgmQ/0l7SvK8S1YNJzPmDPX6a4t0jLtIoNk7j+nroS/pB4nx7vQ==
"@next/swc-linux-x64-gnu@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.25.tgz#64f5a6016a7148297ee80542e0fd788418a32472"
integrity sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w==
"@next/swc-linux-x64-musl@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.17.tgz#93114164b6ccfc533908193ab9065f0c3970abc3"
integrity sha512-HSyEiFaEY3ay5iATDqEup5WAfrhMATNJm8dYx3ZxL+e9eKv10XKZCwtZByDoLST7CyBmyDz+OFJL1wigyXeaoA==
"@next/swc-linux-x64-musl@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.25.tgz#58dc636d7c55828478159546f7b95ab1e902301c"
integrity sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w==
"@next/swc-win32-arm64-msvc@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.17.tgz#4b99dea02178c112e5c33c742f9ff2a49b3b2939"
integrity sha512-h5qM9Btqv87eYH8ArrnLoAHLyi79oPTP2vlGNSg4CDvUiXgi7l0+5KuEGp5pJoMhjuv9ChRdm7mRlUUACeBt4w==
"@next/swc-win32-arm64-msvc@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.25.tgz#93562d447c799bded1e89c1a62d5195a2a8c6c0d"
integrity sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ==
"@next/swc-win32-ia32-msvc@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.17.tgz#f1c23955405a259b6d45c65f918575b01bcf0106"
integrity sha512-BD/G++GKSLexQjdyoEUgyo5nClU7er5rK0sE+HlEqnldJSm96CIr/+YOTT063LVTT/dUOeQsNgp5DXr86/K7/A==
"@next/swc-win32-ia32-msvc@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.25.tgz#ad85a33466be1f41d083211ea21adc0d2c6e6554"
integrity sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw==
"@next/swc-win32-x64-msvc@14.2.17":
version "14.2.17"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.17.tgz#44f5a4fcd8df1396a8d4326510ca2d92fb809cb3"
integrity sha512-vkQfN1+4V4KqDibkW2q0sJ6CxQuXq5l2ma3z0BRcfIqkAMZiiW67T9yCpwqJKP68QghBtPEFjPAlaqe38O6frw==
"@next/swc-win32-x64-msvc@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.25.tgz#3969c66609e683ec63a6a9f320a855f7be686a08"
integrity sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -5864,6 +5864,13 @@ dompurify@^3.2.1:
optionalDependencies:
"@types/trusted-types" "^2.0.7"
dompurify@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz"
@@ -9911,12 +9918,12 @@ neo-async@^2.6.2:
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next@^14.2.10:
version "14.2.17"
resolved "https://registry.npmjs.org/next/-/next-14.2.17.tgz"
integrity sha512-hNo/Zy701DDO3nzKkPmsLRlDfNCtb1OJxFUvjGEl04u7SFa3zwC6hqsOUzMajcaEOEV8ey1GjvByvrg0Qr5AiQ==
next@^14.2.25:
version "14.2.25"
resolved "https://registry.yarnpkg.com/next/-/next-14.2.25.tgz#0657551fde6a97f697cf9870e9ccbdaa465c6008"
integrity sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q==
dependencies:
"@next/env" "14.2.17"
"@next/env" "14.2.25"
"@swc/helpers" "0.5.5"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
@@ -9924,15 +9931,15 @@ next@^14.2.10:
postcss "8.4.31"
styled-jsx "5.1.1"
optionalDependencies:
"@next/swc-darwin-arm64" "14.2.17"
"@next/swc-darwin-x64" "14.2.17"
"@next/swc-linux-arm64-gnu" "14.2.17"
"@next/swc-linux-arm64-musl" "14.2.17"
"@next/swc-linux-x64-gnu" "14.2.17"
"@next/swc-linux-x64-musl" "14.2.17"
"@next/swc-win32-arm64-msvc" "14.2.17"
"@next/swc-win32-ia32-msvc" "14.2.17"
"@next/swc-win32-x64-msvc" "14.2.17"
"@next/swc-darwin-arm64" "14.2.25"
"@next/swc-darwin-x64" "14.2.25"
"@next/swc-linux-arm64-gnu" "14.2.25"
"@next/swc-linux-arm64-musl" "14.2.25"
"@next/swc-linux-x64-gnu" "14.2.25"
"@next/swc-linux-x64-musl" "14.2.25"
"@next/swc-win32-arm64-msvc" "14.2.25"
"@next/swc-win32-ia32-msvc" "14.2.25"
"@next/swc-win32-x64-msvc" "14.2.25"
no-case@^3.0.4:
version "3.0.4"