Compare commits

...

49 Commits

Author SHA1 Message Date
NFish
1b280d30e5 Merge branch 'e-0154' into deploy/enterprise 2025-03-21 11:01:08 +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
71 changed files with 1023 additions and 309 deletions

View File

@ -5,6 +5,7 @@ on:
branches: branches:
- "main" - "main"
- "deploy/dev" - "deploy/dev"
- "deploy/enterprise"
release: release:
types: [published] 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

@ -39,6 +39,17 @@ def only_edition_cloud(view):
return decorated 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): def only_edition_self_hosted(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):

View File

@ -36,6 +36,14 @@ class LicenseModel(BaseModel):
expired_at: str = "" expired_at: str = ""
class BrandingModel(BaseModel):
enabled: bool = False
application_title: str = ""
login_page_logo: str = ""
workspace_logo: str = ""
favicon: str = ""
class FeatureModel(BaseModel): class FeatureModel(BaseModel):
billing: BillingModel = BillingModel() billing: BillingModel = BillingModel()
members: LimitationModel = LimitationModel(size=0, limit=1) members: LimitationModel = LimitationModel(size=0, limit=1)
@ -47,6 +55,7 @@ class FeatureModel(BaseModel):
can_replace_logo: bool = False can_replace_logo: bool = False
model_load_balancing_enabled: bool = False model_load_balancing_enabled: bool = False
dataset_operator_enabled: bool = False dataset_operator_enabled: bool = False
webapp_copyright_enabled: bool = False
# pydantic configs # pydantic configs
model_config = ConfigDict(protected_namespaces=()) model_config = ConfigDict(protected_namespaces=())
@ -65,6 +74,7 @@ class SystemFeatureModel(BaseModel):
is_allow_create_workspace: bool = False is_allow_create_workspace: bool = False
is_email_setup: bool = False is_email_setup: bool = False
license: LicenseModel = LicenseModel() license: LicenseModel = LicenseModel()
branding: BrandingModel = BrandingModel()
class FeatureService: class FeatureService:
@ -77,6 +87,9 @@ class FeatureService:
if dify_config.BILLING_ENABLED and tenant_id: if dify_config.BILLING_ENABLED and tenant_id:
cls._fulfill_params_from_billing_api(features, tenant_id) cls._fulfill_params_from_billing_api(features, tenant_id)
if dify_config.ENTERPRISE_ENABLED:
features.webapp_copyright_enabled = True
return features return features
@classmethod @classmethod
@ -87,7 +100,7 @@ class FeatureService:
if dify_config.ENTERPRISE_ENABLED: if dify_config.ENTERPRISE_ENABLED:
system_features.enable_web_sso_switch_component = True system_features.enable_web_sso_switch_component = True
system_features.branding.enabled = True
cls._fulfill_params_from_enterprise(system_features) cls._fulfill_params_from_enterprise(system_features)
return system_features return system_features
@ -115,6 +128,9 @@ class FeatureService:
features.billing.subscription.plan = billing_info["subscription"]["plan"] features.billing.subscription.plan = billing_info["subscription"]["plan"]
features.billing.subscription.interval = billing_info["subscription"]["interval"] features.billing.subscription.interval = billing_info["subscription"]["interval"]
if features.billing.subscription.plan != "sandbox":
features.webapp_copyright_enabled = True
if "members" in billing_info: if "members" in billing_info:
features.members.size = billing_info["members"]["size"] features.members.size = billing_info["members"]["size"]
features.members.limit = billing_info["members"]["limit"] features.members.limit = billing_info["members"]["limit"]
@ -148,35 +164,41 @@ class FeatureService:
def _fulfill_params_from_enterprise(cls, features): def _fulfill_params_from_enterprise(cls, features):
enterprise_info = EnterpriseService.get_info() enterprise_info = EnterpriseService.get_info()
if "sso_enforced_for_signin" in enterprise_info: if "SSOEnforcedForSignin" in enterprise_info:
features.sso_enforced_for_signin = enterprise_info["sso_enforced_for_signin"] features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
if "sso_enforced_for_signin_protocol" in enterprise_info: if "SSOEnforcedForSigninProtocol" in enterprise_info:
features.sso_enforced_for_signin_protocol = enterprise_info["sso_enforced_for_signin_protocol"] features.sso_enforced_for_signin_protocol = enterprise_info["SSOEnforcedForSigninProtocol"]
if "sso_enforced_for_web" in enterprise_info: if "SSOEnforcedForWeb" in enterprise_info:
features.sso_enforced_for_web = enterprise_info["sso_enforced_for_web"] features.sso_enforced_for_web = enterprise_info["SSOEnforcedForWeb"]
if "sso_enforced_for_web_protocol" in enterprise_info: if "SSOEnforcedForWebProtocol" in enterprise_info:
features.sso_enforced_for_web_protocol = enterprise_info["sso_enforced_for_web_protocol"] features.sso_enforced_for_web_protocol = enterprise_info["SSOEnforcedForWebProtocol"]
if "enable_email_code_login" in enterprise_info: if "EnableEmailCodeLogin" in enterprise_info:
features.enable_email_code_login = enterprise_info["enable_email_code_login"] features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
if "enable_email_password_login" in enterprise_info: if "EnableEmailPasswordLogin" in enterprise_info:
features.enable_email_password_login = enterprise_info["enable_email_password_login"] features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
if "is_allow_register" in enterprise_info: if "IsAllowRegister" in enterprise_info:
features.is_allow_register = enterprise_info["is_allow_register"] features.is_allow_register = enterprise_info["IsAllowRegister"]
if "is_allow_create_workspace" in enterprise_info: if "IsAllowCreateWorkspace" in enterprise_info:
features.is_allow_create_workspace = enterprise_info["is_allow_create_workspace"] features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"]
if "license" in enterprise_info: if "Branding" in enterprise_info:
license_info = enterprise_info["license"] 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: if "status" in license_info:
features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE)) features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE))
if "expired_at" in license_info: if "expired_at" in license_info:
features.license.expired_at = license_info["expired_at"] 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 flask import render_template
from extensions.ext_mail import mail from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail") @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 # send email code login mail using different languages
try: try:
if language == "zh-Hans": 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) mail.send(to=to, subject="邮箱验证码", html=html_content)
else: 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) mail.send(to=to, subject="Email Code", html=html_content)
end_at = time.perf_counter() end_at = time.perf_counter()

View File

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

View File

@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
from flask import render_template from flask import render_template
from extensions.ext_mail import mail from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail") @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 # send reset password mail using different languages
try: try:
if language == "zh-Hans": if language == "zh-Hans":
html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code) template = "reset_password_mail_template_zh-CN.html"
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content) 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: else:
html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code) template = "reset_password_mail_template_en-US.html"
mail.send(to=to, subject="Set Your Dify Password", html=html_content) 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() end_at = time.perf_counter()
logging.info( 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

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,20 @@
'use client' 'use client'
import { useContextSelector } from 'use-context-selector'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiDiscordFill, RiGithubFill } from '@remixicon/react' import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
import Link from 'next/link' import Link from 'next/link'
import style from '../list.module.css' import style from '../list.module.css'
import Apps from './Apps' import Apps from './Apps'
import AppContext from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { LicenseStatus } from '@/types/feature' import useDocumentTitle from '@/hooks/use-document-title'
const AppList = () => { const AppList = () => {
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle(t('common.menus.apps'))
return ( return (
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'> <div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
<Apps /> <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> <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> <p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='flex items-center gap-2 mt-3'> <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 { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetailLayoutProps = { export type IAppDetailLayoutProps = {
children: React.ReactNode children: React.ReactNode
@ -186,11 +187,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
} }
return baseNavigation return baseNavigation
}, [datasetRes?.provider, datasetId, t]) }, [datasetRes?.provider, datasetId, t])
useDocumentTitle(`${datasetRes?.name || 'Dataset'}`)
useEffect(() => {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand) 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 { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context' import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Container = () => { const Container = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter() const router = useRouter()
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext() const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal) const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
@ -123,7 +125,7 @@ const Container = () => {
{activeTab === 'dataset' && ( {activeTab === 'dataset' && (
<> <>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} /> <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
<DatasetFooter /> {!systemFeatures.branding.enabled && <DatasetFooter />}
{showTagManagementModal && ( {showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} /> <TagManagementModal type='knowledge' show={showTagManagementModal} />
)} )}

View File

@ -3,7 +3,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import NewDatasetCard from './NewDatasetCard' import NewDatasetCard from './NewDatasetCard'
import DatasetCard from './DatasetCard' import DatasetCard from './DatasetCard'
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets' import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
@ -57,11 +56,8 @@ const Datasets = ({
const loadingStateRef = useRef(false) const loadingStateRef = useRef(false)
const anchorRef = useRef<HTMLAnchorElement>(null) const anchorRef = useRef<HTMLAnchorElement>(null)
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
loadingStateRef.current = isLoading loadingStateRef.current = isLoading
document.title = `${t('dataset.knowledge')} - Dify`
}, [isLoading]) }, [isLoading])
useEffect(() => { useEffect(() => {
@ -80,7 +76,7 @@ const Datasets = ({
return ( 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'> <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 => ( {data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />), <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 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 /> return <Container />
} }
export const metadata = {
title: 'Datasets - Dify',
}
export default AppList export default AppList

View File

@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div> <div>
### Authentication ### 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. 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> <div>
### 鉴权 ### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。 Service API 使用 `API-Key` 进行鉴权。
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `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 React from 'react'
import { useTranslation } from 'react-i18next'
import ExploreClient from '@/app/components/explore' import ExploreClient from '@/app/components/explore'
export type IAppDetail = { import useDocumentTitle from '@/hooks/use-document-title'
children: React.ReactNode
}
const AppDetail: FC<IAppDetail> = ({ children }) => { const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.explore'))
return ( return (
<ExploreClient> <ExploreClient>
{children} {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 export default Layout

View File

@ -1,22 +1,16 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ToolProviderList from '@/app/components/tools/provider-list' import ToolProviderList from '@/app/components/tools/provider-list'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'
const Layout: FC = () => { const ToolsList: FC = () => {
const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useEffect(() => { useDocumentTitle(t('common.menus.tools'))
if (typeof window !== 'undefined')
document.title = `${t('tools.title')} - Dify`
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator, router, t])
useEffect(() => { useEffect(() => {
if (isCurrentWorkspaceDatasetOperator) if (isCurrentWorkspaceDatasetOperator)
@ -25,4 +19,4 @@ const Layout: FC = () => {
return <ToolProviderList /> 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 AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { useGlobalPublicStore } from '@/context/global-public-context'
const titleClassName = ` const titleClassName = `
system-sm-semibold text-text-secondary system-sm-semibold text-text-secondary
@ -28,7 +29,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function AccountPage() { export default function AccountPage() {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useAppContext() const { systemFeatures } = useGlobalPublicStore()
const { mutateUserProfile, userProfile, apps } = useAppContext() const { mutateUserProfile, userProfile, apps } = useAppContext()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false) 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> <h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
</div> </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'> <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'> <div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p> <p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</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 Button from '../components/base/button'
import Avatar from './avatar' import Avatar from './avatar'
import LogoSite from '@/app/components/base/logo/logo-site' import LogoSite from '@/app/components/base/logo/logo-site'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Header = () => { const Header = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter() const router = useRouter()
const back = () => { const back = () => {
@ -25,7 +27,7 @@ const Header = () => {
<div className='flex items-center flex-shrink-0 gap-3'> <div className='flex items-center flex-shrink-0 gap-3'>
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}> <Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
<RiRobot2Line className='w-4 h-4' /> <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' /> <RiArrowRightUpLine className='w-4 h-4' />
</Button> </Button>
<div className='w-[1px] h-4 bg-divider-regular' /> <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 export default Layout

View File

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

View File

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

View File

@ -1,10 +1,13 @@
'use client'
import React from 'react' import React from 'react'
import Header from '../signin/_header' import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import ActivateForm from './activateForm' import ActivateForm from './activateForm'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Activate = () => { const Activate = () => {
const { systemFeatures } = useGlobalPublicStore()
return ( return (
<div className={cn( <div className={cn(
style.background, style.background,
@ -21,9 +24,9 @@ const Activate = () => {
}> }>
<Header /> <Header />
<ActivateForm /> <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. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
) )

View File

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

View File

@ -11,10 +11,12 @@ import { useLocalStorageState } from 'ahooks'
import produce from 'immer' import produce from 'immer'
import type { import type {
ChatConfig, ChatConfig,
ChatItem,
Feedback, Feedback,
} from '../types' } from '../types'
import { CONVERSATION_ID_INFO } from '../constants' import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils' import { buildChatItemTree, getProcessedInputsFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import { import {
fetchAppInfo, fetchAppInfo,
fetchAppMeta, fetchAppMeta,
@ -32,6 +34,33 @@ import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config' import { changeLanguage } from '@/i18n/i18next-config'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' 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 = () => { export const useEmbeddedChatbot = () => {
const isInstalledApp = false const isInstalledApp = false
@ -77,7 +106,7 @@ export const useEmbeddedChatbot = () => {
const appPrevChatList = useMemo( const appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length) () => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data) ? buildChatItemTree(getFormattedChatList(appChatListData.data))
: [], : [],
[appChatListData, currentConversationId], [appChatListData, currentConversationId],
) )

View File

@ -2,6 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context' import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type LogoSiteProps = { type LogoSiteProps = {
className?: string className?: string
@ -10,13 +11,17 @@ type LogoSiteProps = {
const LogoSite: FC<LogoSiteProps> = ({ const LogoSite: FC<LogoSiteProps> = ({
className, className,
}) => { }) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useSelector((s) => { const { theme } = useSelector((s) => {
return { return {
theme: s.theme, 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 ( return (
<img <img
src={src} src={src}

View File

@ -67,6 +67,7 @@ export type CurrentPlanInfoBackend = {
can_replace_logo: boolean can_replace_logo: boolean
model_load_balancing_enabled: boolean model_load_balancing_enabled: boolean
dataset_operator_enabled: boolean dataset_operator_enabled: boolean
webapp_copyright_enabled: boolean
} }
export type SubscriptionItem = { 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> <i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import { useModalContext } from '@/context/modal-context'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IAppSelector = { export type IAppSelector = {
isMobile: boolean isMobile: boolean
@ -32,6 +33,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
` `
const router = useRouter() const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false) const [aboutVisible, setAboutVisible] = useState(false)
const { systemFeatures } = useGlobalPublicStore()
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const { t } = useTranslation() const { t } = useTranslation()
@ -122,78 +124,80 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<div>{t('common.userProfile.settings')}</div> <div>{t('common.userProfile.settings')}</div>
</div>} </div>}
</Menu.Item> </Menu.Item>
{canEmailSupport && <Menu.Item> {!systemFeatures.branding.enabled && <>
{({ active }) => <a {canEmailSupport && <Menu.Item>
className={classNames(itemClassName, 'group justify-between', {({ active }) => <a
active && 'bg-state-base-hover', className={classNames(itemClassName, 'group justify-between',
)}
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', active && 'bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}> )}
<div>{t('common.userProfile.about')}</div> href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
<div className='flex items-center'> target='_blank' rel='noopener noreferrer'>
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div> <div>{t('common.userProfile.emailSupport')}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} /> <ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</div> </a>}
</div>} </Menu.Item>}
</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> </div>
<Menu.Item> <Menu.Item>
{({ active }) => <div className='p-1' onClick={() => handleLogout()}> {({ 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 UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { NUM_INFINITE } from '@/app/components/billing/config' import { NUM_INFINITE } from '@/app/components/billing/config'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import { useGlobalPublicStore } from '@/context/global-public-context'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const MembersPage = () => { const MembersPage = () => {
@ -34,7 +35,8 @@ const MembersPage = () => {
} }
const { locale } = useContext(I18n) 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 { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
const [inviteModalVisible, setInviteModalVisible] = useState(false) const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([]) const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])

View File

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

View File

@ -5,14 +5,15 @@ import { LicenseStatus } from '@/types/feature'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector' import { useContextSelector } from 'use-context-selector'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useGlobalPublicStore } from '@/context/global-public-context'
const LicenseNav = () => { const LicenseNav = () => {
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) { if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
const expiredAt = systemFeatures.license?.expired_at 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'> 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', { count })}</span>}
{count > 1 && <span>{t('common.license.expiring_plural', { 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 ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/tools/add-tool-modal/empty' import Empty from '@/app/components/tools/add-tool-modal/empty'
import { fetchCollectionList } from '@/service/tools' import { fetchCollectionList } from '@/service/tools'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ProviderList = () => { const ProviderList = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const [activeTab, setActiveTab] = useTabSearchParams({ const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'builtin', 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', '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', 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} />} {activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />}
{filteredCollectionList.map(collection => ( {filteredCollectionList.map(collection => (
<ProviderCard <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='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='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='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='-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> </div>
<div className='mb-3 px-[14px] text-[15px] leading-5 font-semibold'> <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 style from '../signin/page.module.css'
import ForgotPasswordForm from './ForgotPasswordForm' import ForgotPasswordForm from './ForgotPasswordForm'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm' import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ForgotPassword = () => { const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams() const searchParams = useSearchParams()
const token = searchParams.get('token') const token = searchParams.get('token')
const { systemFeatures } = useGlobalPublicStore()
return ( return (
<div className={classNames( <div className={classNames(
@ -27,9 +31,9 @@ const ForgotPassword = () => {
}> }>
<Header /> <Header />
{token ? <ChangePasswordForm /> : <ForgotPasswordForm />} {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. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
) )

View File

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

View File

@ -1,10 +1,13 @@
'use client'
import React from 'react' import React from 'react'
import Header from '../signin/_header' import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import InstallForm from './installForm' import InstallForm from './installForm'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Install = () => { const Install = () => {
const { systemFeatures } = useGlobalPublicStore()
return ( return (
<div className={classNames( <div className={classNames(
style.background, style.background,
@ -21,9 +24,9 @@ const Install = () => {
}> }>
<Header /> <Header />
<InstallForm /> <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. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</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 I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor' import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor' import SentryInitor from './components/sentry-initor'
@ -6,10 +6,7 @@ import { getLocaleOnServer } from '@/i18n/server'
import { TanstackQueryIniter } from '@/context/query-client' import { TanstackQueryIniter } from '@/context/query-client'
import './styles/globals.css' import './styles/globals.css'
import './styles/markdown.scss' import './styles/markdown.scss'
import GlobalPublicStoreProvider from '@/context/global-public-context'
export const metadata = {
title: 'Dify',
}
export const viewport: Viewport = { export const viewport: Viewport = {
width: 'device-width', width: 'device-width',
@ -18,6 +15,10 @@ export const viewport: Viewport = {
viewportFit: 'cover', viewportFit: 'cover',
userScalable: false, userScalable: false,
} }
export const metadata: Metadata = {
title: ' ',
icons: 'data:',
}
const LocaleLayout = ({ const LocaleLayout = ({
children, children,
@ -50,7 +51,11 @@ const LocaleLayout = ({
<BrowserInitor> <BrowserInitor>
<SentryInitor> <SentryInitor>
<TanstackQueryIniter> <TanstackQueryIniter>
<I18nServer>{children}</I18nServer> <I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryIniter> </TanstackQueryIniter>
</SentryInitor> </SentryInitor>
</BrowserInitor> </BrowserInitor>

View File

@ -1,9 +1,12 @@
'use client'
import Header from '../signin/_header' import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import cn from '@/utils/classnames' 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 <> return <>
<div className={cn( <div className={cn(
style.background, style.background,
@ -30,9 +33,9 @@ export default async function SignInLayout({ children }: any) {
{children} {children}
</div> </div>
</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. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</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 Toast from '@/app/components/base/toast'
import { sendResetPasswordCode } from '@/service/common' import { sendResetPasswordCode } from '@/service/common'
import I18NContext from '@/context/i18n' import I18NContext from '@/context/i18n'
import useDocumentTitle from '@/hooks/use-document-title'
export default function CheckCode() { export default function CheckCode() {
const { t } = useTranslation() const { t } = useTranslation()
useDocumentTitle('')
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const [email, setEmail] = useState('') 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' 'use client'
import React from 'react' import React from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import LoginLogo from './LoginLogo'
import Select from '@/app/components/base/select/locale' import Select from '@/app/components/base/select/locale'
import { languages } from '@/i18n/language' import { languages } from '@/i18n/language'
import { type Locale } from '@/i18n' import { type Locale } from '@/i18n'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import LogoSite from '@/app/components/base/logo/logo-site'
const Header = () => { const Header = () => {
const { locale, setLocaleOnClient } = useContext(I18n) const { locale, setLocaleOnClient } = useContext(I18n)
return <div className='flex items-center justify-between p-6 w-full'> return <div className='flex items-center justify-between p-6 w-full'>
<LogoSite /> <LoginLogo />
<Select <Select
value={locale} value={locale}
items={languages.filter(item => item.supported)} items={languages.filter(item => item.supported)}

View File

@ -1,9 +1,14 @@
'use client'
import Header from './_header' import Header from './_header'
import style from './page.module.css' import style from './page.module.css'
import cn from '@/utils/classnames' 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 <> return <>
<div className={cn( <div className={cn(
style.background, style.background,
@ -30,9 +35,9 @@ export default async function SignInLayout({ children }: any) {
{children} {children}
</div> </div>
</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. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</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 SocialAuth from './components/social-auth'
import SSOAuth from './components/sso-auth' import SSOAuth from './components/sso-auth'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { getSystemFeatures, invitationCheck } from '@/service/common' import { invitationCheck } from '@/service/common'
import { LicenseStatus, defaultSystemFeatures } from '@/types/feature' import { LicenseStatus } from '@/types/feature'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NormalForm = () => { const NormalForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -23,7 +24,7 @@ const NormalForm = () => {
const message = decodeURIComponent(searchParams.get('message') || '') const message = decodeURIComponent(searchParams.get('message') || '')
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures) const { systemFeatures } = useGlobalPublicStore()
const [authType, updateAuthType] = useState<'code' | 'password'>('password') const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false) const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
@ -46,12 +47,9 @@ const NormalForm = () => {
message, message,
}) })
} }
const features = await getSystemFeatures() setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
const allFeatures = { ...defaultSystemFeatures, ...features } setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
setSystemFeatures(allFeatures) updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
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')
if (isInviteLink) { if (isInviteLink) {
const checkRes = await invitationCheck({ const checkRes = await invitationCheck({
url: '/activate/check', url: '/activate/check',
@ -65,10 +63,9 @@ const NormalForm = () => {
catch (error) { catch (error) {
console.error(error) console.error(error)
setAllMethodsAreDisabled(true) setAllMethodsAreDisabled(true)
setSystemFeatures(defaultSystemFeatures)
} }
finally { setIsLoading(false) } finally { setIsLoading(false) }
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink]) }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, systemFeatures])
useEffect(() => { useEffect(() => {
init() init()
}, [init]) }, [init])
@ -83,7 +80,7 @@ const NormalForm = () => {
<Loading type='area' /> <Loading type='area' />
</div> </div>
} }
if (systemFeatures.license?.status === LicenseStatus.LOST) { if (systemFeatures.license.status === LicenseStatus.LOST) {
return <div className='w-full mx-auto mt-8'> return <div className='w-full mx-auto mt-8'>
<div className='bg-white'> <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"> <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>
</div> </div>
} }
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { if (systemFeatures.license.status === LicenseStatus.EXPIRED) {
return <div className='w-full mx-auto mt-8'> return <div className='w-full mx-auto mt-8'>
<div className='bg-white'> <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"> <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>
</div> </div>
} }
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { if (systemFeatures.license.status === LicenseStatus.INACTIVE) {
return <div className='w-full mx-auto mt-8'> return <div className='w-full mx-auto mt-8'>
<div className='bg-white'> <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"> <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 {isInviteLink
? <div className="w-full mx-auto"> ? <div className="w-full mx-auto">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.join')}{workspaceName}</h2> <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>
: <div className="w-full mx-auto"> : <div className="w-full mx-auto">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2> <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>}
<div className="bg-white"> <div className="bg-white">
<div className="flex flex-col gap-3 mt-6"> <div className="flex flex-col gap-3 mt-6">
@ -184,29 +181,31 @@ const NormalForm = () => {
</div> </div>
</div> </div>
</>} </>}
<div className="w-full block mt-2 system-xs-regular text-text-tertiary"> {!systemFeatures.branding.enabled && <>
{t('login.tosDesc')} <div className="w-full block mt-2 system-xs-regular text-text-tertiary">
&nbsp; {t('login.tosDesc')}
<Link &nbsp;
className='system-xs-medium text-text-secondary hover:underline' <Link
target='_blank' rel='noopener noreferrer' className='system-xs-medium text-text-secondary hover:underline'
href='https://dify.ai/terms' target='_blank' rel='noopener noreferrer'
>{t('login.tos')}</Link> href='https://dify.ai/terms'
&nbsp;&&nbsp; >{t('login.tos')}</Link>
<Link &nbsp;&&nbsp;
className='system-xs-medium text-text-secondary hover:underline' <Link
target='_blank' rel='noopener noreferrer' className='system-xs-medium text-text-secondary hover:underline'
href='https://dify.ai/privacy' target='_blank' rel='noopener noreferrer'
>{t('login.pp')}</Link> href='https://dify.ai/privacy'
</div> >{t('login.pp')}</Link>
{IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary"> </div>
{t('login.goToInit')} {IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary">
&nbsp; {t('login.goToInit')}
<Link &nbsp;
className='system-xs-medium text-text-secondary hover:underline' <Link
href='/install' className='system-xs-medium text-text-secondary hover:underline'
>{t('login.setAdminAccount')}</Link> href='/install'
</div>} >{t('login.setAdminAccount')}</Link>
</div>}
</>}
</div> </div>
</div> </div>

View File

@ -6,19 +6,16 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import { fetchAppList } from '@/service/apps' import { fetchAppList } from '@/service/apps'
import Loading from '@/app/components/base/loading' 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 type { App } from '@/types/app'
import { Theme } from '@/types/app' import { Theme } from '@/types/app'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import MaintenanceNotice from '@/app/components/header/maintenance-notice' import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
export type AppContextValue = { export type AppContextValue = {
theme: Theme theme: Theme
setTheme: (theme: Theme) => void setTheme: (theme: Theme) => void
apps: App[] apps: App[]
systemFeatures: SystemFeatures
mutateApps: VoidFunction mutateApps: VoidFunction
userProfile: UserProfileResponse userProfile: UserProfileResponse
mutateUserProfile: VoidFunction mutateUserProfile: VoidFunction
@ -57,7 +54,6 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
const AppContext = createContext<AppContextValue>({ const AppContext = createContext<AppContextValue>({
theme: Theme.light, theme: Theme.light,
systemFeatures: defaultSystemFeatures,
setTheme: () => { }, setTheme: () => { },
apps: [], apps: [],
mutateApps: () => { }, mutateApps: () => { },
@ -96,10 +92,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) 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: 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 [userProfile, setUserProfile] = useState<UserProfileResponse>()
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo) const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
@ -146,7 +138,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
theme, theme,
setTheme: handleSetTheme, setTheme: handleSetTheme,
apps: appList.data, apps: appList.data,
systemFeatures: { ...defaultSystemFeatures, ...systemFeatures },
mutateApps, mutateApps,
userProfile, userProfile,
mutateUserProfile, 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 enableReplaceWebAppLogo: boolean
modelLoadBalancingEnabled: boolean modelLoadBalancingEnabled: boolean
datasetOperatorEnabled: boolean datasetOperatorEnabled: boolean
webappCopyrightEnabled: boolean
} }
const ProviderContext = createContext<ProviderContextState>({ const ProviderContext = createContext<ProviderContextState>({
modelProviders: [], modelProviders: [],
@ -64,6 +65,7 @@ const ProviderContext = createContext<ProviderContextState>({
enableReplaceWebAppLogo: false, enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false, modelLoadBalancingEnabled: false,
datasetOperatorEnabled: false, datasetOperatorEnabled: false,
webappCopyrightEnabled: false,
}) })
export const useProviderContext = () => useContext(ProviderContext) export const useProviderContext = () => useContext(ProviderContext)
@ -91,6 +93,7 @@ export const ProviderContextProvider = ({
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false) const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false) const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false)
const fetchPlan = async () => { const fetchPlan = async () => {
const data = await fetchCurrentPlanInfo() const data = await fetchCurrentPlanInfo()
@ -105,6 +108,8 @@ export const ProviderContextProvider = ({
setModelLoadBalancingEnabled(true) setModelLoadBalancingEnabled(true)
if (data.dataset_operator_enabled) if (data.dataset_operator_enabled)
setDatasetOperatorEnabled(true) setDatasetOperatorEnabled(true)
if (data.webapp_copyright_enabled)
setWebappCopyrightEnabled(true)
} }
useEffect(() => { useEffect(() => {
fetchPlan() fetchPlan()
@ -123,6 +128,7 @@ export const ProviderContextProvider = ({
enableReplaceWebAppLogo, enableReplaceWebAppLogo,
modelLoadBalancingEnabled, modelLoadBalancingEnabled,
datasetOperatorEnabled, datasetOperatorEnabled,
webappCopyrightEnabled,
}}> }}>
{children} {children}
</ProviderContext.Provider> </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', status: 'beta',
explore: 'Explore', explore: 'Explore',
apps: 'Studio', apps: 'Studio',
appDetail: 'App Detail',
account: 'Account',
plugins: 'Plugins', plugins: 'Plugins',
pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.', pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.',
datasets: 'Knowledge', datasets: 'Knowledge',
@ -167,7 +169,7 @@ const translation = {
account: { account: {
account: 'Account', account: 'Account',
myAccount: 'My Account', myAccount: 'My Account',
studio: 'Dify Studio', studio: 'Studio',
avatar: 'Avatar', avatar: 'Avatar',
name: 'Name', name: 'Name',
email: 'Email', email: 'Email',
@ -179,8 +181,8 @@ const translation = {
newPassword: 'New password', newPassword: 'New password',
confirmPassword: 'Confirm password', confirmPassword: 'Confirm password',
notEqual: 'Two passwords are different.', notEqual: 'Two passwords are different.',
langGeniusAccount: 'Dify account', langGeniusAccount: 'Account\'s data',
langGeniusAccountTip: 'Your Dify account and associated user data.', langGeniusAccountTip: 'The user data of your account.',
editName: 'Edit Name', editName: 'Edit Name',
showAppLength: 'Show {{length}} apps', showAppLength: 'Show {{length}} apps',
delete: 'Delete Account', delete: 'Delete Account',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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