Compare commits

...

36 Commits

Author SHA1 Message Date
lyzno1
18d7c99ab7 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-17 16:27:50 +08:00
lyzno1
de2a469048 feat: add trigger limit modal (#28257)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
2025-11-17 16:27:24 +08:00
lyzno1
1730572498 feat: enforce sandbox start-node limit by disabling publish and surfacing an upgrade CTA with localized copy 2025-11-15 15:02:49 +08:00
lyzno1
9e763e80e8 feat: add resets time in billing usage info 2025-11-15 13:04:48 +08:00
lyzno1
196bf3d9a0 fix: align without brand template 2025-11-15 11:34:51 +08:00
lyzno1
c78fbabe60 fix: email template design 2025-11-15 11:23:59 +08:00
lyzno1
7666013227 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-15 10:24:16 +08:00
lyzno1
20b1b1bf43 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-14 10:44:35 +08:00
lyzno1
55e536b1e0 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 22:08:11 +08:00
lyzno1
e6bc5a9629 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 19:09:39 +08:00
lyzno1
f021b8248e refactor: usage info bar color 2025-11-13 17:36:38 +08:00
lyzno1
0ac9b308cf chore: rm per month unit in cards 2025-11-13 17:34:56 +08:00
lyzno1
6e5486b556 fix: remove 'Current plan' in billing setting 2025-11-13 17:22:38 +08:00
lyzno1
0f6e058c73 refactor: billing cards icon 2025-11-13 17:20:51 +08:00
lyzno1
4bb225c7ee feat: add tooltip in api rate limit card 2025-11-13 17:16:16 +08:00
lyzno1
79fdc5b07b fix: align translations 2025-11-13 17:03:54 +08:00
lyzno1
e7794be27a fix: billing titlePerMonth translations 2025-11-13 16:39:31 +08:00
lyzno1
ae36958ef4 refactor: pricing modal 2025-11-13 16:37:28 +08:00
lyzno1
86bc2924f3 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:58:43 +08:00
lyzno1
318b9d707b Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:45:44 +08:00
lyzno1
9174597eb8 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:43:27 +08:00
lyzno1
0e689b14a6 fix: align translations 2025-11-13 15:37:27 +08:00
lyzno1
9c09c993ba Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:26:20 +08:00
lyzno1
7a810a4412 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:21:48 +08:00
lyzno1
639a78fc7b Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:18:09 +08:00
lyzno1
dfff9ec00a Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 15:02:48 +08:00
lyzno1
d16d61425f Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 14:42:54 +08:00
lyzno1
3c06b62fc5 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 13:12:49 +08:00
lyzno1
5ffe7f8c0c Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 11:49:26 +08:00
lyzno1
87954a8226 Merge remote-tracking branch 'origin/main' into feat/trigger-saas 2025-11-13 11:37:48 +08:00
lyzno1
6430e014b0 feat: align trigger usage text and add tooltips 2025-11-12 18:26:08 +08:00
lyzno1
494f6b06e1 fix: email template 2025-11-12 18:26:08 +08:00
lyzno1
404240baf9 fix: template translations 2025-11-12 18:26:08 +08:00
lyzno1
8fb027d331 feat: add sandbox api rate limit email template 2025-11-12 18:26:08 +08:00
lyzno1
d3d3868b4a feat: add trigger events usage warning email template 2025-11-12 18:26:08 +08:00
lyzno1
7c5a008f5c feat: add trigger events limit email template 2025-11-12 18:26:08 +08:00
58 changed files with 3800 additions and 125 deletions

View File

@@ -38,6 +38,12 @@ class EmailType(StrEnum):
EMAIL_REGISTER = auto()
EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = auto()
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = auto()
TRIGGER_EVENTS_LIMIT_SANDBOX = auto()
TRIGGER_EVENTS_LIMIT_PROFESSIONAL = auto()
TRIGGER_EVENTS_USAGE_WARNING_SANDBOX = auto()
TRIGGER_EVENTS_USAGE_WARNING_PROFESSIONAL = auto()
API_RATE_LIMIT_LIMIT_SANDBOX = auto()
API_RATE_LIMIT_WARNING_SANDBOX = auto()
class EmailLanguage(StrEnum):
@@ -445,6 +451,78 @@ def create_default_email_config() -> EmailI18nConfig:
branded_template_path="clean_document_job_mail_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_LIMIT_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youve reached your Sandbox Trigger Events limit",
template_path="trigger_events_limit_template_en-US.html",
branded_template_path="without-brand/trigger_events_limit_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 Sandbox 触发事件额度已用尽",
template_path="trigger_events_limit_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_limit_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_LIMIT_PROFESSIONAL: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youve reached your monthly Trigger Events limit",
template_path="trigger_events_limit_template_en-US.html",
branded_template_path="without-brand/trigger_events_limit_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的月度触发事件额度已用尽",
template_path="trigger_events_limit_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_limit_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_USAGE_WARNING_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youre nearing your Sandbox Trigger Events limit",
template_path="trigger_events_usage_warning_template_en-US.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 Sandbox 触发事件额度接近上限",
template_path="trigger_events_usage_warning_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_USAGE_WARNING_PROFESSIONAL: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youre nearing your Monthly Trigger Events limit",
template_path="trigger_events_usage_warning_template_en-US.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的月度触发事件额度接近上限",
template_path="trigger_events_usage_warning_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_zh-CN.html",
),
},
EmailType.API_RATE_LIMIT_LIMIT_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youve reached your API Rate Limit",
template_path="api_rate_limit_limit_template_en-US.html",
branded_template_path="without-brand/api_rate_limit_limit_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 API 速率额度已用尽",
template_path="api_rate_limit_limit_template_zh-CN.html",
branded_template_path="without-brand/api_rate_limit_limit_template_zh-CN.html",
),
},
EmailType.API_RATE_LIMIT_WARNING_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youre nearing your API Rate Limit",
template_path="api_rate_limit_warning_template_en-US.html",
branded_template_path="without-brand/api_rate_limit_warning_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 API 速率额度接近上限",
template_path="api_rate_limit_warning_template_zh-CN.html",
branded_template_path="without-brand/api_rate_limit_warning_template_zh-CN.html",
),
},
EmailType.EMAIL_REGISTER: {
EmailLanguage.EN_US: EmailTemplate(
subject="Register Your {application_title} Account",

View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 434px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">Youve reached your API Rate Limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>Monthly API Rate Limit</strong> for the
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
As a result, API access has been temporarily paused.
</p>
<p class="body-text">
To continue using the Dify API and unlock a higher limit, please upgrade to a paid plan.
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 434px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">您的 API 速率额度已用尽</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>月度 API 速率额度</strong>,触及
<strong>{{planName}} 计划(上限:{{planLimit}}</strong>
</p>
<p class="body-text">
因此API 访问已被暂时暂停。
</p>
<p class="body-text">
若要继续使用 Dify API 并解锁更高额度,请升级到付费套餐。
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">Youre nearing your API Rate Limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used <strong>80% of its Monthly API Rate Limit</strong> for the
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
Once the limit is reached, API access will be temporarily paused until the next monthly reset.
</p>
<p class="body-text">
To avoid service interruptions and ensure continued access to the Dify API, please consider upgrading your plan for a higher API
Rate Limit.
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">您的 API 速率额度接近上限</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>80% 的月度 API 速率额度</strong>,触及
<strong>{{planName}} 计划(上限:{{planLimit}}</strong>
</p>
<p class="body-text">
一旦达到上限API 访问将暂停,直至下一个月度重置。
</p>
<p class="body-text">
为避免服务中断并持续访问 Dify API请考虑升级到额度更高的套餐。
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">Youve reached your trigger events limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>{{usageScope | default('Trigger Events')}}</strong> for the
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
Workflows triggered by <strong>{{triggerSources}}</strong> events have been temporarily paused.
</p>
<p class="body-text">
To keep your workflows running without interruption, please upgrade your plan to unlock more Trigger Events.
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
Trigger Events for the {{planName}} Plan {{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">您的触发事件额度已用尽</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>{{usageScope | default('触发事件额度')}}</strong>,并耗尽
<strong>{{planName}} 计划(上限:{{planLimit}}</strong> 的全部额度。
</p>
<p class="body-text">
<strong>{{triggerSources}}</strong> 触发的工作流已被暂时暂停。
</p>
<p class="body-text">
为保证工作流不中断,请升级套餐以解锁更多触发事件额度。
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
{{planName}} 计划的触发事件额度{{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,185 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">Youre nearing your Trigger Events limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used <strong>{{usagePercent}}</strong> of its
<strong>{{usageScope}}</strong> for the <strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
Once the limit is reached, workflows triggered by <strong>{{triggerSources}}</strong> events will be temporarily
paused.
</p>
<p class="body-text">
{{upgradeHint}}
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
Trigger Events for the {{planName}} Plan {{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-header img {
width: 68px;
height: auto;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<div class="card-content">
<h1 class="title">您的触发事件额度接近上限</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>{{usagePercent}}</strong>
<strong>{{usageScope}}</strong>,触及 <strong>{{planName}} 计划(上限:{{planLimit}}</strong>
</p>
<p class="body-text">
一旦达到上限,由 <strong>{{triggerSources}}</strong> 触发的工作流将被暂时暂停。
</p>
<p class="body-text">
{{upgradeHint}}
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
{{planName}} 计划的触发事件额度{{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 434px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">Youve reached your API Rate Limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>Monthly API Rate Limit</strong> for the
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
As a result, API access has been temporarily paused.
</p>
<p class="body-text">
To continue using the Dify API and unlock a higher limit, please upgrade to a paid plan.
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 434px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">您的 API 速率额度已用尽</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>月度 API 速率额度</strong>,触及
<strong>{{planName}} 计划(上限:{{planLimit}}</strong>
</p>
<p class="body-text">
因此API 访问已被暂时暂停。
</p>
<p class="body-text">
若要继续使用 Dify API 并解锁更高额度,请升级到付费套餐。
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">Youre nearing your API Rate Limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used <strong>80% of its Monthly API Rate Limit</strong> for the
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
Once the limit is reached, API access will be temporarily paused until the next monthly reset.
</p>
<p class="body-text">
To avoid service interruptions and ensure continued access to the Dify API, please consider upgrading your plan for a higher API
Rate Limit.
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">您的 API 速率额度接近上限</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>80% 的月度 API 速率额度</strong>,触及
<strong>{{planName}} 计划(上限:{{planLimit}}</strong>
</p>
<p class="body-text">
一旦达到上限API 访问将暂停,直至下一个月度重置。
</p>
<p class="body-text">
为避免服务中断并持续访问 Dify API请考虑升级到额度更高的套餐。
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">Youve reached your trigger events limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>{{usageScope | default('Trigger Events')}}</strong> for the
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
Workflows triggered by <strong>{{triggerSources}}</strong> events have been temporarily paused.
</p>
<p class="body-text">
To keep your workflows running without interruption, please upgrade your plan to unlock more Trigger Events.
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
Trigger Events for the {{planName}} Plan {{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">您的触发事件额度已用尽</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>{{usageScope | default('触发事件额度')}}</strong>,并耗尽
<strong>{{planName}} 计划(上限:{{planLimit}}</strong> 的全部额度。
</p>
<p class="body-text">
<strong>{{triggerSources}}</strong> 触发的工作流已被暂时暂停。
</p>
<p class="body-text">
为保证工作流不中断,请升级套餐以解锁更多触发事件额度。
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
{{planName}} 计划的触发事件额度{{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: 'Inter', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">Youre nearing your Trigger Events limit</h1>
<div class="body-group">
<p class="body-text">
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
</p>
<p class="body-text">
Your workspace <strong>{{workspaceName}}</strong> has used <strong>{{usagePercent}}</strong> of its
<strong>{{usageScope}}</strong> for the <strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
</p>
<p class="body-text">
Once the limit is reached, workflows triggered by <strong>{{triggerSources}}</strong> events will be temporarily
paused.
</p>
<p class="body-text">
{{upgradeHint}}
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
Trigger Events for the {{planName}} Plan {{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">Best regards,</p>
<p class="signature-text">The Dify Team</p>
</div>
</div>
</div>
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
</body>
</html>

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
body {
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
background-color: #e9ebf0;
margin: 0;
padding: 0;
color: #000000;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
width: 600px;
min-height: 454px;
margin: 40px auto 0;
display: flex;
flex-direction: column;
align-items: flex-start;
background: #fcfcfd;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
border-radius: 16px;
}
.card-header {
display: flex;
padding: 36px 48px 24px 48px;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.card-content {
display: flex;
padding: 8px 48px 48px 48px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
align-self: stretch;
width: 100%;
}
.title {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 120%;
color: #000000;
}
.body-group {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0 0;
}
.body-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.body-text strong {
font-weight: 600;
color: #101828;
}
.cta {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 504px;
height: 32px;
padding: 0 16px;
gap: 8px;
background: #1677ff;
border-radius: 6px;
text-decoration: none;
color: #ffffff !important;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.note {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.signature {
margin-top: 28px;
margin-bottom: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.signature-text {
margin: 0;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.005em;
color: #354052;
}
.email-footer {
margin: 20px auto 40px;
max-width: 600px;
color: #676f83;
text-align: center;
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
flex: 1 0 0;
}
</style>
</head>
<body>
<div class="card">
<div class="card-header">
</div>
<div class="card-content">
<h1 class="title">您的触发事件额度接近上限</h1>
<div class="body-group">
<p class="body-text">
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }}</strong>
</p>
<p class="body-text">
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>{{usagePercent}}</strong>
<strong>{{usageScope}}</strong>,触及 <strong>{{planName}} 计划(上限:{{planLimit}}</strong>
</p>
<p class="body-text">
一旦达到上限,由 <strong>{{triggerSources}}</strong> 触发的工作流将被暂时暂停。
</p>
<p class="body-text">
{{upgradeHint}}
</p>
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
<p class="note">
<strong>
{% if resetLine is defined %}
{{ resetLine }}
{% else %}
{{planName}} 计划的触发事件额度{{resetDescription}}
{% endif %}
</strong>
</p>
</div>
<div class="signature">
<p class="signature-text">此致敬礼,</p>
<p class="signature-text">Dify 团队</p>
</div>
</div>
</div>
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
</body>
</html>

View File

@@ -49,6 +49,7 @@ import { fetchInstalledAppList } from '@/service/explore'
import { AppModeEnum } from '@/types/app'
import type { PublishWorkflowParams } from '@/types/workflow'
import { basePath } from '@/utils/var'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
[AccessMode.ORGANIZATION]: {
@@ -106,6 +107,7 @@ export type AppPublisherProps = {
workflowToolAvailable?: boolean
missingStartNode?: boolean
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist).
startNodeLimitExceeded?: boolean
}
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
@@ -127,6 +129,7 @@ const AppPublisher = ({
workflowToolAvailable = true,
missingStartNode = false,
hasTriggerNode = false,
startNodeLimitExceeded = false,
}: AppPublisherProps) => {
const { t } = useTranslation()
@@ -246,6 +249,13 @@ const AppPublisher = ({
const hasPublishedVersion = !!publishedAt
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined
const showStartNodeLimitHint = Boolean(startNodeLimitExceeded)
const upgradeHighlightStyle = useMemo(() => ({
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}), [])
return (
<>
@@ -304,29 +314,49 @@ const AppPublisher = ({
/>
)
: (
<Button
variant='primary'
className='mt-3 w-full'
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{
published
? t('workflow.common.published')
: (
<div className='flex gap-1'>
<span>{t('workflow.common.publishUpdate')}</span>
<div className='flex gap-0.5'>
{PUBLISH_SHORTCUT.map(key => (
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
{getKeyboardKeyNameBySystem(key)}
</span>
))}
<>
<Button
variant='primary'
className='mt-3 w-full'
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{
published
? t('workflow.common.published')
: (
<div className='flex gap-1'>
<span>{t('workflow.common.publishUpdate')}</span>
<div className='flex gap-0.5'>
{PUBLISH_SHORTCUT.map(key => (
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
{getKeyboardKeyNameBySystem(key)}
</span>
))}
</div>
</div>
</div>
)
}
</Button>
)
}
</Button>
{showStartNodeLimitHint && (
<div className='mt-3 flex flex-col items-stretch'>
<p
className='text-sm font-semibold leading-5 text-transparent'
style={upgradeHighlightStyle}
>
<span className='block'>{t('workflow.publishLimit.startNodeTitlePrefix')}</span>
<span className='block'>{t('workflow.publishLimit.startNodeTitleSuffix')}</span>
</p>
<p className='mt-1 text-xs leading-4 text-text-secondary'>
{t('workflow.publishLimit.startNodeDesc')}
</p>
<UpgradeBtn
isShort
className='mb-[12px] mt-[9px] h-[32px] w-[93px] self-start'
/>
</div>
)}
</>
)
}
</div>

View File

@@ -90,4 +90,8 @@ export const defaultPlan = {
apiRateLimit: ALL_PLANS.sandbox.apiRateLimit,
triggerEvents: ALL_PLANS.sandbox.triggerEvents,
},
reset: {
apiRateLimit: null,
triggerEvents: null,
},
}

View File

@@ -6,15 +6,16 @@ import { useRouter } from 'next/navigation'
import {
RiBook2Line,
RiFileEditLine,
RiFlashlightLine,
RiGraduationCapLine,
RiGroupLine,
RiSpeedLine,
} from '@remixicon/react'
import { Plan, SelfHostedPlan } from '../type'
import { NUM_INFINITE } from '../config'
import { getDaysUntilEndOfMonth } from '@/utils/time'
import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn'
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import Button from '@/app/components/base/button'
@@ -44,9 +45,20 @@ const PlanComp: FC<Props> = ({
const {
usage,
total,
reset,
} = plan
const perMonthUnit = ` ${t('billing.usagePage.perMonth')}`
const triggerEventUnit = plan.type === Plan.sandbox ? undefined : perMonthUnit
const triggerEventsResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
? reset.triggerEvents ?? undefined
: undefined
const apiRateLimitResetInDays = (() => {
if (total.apiRateLimit === NUM_INFINITE)
return undefined
if (typeof reset.apiRateLimit === 'number')
return reset.apiRateLimit
if (type === Plan.sandbox)
return getDaysUntilEndOfMonth()
return undefined
})()
const [showModal, setShowModal] = React.useState(false)
const { mutateAsync } = useEducationVerify()
@@ -79,7 +91,6 @@ const PlanComp: FC<Props> = ({
<div className='grow'>
<div className='mb-1 flex items-center gap-1'>
<div className='system-md-semibold-uppercase text-text-primary'>{t(`billing.plans.${type}.name`)}</div>
<div className='system-2xs-medium-uppercase rounded-[5px] border border-divider-deep px-1 py-0.5 text-text-tertiary'>{t('billing.currentPlan')}</div>
</div>
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
@@ -124,18 +135,20 @@ const PlanComp: FC<Props> = ({
total={total.annotatedResponse}
/>
<UsageInfo
Icon={RiFlashlightLine}
Icon={TriggerAll}
name={t('billing.usagePage.triggerEvents')}
usage={usage.triggerEvents}
total={total.triggerEvents}
unit={triggerEventUnit}
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
resetInDays={triggerEventsResetInDays}
/>
<UsageInfo
Icon={RiSpeedLine}
Icon={ApiAggregate}
name={t('billing.plansCommon.apiRateLimit')}
usage={usage.apiRateLimit}
total={total.apiRateLimit}
unit={perMonthUnit}
tooltip={total.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
resetInDays={apiRateLimitResetInDays}
/>
</div>

View File

@@ -46,16 +46,10 @@ const List = ({
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
/>
<Item
label={
planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}`
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
/>
<Item
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
/>
<Divider bgStyle='gradient' />
<Item
label={
planInfo.triggerEvents === NUM_INFINITE
@@ -64,6 +58,14 @@ const List = ({
? t('billing.plansCommon.triggerEvents.sandbox', { count: planInfo.triggerEvents })
: t('billing.plansCommon.triggerEvents.professional', { count: planInfo.triggerEvents })
}
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
/>
<Item
label={
plan === Plan.sandbox
? t('billing.plansCommon.startNodes.limited', { count: 2 })
: t('billing.plansCommon.startNodes.unlimited')
}
/>
<Item
label={
@@ -73,13 +75,7 @@ const List = ({
? t('billing.plansCommon.workflowExecution.faster')
: t('billing.plansCommon.workflowExecution.priority')
}
/>
<Item
label={
plan === Plan.sandbox
? t('billing.plansCommon.startNodes.limited', { count: 2 })
: t('billing.plansCommon.startNodes.unlimited')
}
tooltip={t('billing.plansCommon.workflowExecution.tooltip') as string}
/>
<Divider bgStyle='gradient' />
<Item
@@ -89,6 +85,14 @@ const List = ({
<Item
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
/>
<Item
label={
planInfo.apiRateLimit === NUM_INFINITE
? t('billing.plansCommon.unlimitedApiRate')
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')} / ${t('billing.plansCommon.month')}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
/>
<Divider bgStyle='gradient' />
<Item
label={t('billing.plansCommon.modelProviders')}

View File

@@ -0,0 +1,30 @@
.surface {
border: 0.5px solid var(--color-components-panel-border, rgba(16, 24, 40, 0.08));
background:
linear-gradient(109deg, var(--color-background-section, #f9fafb) 0%, var(--color-background-section-burn, #f2f4f7) 100%),
var(--color-components-panel-bg, #fff);
}
.heroOverlay {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='54' height='54' fill='none'%3E%3Crect x='1' y='1' width='48' height='48' rx='12' stroke='rgba(16, 24, 40, 0.3)' stroke-width='1' opacity='0.08'/%3E%3C/svg%3E");
background-size: 54px 54px;
background-position: 31px -23px;
background-repeat: repeat;
mask-image: linear-gradient(180deg, rgba(255, 255, 255, 1) 45%, rgba(255, 255, 255, 0) 75%);
-webkit-mask-image: linear-gradient(180deg, rgba(255, 255, 255, 1) 45%, rgba(255, 255, 255, 0) 75%);
}
.icon {
border: 0.5px solid transparent;
background:
linear-gradient(180deg, var(--color-components-avatar-bg-mask-stop-0, rgba(255, 255, 255, 0.12)) 0%, var(--color-components-avatar-bg-mask-stop-100, rgba(255, 255, 255, 0.08)) 100%),
var(--color-util-colors-blue-brand-blue-brand-500, #296dff);
box-shadow: 0 10px 20px color-mix(in srgb, var(--color-util-colors-blue-brand-blue-brand-500, #296dff) 35%, transparent);
}
.highlight {
background: linear-gradient(97deg, var(--color-components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -4%, var(--color-components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}

View File

@@ -0,0 +1,97 @@
import type { Meta, StoryObj } from '@storybook/nextjs'
import React, { useEffect, useState } from 'react'
import i18next from 'i18next'
import { I18nextProvider } from 'react-i18next'
import TriggerEventsLimitModal from '.'
import { Plan } from '../type'
const i18n = i18next.createInstance()
i18n.init({
lng: 'en',
resources: {
en: {
translation: {
billing: {
triggerLimitModal: {
title: 'Upgrade to unlock more trigger events',
description: 'Youve reached the limit of workflow event triggers for this plan.',
dismiss: 'Dismiss',
upgrade: 'Upgrade',
usageTitle: 'TRIGGER EVENTS',
},
usagePage: {
triggerEvents: 'Trigger Events',
resetsIn: 'Resets in {{count, number}} days',
},
upgradeBtn: {
encourage: 'Upgrade Now',
encourageShort: 'Upgrade',
plain: 'View Plan',
},
},
},
},
},
})
const Template = (args: React.ComponentProps<typeof TriggerEventsLimitModal>) => {
const [visible, setVisible] = useState<boolean>(args.show ?? true)
useEffect(() => {
setVisible(args.show ?? true)
}, [args.show])
const handleHide = () => setVisible(false)
return (
<I18nextProvider i18n={i18n}>
<div className="flex flex-col gap-4">
<button
className="rounded-lg border border-divider-subtle px-4 py-2 text-sm text-text-secondary hover:border-divider-deep hover:text-text-primary"
onClick={() => setVisible(true)}
>
Open Modal
</button>
<TriggerEventsLimitModal
{...args}
show={visible}
onDismiss={handleHide}
onUpgrade={handleHide}
/>
</div>
</I18nextProvider>
)
}
const meta = {
title: 'Billing/TriggerEventsLimitModal',
component: TriggerEventsLimitModal,
parameters: {
layout: 'centered',
},
args: {
show: true,
usage: 120,
total: 120,
resetInDays: 5,
planType: Plan.professional,
},
} satisfies Meta<typeof TriggerEventsLimitModal>
export default meta
type Story = StoryObj<typeof meta>
export const Professional: Story = {
args: {
onDismiss: () => { /* noop */ },
onUpgrade: () => { /* noop */ },
},
render: args => <Template {...args} />,
}
export const Sandbox: Story = {
render: args => <Template {...args} />,
args: {
onDismiss: () => { /* noop */ },
onUpgrade: () => { /* noop */ },
resetInDays: undefined,
planType: Plan.sandbox,
},
}

View File

@@ -0,0 +1,90 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
import UsageInfo from '@/app/components/billing/usage-info'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import type { Plan } from '@/app/components/billing/type'
import styles from './index.module.css'
type Props = {
show: boolean
onDismiss: () => void
onUpgrade: () => void
usage: number
total: number
resetInDays?: number
planType: Plan
}
const TriggerEventsLimitModal: FC<Props> = ({
show,
onDismiss,
onUpgrade,
usage,
total,
resetInDays,
}) => {
const { t } = useTranslation()
return (
<Modal
isShow={show}
onClose={onDismiss}
closable={false}
clickOutsideNotClose
className={`${styles.surface} flex h-[360px] w-[580px] flex-col overflow-hidden rounded-2xl !p-0 shadow-xl`}
>
<div className='relative flex w-full flex-1 items-stretch justify-center'>
<div
aria-hidden
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
/>
<div className='relative z-10 flex w-full flex-col items-start gap-4 px-8 pt-8'>
<div className={`${styles.icon} flex h-12 w-12 items-center justify-center rounded-[12px]`}>
<TriggerAll className='h-5 w-5 text-text-primary-on-surface' />
</div>
<div className='flex flex-col items-start gap-2'>
<div className={`${styles.highlight} title-lg-semi-bold`}>
{t('billing.triggerLimitModal.title')}
</div>
<div className='body-md-regular text-text-secondary'>
{t('billing.triggerLimitModal.description')}
</div>
</div>
<UsageInfo
className='mb-5 w-full rounded-[12px] bg-components-panel-on-panel-item-bg'
Icon={TriggerAll}
name={t('billing.triggerLimitModal.usageTitle')}
usage={usage}
total={total}
resetInDays={resetInDays}
hideIcon
/>
</div>
</div>
<div className='flex h-[76px] w-full items-center justify-end gap-2 px-8 pb-8 pt-5'>
<Button
className='h-8 w-[77px] min-w-[72px] !rounded-lg !border-[0.5px] px-3 py-2'
onClick={onDismiss}
>
{t('billing.triggerLimitModal.dismiss')}
</Button>
<UpgradeBtn
isShort
onClick={onUpgrade}
className='flex w-[93px] items-center justify-center !rounded-lg !px-2'
style={{ height: 32 }}
labelKey='billing.triggerLimitModal.upgrade'
loc='trigger-events-limit-modal'
/>
</div>
</Modal>
)
}
export default React.memo(TriggerEventsLimitModal)

View File

@@ -55,6 +55,11 @@ export type SelfHostedPlanInfo = {
export type UsagePlanInfo = Pick<PlanInfo, 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota' | 'apiRateLimit' | 'triggerEvents'> & { vectorSpace: number }
export type UsageResetInfo = {
apiRateLimit?: number | null
triggerEvents?: number | null
}
export enum DocumentProcessingPriority {
standard = 'standard',
priority = 'priority',
@@ -91,10 +96,12 @@ export type CurrentPlanInfoBackend = {
api_rate_limit?: {
size: number
limit: number // total. 0 means unlimited
reset_in_days?: number
}
trigger_events?: {
size: number
limit: number // total. 0 means unlimited
reset_in_days?: number
}
docs_processing: DocumentProcessingPriority
can_replace_logo: boolean

View File

@@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { CSSProperties, FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import PremiumBadge from '../../base/premium-badge'
@@ -9,19 +9,24 @@ import { useModalContext } from '@/context/modal-context'
type Props = {
className?: string
style?: CSSProperties
isFull?: boolean
size?: 'md' | 'lg'
isPlain?: boolean
isShort?: boolean
onClick?: () => void
loc?: string
labelKey?: string
}
const UpgradeBtn: FC<Props> = ({
className,
style,
isPlain = false,
isShort = false,
onClick: _onClick,
loc,
labelKey,
}) => {
const { t } = useTranslation()
const { setShowPricingModal } = useModalContext()
@@ -40,10 +45,17 @@ const UpgradeBtn: FC<Props> = ({
}
}
const defaultBadgeLabel = t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)
const label = labelKey ? t(labelKey) : defaultBadgeLabel
if (isPlain) {
return (
<Button onClick={onClick}>
{t('billing.upgradeBtn.plain')}
<Button
className={className}
style={style}
onClick={onClick}
>
{labelKey ? label : t('billing.upgradeBtn.plain')}
</Button>
)
}
@@ -54,11 +66,13 @@ const UpgradeBtn: FC<Props> = ({
color='blue'
allowHover={true}
onClick={onClick}
className={className}
style={style}
>
<SparklesSoft className='flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
{label}
</span>
</div>
</PremiumBadge>

View File

@@ -16,10 +16,12 @@ type Props = {
total: number
unit?: string
unitPosition?: 'inline' | 'suffix'
resetHint?: string
resetInDays?: number
hideIcon?: boolean
}
const LOW = 50
const MIDDLE = 80
const WARNING_THRESHOLD = 80
const UsageInfo: FC<Props> = ({
className,
@@ -30,28 +32,39 @@ const UsageInfo: FC<Props> = ({
total,
unit,
unitPosition = 'suffix',
resetHint,
resetInDays,
hideIcon = false,
}) => {
const { t } = useTranslation()
const percent = usage / total * 100
const color = (() => {
if (percent < LOW)
return 'bg-components-progress-bar-progress-solid'
if (percent < MIDDLE)
return 'bg-components-progress-warning-progress'
return 'bg-components-progress-error-progress'
})()
const color = percent >= 100
? 'bg-components-progress-error-progress'
: (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid')
const isUnlimited = total === NUM_INFINITE
let totalDisplay: string | number = isUnlimited ? t('billing.plansCommon.unlimited') : total
if (!isUnlimited && unit && unitPosition === 'inline')
totalDisplay = `${total}${unit}`
const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix'
const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('billing.usagePage.resetsIn', { count: resetInDays }) : undefined)
const rightInfo = resetText
? (
<div className='system-xs-regular ml-auto flex-1 text-right text-text-tertiary'>
{resetText}
</div>
)
: (showUnit && (
<div className='system-xs-medium ml-auto text-text-tertiary'>
{unit}
</div>
))
return (
<div className={cn('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
<Icon className='h-4 w-4 text-text-tertiary' />
{!hideIcon && Icon && (
<Icon className='h-4 w-4 text-text-tertiary' />
)}
<div className='flex items-center gap-1'>
<div className='system-xs-medium text-text-tertiary'>{name}</div>
{tooltip && (
@@ -70,11 +83,7 @@ const UsageInfo: FC<Props> = ({
<div className='system-md-regular text-text-quaternary'>/</div>
<div>{totalDisplay}</div>
</div>
{showUnit && (
<div className='system-xs-medium ml-auto text-text-tertiary'>
{unit}
</div>
)}
{rightInfo}
</div>
<ProgressBar
percent={percent}

View File

@@ -36,5 +36,9 @@ export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE),
triggerEvents: resolveLimit(data.trigger_events?.limit, planPreset?.triggerEvents),
},
reset: {
apiRateLimit: data.api_rate_limit?.reset_in_days ?? null,
triggerEvents: data.trigger_events?.reset_in_days ?? null,
},
}
}

View File

@@ -40,6 +40,8 @@ import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames'
import { useIsChatMode } from '@/app/components/workflow/hooks'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
const FeaturesTrigger = () => {
const { t } = useTranslation()
@@ -50,6 +52,7 @@ const FeaturesTrigger = () => {
const appID = appDetail?.id
const setAppDetail = useAppStore(s => s.setAppDetail)
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const { plan, isFetchedPlan } = useProviderContext()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
@@ -95,6 +98,15 @@ const FeaturesTrigger = () => {
const hasTriggerNode = useMemo(() => (
nodes.some(node => isTriggerNode(node.data.type as BlockEnum))
), [nodes])
const startNodeLimitExceeded = useMemo(() => {
const entryCount = nodes.reduce((count, node) => {
const nodeType = node.data.type as BlockEnum
if (nodeType === BlockEnum.Start || isTriggerNode(nodeType))
return count + 1
return count
}, 0)
return isFetchedPlan && plan.type === Plan.sandbox && entryCount > 2
}, [nodes, plan.type, isFetchedPlan])
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
const invalidateAppTriggers = useInvalidateAppTriggers()
@@ -196,7 +208,8 @@ const FeaturesTrigger = () => {
crossAxisOffset: 4,
missingStartNode: !startNode,
hasTriggerNode,
publishDisabled: !hasWorkflowNodes,
startNodeLimitExceeded,
publishDisabled: !hasWorkflowNodes || startNodeLimitExceeded,
}}
/>
</>

View File

@@ -0,0 +1,130 @@
import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
import dayjs from 'dayjs'
import { NUM_INFINITE } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type'
import { IS_CLOUD_EDITION } from '@/config'
import type { ModalState } from '../modal-context'
export type TriggerEventsLimitModalPayload = {
usage: number
total: number
resetInDays?: number
planType: Plan
storageKey?: string
persistDismiss?: boolean
}
type TriggerPlanInfo = {
type: Plan
usage: { triggerEvents: number }
total: { triggerEvents: number }
reset: { triggerEvents?: number | null }
}
type UseTriggerEventsLimitModalOptions = {
plan: TriggerPlanInfo
isFetchedPlan: boolean
currentWorkspaceId?: string
}
type UseTriggerEventsLimitModalResult = {
showTriggerEventsLimitModal: ModalState<TriggerEventsLimitModalPayload> | null
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
persistTriggerEventsLimitModalDismiss: () => void
}
const TRIGGER_EVENTS_LOCALSTORAGE_PREFIX = 'trigger-events-limit-dismissed'
export const useTriggerEventsLimitModal = ({
plan,
isFetchedPlan,
currentWorkspaceId,
}: UseTriggerEventsLimitModalOptions): UseTriggerEventsLimitModalResult => {
const [showTriggerEventsLimitModal, setShowTriggerEventsLimitModal] = useState<ModalState<TriggerEventsLimitModalPayload> | null>(null)
const dismissedTriggerEventsLimitStorageKeysRef = useRef<Record<string, boolean>>({})
useEffect(() => {
if (!IS_CLOUD_EDITION)
return
if (typeof window === 'undefined')
return
if (!currentWorkspaceId)
return
if (!isFetchedPlan) {
setShowTriggerEventsLimitModal(null)
return
}
const { type, usage, total, reset } = plan
const isUnlimited = total.triggerEvents === NUM_INFINITE
const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents
if (type === Plan.team || isUnlimited || !reachedLimit) {
if (showTriggerEventsLimitModal)
setShowTriggerEventsLimitModal(null)
return
}
const triggerResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
? reset.triggerEvents ?? undefined
: undefined
const cycleTag = (() => {
if (typeof reset.triggerEvents === 'number')
return dayjs().startOf('day').add(reset.triggerEvents, 'day').format('YYYY-MM-DD')
if (type === Plan.sandbox)
return dayjs().endOf('month').format('YYYY-MM-DD')
return 'none'
})()
const storageKey = `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${type}-${total.triggerEvents}-${cycleTag}`
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
return
let persistDismiss = true
let hasDismissed = false
try {
if (localStorage.getItem(storageKey) === '1')
hasDismissed = true
}
catch {
persistDismiss = false
}
if (hasDismissed)
return
if (showTriggerEventsLimitModal?.payload.storageKey === storageKey)
return
setShowTriggerEventsLimitModal({
payload: {
usage: usage.triggerEvents,
total: total.triggerEvents,
planType: type,
resetInDays: triggerResetInDays,
storageKey,
persistDismiss,
},
})
}, [plan, isFetchedPlan, showTriggerEventsLimitModal, currentWorkspaceId])
const persistTriggerEventsLimitModalDismiss = useCallback(() => {
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
if (!storageKey)
return
if (showTriggerEventsLimitModal?.payload.persistDismiss) {
try {
localStorage.setItem(storageKey, '1')
return
}
catch {
// ignore error and fall back to in-memory guard
}
}
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
}, [showTriggerEventsLimitModal])
return {
showTriggerEventsLimitModal,
setShowTriggerEventsLimitModal,
persistTriggerEventsLimitModalDismiss,
}
}

View File

@@ -0,0 +1,181 @@
import React from 'react'
import { act, render, screen, waitFor } from '@testing-library/react'
import { ModalContextProvider } from '@/context/modal-context'
import { Plan } from '@/app/components/billing/type'
import { defaultPlan } from '@/app/components/billing/config'
jest.mock('@/config', () => {
const actual = jest.requireActual('@/config')
return {
...actual,
IS_CLOUD_EDITION: true,
}
})
jest.mock('next/navigation', () => ({
useSearchParams: jest.fn(() => new URLSearchParams()),
}))
const mockUseProviderContext = jest.fn()
jest.mock('@/context/provider-context', () => ({
useProviderContext: () => mockUseProviderContext(),
}))
const mockUseAppContext = jest.fn()
jest.mock('@/context/app-context', () => ({
useAppContext: () => mockUseAppContext(),
}))
let latestTriggerEventsModalProps: any = null
const triggerEventsLimitModalMock = jest.fn((props: any) => {
latestTriggerEventsModalProps = props
return (
<div data-testid="trigger-limit-modal">
<button type="button" onClick={props.onDismiss}>dismiss</button>
<button type="button" onClick={props.onUpgrade}>upgrade</button>
</div>
)
})
jest.mock('@/app/components/billing/trigger-events-limit-modal', () => ({
__esModule: true,
default: (props: any) => triggerEventsLimitModalMock(props),
}))
type DefaultPlanShape = typeof defaultPlan
type PlanOverrides = Partial<Omit<DefaultPlanShape, 'usage' | 'total' | 'reset'>> & {
usage?: Partial<DefaultPlanShape['usage']>
total?: Partial<DefaultPlanShape['total']>
reset?: Partial<DefaultPlanShape['reset']>
}
const createPlan = (overrides: PlanOverrides = {}): DefaultPlanShape => ({
...defaultPlan,
...overrides,
usage: {
...defaultPlan.usage,
...(overrides.usage ?? {}),
},
total: {
...defaultPlan.total,
...(overrides.total ?? {}),
},
reset: {
...defaultPlan.reset,
...(overrides.reset ?? {}),
},
})
const renderProvider = () => render(
<ModalContextProvider>
<div data-testid="modal-context-test-child" />
</ModalContextProvider>,
)
describe('ModalContextProvider trigger events limit modal', () => {
beforeEach(() => {
latestTriggerEventsModalProps = null
triggerEventsLimitModalMock.mockClear()
mockUseAppContext.mockReset()
mockUseProviderContext.mockReset()
window.localStorage.clear()
mockUseAppContext.mockReturnValue({
currentWorkspace: {
id: 'workspace-1',
},
})
})
afterEach(() => {
jest.restoreAllMocks()
})
it('opens the trigger events limit modal and persists dismissal in localStorage', async () => {
const plan = createPlan({
type: Plan.professional,
usage: { triggerEvents: 3000 },
total: { triggerEvents: 3000 },
reset: { triggerEvents: 5 },
})
mockUseProviderContext.mockReturnValue({
plan,
isFetchedPlan: true,
})
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
renderProvider()
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
expect(latestTriggerEventsModalProps).toMatchObject({
usage: 3000,
total: 3000,
resetInDays: 5,
planType: Plan.professional,
})
act(() => {
latestTriggerEventsModalProps.onDismiss()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
const [key, value] = setItemSpy.mock.calls[0]
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
expect(value).toBe('1')
})
it('relies on the in-memory guard when localStorage reads throw', async () => {
const plan = createPlan({
type: Plan.professional,
usage: { triggerEvents: 200 },
total: { triggerEvents: 200 },
reset: { triggerEvents: 3 },
})
mockUseProviderContext.mockReturnValue({
plan,
isFetchedPlan: true,
})
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new Error('Storage disabled')
})
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
renderProvider()
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
act(() => {
latestTriggerEventsModalProps.onDismiss()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
expect(setItemSpy).not.toHaveBeenCalled()
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
})
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
const plan = createPlan({
type: Plan.professional,
usage: { triggerEvents: 120 },
total: { triggerEvents: 120 },
reset: { triggerEvents: 2 },
})
mockUseProviderContext.mockReturnValue({
plan,
isFetchedPlan: true,
})
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new Error('Quota exceeded')
})
renderProvider()
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
act(() => {
latestTriggerEventsModalProps.onDismiss()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
})
})

View File

@@ -36,6 +36,12 @@ import { noop } from 'lodash-es'
import dynamic from 'next/dynamic'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import {
type TriggerEventsLimitModalPayload,
useTriggerEventsLimitModal,
} from './hooks/use-trigger-events-limit-modal'
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
ssr: false,
@@ -74,6 +80,9 @@ const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugi
const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
ssr: false,
})
const TriggerEventsLimitModal = dynamic(() => import('@/app/components/billing/trigger-events-limit-modal'), {
ssr: false,
})
export type ModalState<T> = {
payload: T
@@ -113,6 +122,7 @@ export type ModalContextState = {
}> | null>>
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
}
const PRICING_MODAL_QUERY_PARAM = 'pricing'
const PRICING_MODAL_QUERY_VALUE = 'open'
@@ -130,6 +140,7 @@ const ModalContext = createContext<ModalContextState>({
setShowOpeningModal: noop,
setShowUpdatePluginModal: noop,
setShowEducationExpireNoticeModal: noop,
setShowTriggerEventsLimitModal: noop,
})
export const useModalContext = () => useContext(ModalContext)
@@ -168,6 +179,7 @@ export const ModalContextProvider = ({
}> | null>(null)
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const [showPricingModal, setShowPricingModal] = useState(
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
@@ -228,6 +240,17 @@ export const ModalContextProvider = ({
window.history.replaceState(null, '', url.toString())
}, [showPricingModal])
const { plan, isFetchedPlan } = useProviderContext()
const {
showTriggerEventsLimitModal,
setShowTriggerEventsLimitModal,
persistTriggerEventsLimitModalDismiss,
} = useTriggerEventsLimitModal({
plan,
isFetchedPlan,
currentWorkspaceId: currentWorkspace?.id,
})
const handleCancelModerationSettingModal = () => {
setShowModerationSettingModal(null)
if (showModerationSettingModal?.onCancelCallback)
@@ -334,6 +357,7 @@ export const ModalContextProvider = ({
setShowOpeningModal,
setShowUpdatePluginModal,
setShowEducationExpireNoticeModal,
setShowTriggerEventsLimitModal,
}}>
<>
{children}
@@ -455,6 +479,25 @@ export const ModalContextProvider = ({
onClose={() => setShowEducationExpireNoticeModal(null)}
/>
)}
{
!!showTriggerEventsLimitModal && (
<TriggerEventsLimitModal
show
usage={showTriggerEventsLimitModal.payload.usage}
total={showTriggerEventsLimitModal.payload.total}
planType={showTriggerEventsLimitModal.payload.planType}
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
onDismiss={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
}}
onUpgrade={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
handleShowPricingModal()
}}
/>
)}
</>
</ModalContext.Provider>
)

View File

@@ -17,7 +17,7 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { Plan } from '@/app/components/billing/type'
import type { Plan, UsageResetInfo } from '@/app/components/billing/type'
import type { UsagePlanInfo } from '@/app/components/billing/type'
import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils'
@@ -40,6 +40,7 @@ type ProviderContextState = {
type: Plan
usage: UsagePlanInfo
total: UsagePlanInfo
reset: UsageResetInfo
}
isFetchedPlan: boolean
enableBilling: boolean

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Nachrichtenguthaben',
tooltip: 'Nachrichtenaufrufkontingente für verschiedene Tarife unter Verwendung von OpenAI-Modellen (außer gpt4).Nachrichten über dem Limit verwenden Ihren OpenAI-API-Schlüssel.',
titlePerMonth: '{{count,number}} Nachrichten/Monat',
titlePerMonth: '{{count,number}} Nachrichten / Monat',
},
annotatedResponse: {
title: 'Kontingentgrenzen für Annotationen',
@@ -83,7 +83,7 @@ const translation = {
cloud: 'Cloud-Dienst',
apiRateLimitTooltip: 'Die API-Datenbeschränkung gilt für alle Anfragen, die über die Dify-API gemacht werden, einschließlich Textgenerierung, Chat-Konversationen, Workflow-Ausführungen und Dokumentenverarbeitung.',
getStarted: 'Loslegen',
apiRateLimitUnit: '{{count,number}}/Monat',
apiRateLimitUnit: '{{count,number}}',
documentsTooltip: 'Vorgabe für die Anzahl der Dokumente, die aus der Wissensdatenquelle importiert werden.',
apiRateLimit: 'API-Datenlimit',
documents: '{{count,number}} Wissensdokumente',

View File

@@ -9,8 +9,16 @@ const translation = {
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
triggerEvents: 'Trigger Events',
perMonth: 'per month',
resetsIn: 'Resets in {{count,number}} days',
},
teamMembers: 'Team Members',
triggerLimitModal: {
title: 'Upgrade to unlock more trigger events',
description: 'Youve reached the limit of workflow event triggers for this plan.',
dismiss: 'Dismiss',
upgrade: 'Upgrade',
usageTitle: 'TRIGGER EVENTS',
},
upgradeBtn: {
plain: 'View Plan',
encourage: 'Upgrade Now',
@@ -61,11 +69,11 @@ const translation = {
documentsTooltip: 'Quota on the number of documents imported from the Knowledge Data Source.',
vectorSpace: '{{size}} Knowledge Data Storage',
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
documentsRequestQuota: '{{count,number}}/min Knowledge Request Rate Limit',
documentsRequestQuota: '{{count,number}} Knowledge Request / min',
documentsRequestQuotaTooltip: 'Specifies the total number of actions a workspace can perform per minute within the knowledge base, including dataset creation, deletion, updates, document uploads, modifications, archiving, and knowledge base queries. This metric is used to evaluate the performance of knowledge base requests. For example, if a Sandbox user performs 10 consecutive hit tests within one minute, their workspace will be temporarily restricted from performing the following actions for the next minute: dataset creation, deletion, updates, and document uploads or modifications. ',
apiRateLimit: 'API Rate Limit',
apiRateLimitUnit: '{{count,number}}/month',
unlimitedApiRate: 'No API Rate Limit',
apiRateLimitUnit: '{{count,number}}',
unlimitedApiRate: 'No Dify API Rate Limit',
apiRateLimitTooltip: 'API Rate Limit applies to all requests made through the Dify API, including text generation, chat conversations, workflow executions, and document processing.',
documentProcessingPriority: ' Document Processing',
documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
@@ -76,17 +84,19 @@ const translation = {
},
triggerEvents: {
sandbox: '{{count,number}} Trigger Events',
professional: '{{count,number}} Trigger Events/month',
professional: '{{count,number}} Trigger Events / month',
unlimited: 'Unlimited Trigger Events',
tooltip: 'The number of events that automatically start workflows through Plugin, Schedule, or Webhook triggers.',
},
workflowExecution: {
standard: 'Standard Workflow Execution',
faster: 'Faster Workflow Execution',
priority: 'Priority Workflow Execution',
tooltip: 'Workflow execution queue priority and speed.',
},
startNodes: {
limited: 'Up to {{count}} Start Nodes per Workflow',
unlimited: 'Unlimited Start Nodes per Workflow',
limited: 'Up to {{count}} Start Nodes / workflow',
unlimited: 'Unlimited Start Nodes / workflow',
},
logsHistory: '{{days}} Log history',
customTools: 'Custom Tools',
@@ -115,7 +125,7 @@ const translation = {
memberAfter: 'Member',
messageRequest: {
title: '{{count,number}} message credits',
titlePerMonth: '{{count,number}} message credits/month',
titlePerMonth: '{{count,number}} message credits / month',
tooltip: 'Message credits are provided to help you easily try out different OpenAI models in Dify. Credits are consumed based on the model type. Once theyre used up, you can switch to your own OpenAI API key.',
},
annotatedResponse: {

View File

@@ -123,6 +123,11 @@ const translation = {
noHistory: 'No History',
tagBound: 'Number of apps using this tag',
},
publishLimit: {
startNodeTitlePrefix: 'Upgrade to',
startNodeTitleSuffix: 'unlock unlimited start nodes',
startNodeDesc: 'Youve reached the limit of 2 start nodes for your current plan. Upgrade to publish this workflow.',
},
env: {
envPanelTitle: 'Environment Variables',
envDescription: 'Environment variables can be used to store private information and credentials. They are read-only and can be separated from the DSL file during export.',

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Créditos de Mensajes',
tooltip: 'Cuotas de invocación de mensajes para varios planes utilizando modelos de OpenAI (excepto gpt4). Los mensajes que excedan el límite utilizarán tu clave API de OpenAI.',
titlePerMonth: '{{count,number}} mensajes/mes',
titlePerMonth: '{{count,number}} mensajes / mes',
},
annotatedResponse: {
title: 'Límites de Cuota de Anotación',
@@ -76,7 +76,7 @@ const translation = {
priceTip: 'por espacio de trabajo/',
teamMember_one: '{{count, número}} Miembro del Equipo',
getStarted: 'Comenzar',
apiRateLimitUnit: '{{count, número}}/mes',
apiRateLimitUnit: '{{count, número}}',
freeTrialTipSuffix: 'No se requiere tarjeta de crédito',
unlimitedApiRate: 'Sin límite de tasa de API',
apiRateLimit: 'Límite de tasa de API',

View File

@@ -73,7 +73,7 @@ const translation = {
},
ragAPIRequestTooltip: 'به تعداد درخواست‌های API که فقط قابلیت‌های پردازش پایگاه دانش Dify را فراخوانی می‌کنند اشاره دارد.',
receiptInfo: 'فقط صاحب تیم و مدیر تیم می‌توانند اشتراک تهیه کنند و اطلاعات صورتحساب را مشاهده کنند',
apiRateLimitUnit: '{{count,number}}/ماه',
apiRateLimitUnit: '{{count,number}}',
cloud: 'سرویس ابری',
documents: '{{count,number}} سندهای دانش',
self: 'خود میزبان',

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Crédits de message',
tooltip: 'Quotas d\'invocation de messages pour divers plans utilisant les modèles OpenAI (sauf gpt4). Les messages dépassant la limite utiliseront votre clé API OpenAI.',
titlePerMonth: '{{count,number}} messages/mois',
titlePerMonth: '{{count,number}} messages / mois',
},
annotatedResponse: {
title: 'Limites de quota d\'annotation',
@@ -73,7 +73,7 @@ const translation = {
ragAPIRequestTooltip: 'Fait référence au nombre d\'appels API invoquant uniquement les capacités de traitement de la base de connaissances de Dify.',
receiptInfo: 'Seuls le propriétaire de l\'équipe et l\'administrateur de l\'équipe peuvent s\'abonner et consulter les informations de facturation',
annotationQuota: 'Quota dannotation',
apiRateLimitUnit: '{{count,number}}/mois',
apiRateLimitUnit: '{{count,number}}',
priceTip: 'par espace de travail/',
freeTrialTipSuffix: 'Aucune carte de crédit requise',
teamWorkspace: '{{count,number}} Espace de travail d\'équipe',

View File

@@ -70,7 +70,7 @@ const translation = {
title: 'संदेश क्रेडिट्स',
tooltip:
'विभिन्न योजनाओं के लिए संदेश आह्वान कोटा OpenAI मॉडलों का उपयोग करके (gpt4 को छोड़कर)। सीमा से अधिक संदेश आपके OpenAI API कुंजी का उपयोग करेंगे।',
titlePerMonth: '{{count,number}} संदेश/महीना',
titlePerMonth: '{{count,number}} संदेश / महीना',
},
annotatedResponse: {
title: 'एनोटेशन कोटा सीमाएं',
@@ -96,7 +96,7 @@ const translation = {
freeTrialTip: '200 ओपनएआई कॉल्स का मुफ्त परीक्षण।',
documents: '{{count,number}} ज्ञान दस्तावेज़',
freeTrialTipSuffix: 'कोई क्रेडिट कार्ड की आवश्यकता नहीं है',
apiRateLimitUnit: '{{count,number}}/माह',
apiRateLimitUnit: '{{count,number}}',
teamWorkspace: '{{count,number}} टीम कार्यक्षेत्र',
apiRateLimitTooltip: 'Dify API के माध्यम से की गई सभी अनुरोधों पर API दर सीमा लागू होती है, जिसमें टेक्स्ट जनरेशन, चैट वार्तालाप, कार्यप्रवाह निष्पादन और दस्तावेज़ प्रसंस्करण शामिल हैं।',
teamMember_one: '{{count,number}} टीम सदस्य',

View File

@@ -70,7 +70,7 @@ const translation = {
title: 'Crediti Messaggi',
tooltip:
'Quote di invocazione dei messaggi per vari piani utilizzando i modelli OpenAI (eccetto gpt4). I messaggi oltre il limite utilizzeranno la tua chiave API OpenAI.',
titlePerMonth: '{{count,number}} messaggi/mese',
titlePerMonth: '{{count,number}} messaggi / mese',
},
annotatedResponse: {
title: 'Limiti di Quota di Annotazione',
@@ -88,7 +88,7 @@ const translation = {
freeTrialTipPrefix: 'Iscriviti e ricevi un',
teamMember_one: '{{count,number}} membro del team',
documents: '{{count,number}} Documenti di Conoscenza',
apiRateLimitUnit: '{{count,number}}/mese',
apiRateLimitUnit: '{{count,number}}',
documentsRequestQuota: '{{count,number}}/min Limite di richiesta di conoscenza',
teamMember_other: '{{count,number}} membri del team',
freeTrialTip: 'prova gratuita di 200 chiamate OpenAI.',

View File

@@ -7,8 +7,16 @@ const translation = {
documentsUploadQuota: 'ドキュメント・アップロード・クォータ',
vectorSpace: 'ナレッジベースのデータストレージ',
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
triggerEvents: 'トリガーイベント',
triggerEvents: 'トリガーイベント',
perMonth: '月あたり',
resetsIn: '{{count,number}}日後にリセット',
},
triggerLimitModal: {
title: 'さらにトリガーイベントを利用するにはアップグレードしてください',
description: 'このプランのワークフロー・トリガーイベントの上限に達しました。',
dismiss: '閉じる',
upgrade: 'アップグレード',
usageTitle: 'TRIGGER EVENTS',
},
upgradeBtn: {
plain: 'プランをアップグレード',
@@ -59,10 +67,10 @@ const translation = {
documentsTooltip: 'ナレッジデータソースからインポートされたドキュメントの数に対するクォータ。',
vectorSpace: '{{size}}のナレッジベースのデータストレージ',
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限',
documentsRequestQuota: '{{count,number}} のナレッジリクエスト上限 / 分',
documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが 1 分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが 1 分間に 10 回連続でヒットテストを実行した場合、そのワークスペースは次の 1 分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。',
apiRateLimit: 'API レート制限',
apiRateLimitUnit: '{{count,number}}/月',
apiRateLimit: 'API リクエスト制限',
apiRateLimitUnit: '{{count,number}}',
unlimitedApiRate: '無制限の API コール',
apiRateLimitTooltip: 'API レート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API 経由のすべてのリクエストに適用されます。',
documentProcessingPriority: '文書処理',
@@ -72,6 +80,22 @@ const translation = {
'priority': '優先',
'top-priority': '最優先',
},
triggerEvents: {
sandbox: '{{count,number}} トリガーイベント数',
professional: '{{count,number}} トリガーイベント数 / 月',
unlimited: '無制限のトリガーイベント数',
tooltip: 'プラグイントリガー、タイマートリガー、または Webhook トリガーによって自動的にワークフローを起動するイベントの回数です。',
},
workflowExecution: {
standard: '標準ワークフロー実行キュー',
faster: '高速ワークフロー実行キュー',
priority: '優先度の高いワークフロー実行キュー',
tooltip: 'ワークフローの実行キューの優先度と実行速度。',
},
startNodes: {
limited: '各ワークフローにつき、開始ノードは最大{{count}}つまで設定可能',
unlimited: '各ワークフローの開始ノード数は無制限',
},
logsHistory: '{{days}}のログ履歴',
customTools: 'カスタムツール',
unavailable: '利用不可',
@@ -99,7 +123,7 @@ const translation = {
memberAfter: 'メンバー',
messageRequest: {
title: '{{count,number}}メッセージクレジット',
titlePerMonth: '{{count,number}}メッセージクレジット/月',
titlePerMonth: '{{count,number}}メッセージクレジット / 月',
tooltip: 'メッセージクレジットは、Dify でさまざまな OpenAI モデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身の OpenAI API キーに切り替えていただけます。',
},
annotatedResponse: {

View File

@@ -119,6 +119,11 @@ const translation = {
tagBound: 'このタグを使用しているアプリの数',
moreActions: 'さらにアクション',
},
publishLimit: {
startNodeTitlePrefix: 'アップグレードして',
startNodeTitleSuffix: '開始ノードの上限を解除',
startNodeDesc: '現在のプランでは開始ードは2個までです。公開するにはプランをアップグレードしてください。',
},
env: {
envPanelTitle: '環境変数',
envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。',

View File

@@ -68,7 +68,7 @@ const translation = {
title: '메시지 크레딧',
tooltip:
'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.',
titlePerMonth: '{{count,number}} 메시지/월',
titlePerMonth: '{{count,number}} 메시지 / 월',
},
annotatedResponse: {
title: '주석 응답 쿼터',
@@ -88,7 +88,7 @@ const translation = {
freeTrialTip: '200 회의 OpenAI 호출 무료 체험을 받으세요. ',
annualBilling: '연간 청구',
getStarted: '시작하기',
apiRateLimitUnit: '{{count,number}}/월',
apiRateLimitUnit: '{{count,number}}',
freeTrialTipSuffix: '신용카드 없음',
teamWorkspace: '{{count,number}} 팀 작업 공간',
self: '자체 호스팅',

View File

@@ -68,7 +68,7 @@ const translation = {
title: 'Limity kredytów wiadomości',
tooltip:
'Limity wywołań wiadomości dla różnych planów używających modeli OpenAI (z wyjątkiem gpt4). Wiadomości przekraczające limit będą korzystać z twojego klucza API OpenAI.',
titlePerMonth: '{{count,number}} wiadomości/miesiąc',
titlePerMonth: '{{count,number}} wiadomości / miesiąc',
},
annotatedResponse: {
title: 'Limity kredytów na adnotacje',
@@ -91,7 +91,7 @@ const translation = {
freeTrialTipPrefix: 'Zarejestruj się i zdobądź',
teamMember_other: '{{count,number}} członków zespołu',
teamWorkspace: '{{count,number}} Zespół Workspace',
apiRateLimitUnit: '{{count,number}}/miesiąc',
apiRateLimitUnit: '{{count,number}}',
cloud: 'Usługa chmurowa',
teamMember_one: '{{count,number}} Członek zespołu',
priceTip: 'na przestrzeń roboczą/',

View File

@@ -61,7 +61,7 @@ const translation = {
messageRequest: {
title: 'Créditos de Mensagem',
tooltip: 'Cotas de invocação de mensagens para vários planos usando modelos da OpenAI (exceto gpt4). Mensagens além do limite usarão sua Chave de API da OpenAI.',
titlePerMonth: '{{count,number}} mensagens/mês',
titlePerMonth: '{{count,number}} mensagens / mês',
},
annotatedResponse: {
title: 'Limites de Cota de Anotação',
@@ -80,7 +80,7 @@ const translation = {
documentsRequestQuota: '{{count,number}}/min Limite de Taxa de Solicitação de Conhecimento',
cloud: 'Serviço de Nuvem',
teamWorkspace: '{{count,number}} Espaço de Trabalho da Equipe',
apiRateLimitUnit: '{{count,number}}/mês',
apiRateLimitUnit: '{{count,number}}',
freeTrialTipSuffix: 'Nenhum cartão de crédito necessário',
teamMember_other: '{{count,number}} Membros da Equipe',
comparePlanAndFeatures: 'Compare planos e recursos',

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Credite de mesaje',
tooltip: 'Cote de invocare a mesajelor pentru diferite planuri utilizând modele OpenAI (cu excepția gpt4). Mesajele peste limită vor utiliza cheia API OpenAI.',
titlePerMonth: '{{count,number}} mesaje/lună',
titlePerMonth: '{{count,number}} mesaje / lună',
},
annotatedResponse: {
title: 'Limite de cotă de anotare',
@@ -82,7 +82,7 @@ const translation = {
documentsTooltip: 'Cota pe numărul de documente importate din Sursele de Date de Cunoștințe.',
getStarted: 'Întrebați-vă',
cloud: 'Serviciu de cloud',
apiRateLimitUnit: '{{count,number}}/lună',
apiRateLimitUnit: '{{count,number}}',
comparePlanAndFeatures: 'Compară planurile și caracteristicile',
documentsRequestQuota: '{{count,number}}/min Limita de rată a cererilor de cunoștințe',
documents: '{{count,number}} Documente de Cunoaștere',

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Кредиты на сообщения',
tooltip: 'Квоты вызова сообщений для различных тарифных планов, использующих модели OpenAI (кроме gpt4). Сообщения, превышающие лимит, будут использовать ваш ключ API OpenAI.',
titlePerMonth: '{{count,number}} сообщений/месяц',
titlePerMonth: '{{count,number}} сообщений / месяц',
},
annotatedResponse: {
title: 'Ограничения квоты аннотаций',
@@ -78,7 +78,7 @@ const translation = {
apiRateLimit: 'Ограничение скорости API',
self: 'Самостоятельно размещенный',
teamMember_other: '{{count,number}} Члены команды',
apiRateLimitUnit: '{{count,number}}/месяц',
apiRateLimitUnit: '{{count,number}}',
unlimitedApiRate: 'Нет ограничений на количество запросов к API',
freeTrialTip: 'бесплатная пробная версия из 200 вызовов OpenAI.',
freeTrialTipSuffix: 'Кредитная карта не требуется',

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Krediti za sporočila',
tooltip: 'Kvota za klice sporočil pri različnih načrtih z uporabo modelov OpenAI (razen GPT-4). Sporočila preko omejitve bodo uporabljala vaš OpenAI API ključ.',
titlePerMonth: '{{count,number}} sporočil/mesec',
titlePerMonth: '{{count,number}} sporočil / mesec',
},
annotatedResponse: {
title: 'Omejitve kvote za označevanje',
@@ -86,7 +86,7 @@ const translation = {
teamMember_one: '{{count,number}} član ekipe',
teamMember_other: '{{count,number}} Članov ekipe',
documentsRequestQuota: '{{count,number}}/min Omejitev stopnje zahtev po znanju',
apiRateLimitUnit: '{{count,number}}/mesec',
apiRateLimitUnit: '{{count,number}}',
priceTip: 'na delovnem prostoru/',
freeTrialTipPrefix: 'Prijavite se in prejmite',
cloud: 'Oblačna storitev',

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'เครดิตข้อความ',
tooltip: 'โควต้าการเรียกใช้ข้อความสําหรับแผนต่างๆ โดยใช้โมเดล OpenAI (ยกเว้น gpt4) ข้อความที่เกินขีดจํากัดจะใช้คีย์ OpenAI API ของคุณ',
titlePerMonth: '{{count,number}} ข้อความ/เดือน',
titlePerMonth: '{{count,number}} ข้อความ / เดือน',
},
annotatedResponse: {
title: 'ขีดจํากัดโควต้าคําอธิบายประกอบ',
@@ -82,7 +82,7 @@ const translation = {
teamMember_one: '{{count,number}} สมาชิกทีม',
unlimitedApiRate: 'ไม่มีข้อจำกัดอัตราการเรียก API',
self: 'โฮสต์ด้วยตัวเอง',
apiRateLimitUnit: '{{count,number}}/เดือน',
apiRateLimitUnit: '{{count,number}}',
teamMember_other: '{{count,number}} สมาชิกทีม',
teamWorkspace: '{{count,number}} ทีมทำงาน',
priceTip: 'ต่อพื้นที่ทำงาน/',

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Mesaj Kredileri',
tooltip: 'OpenAI modellerini (gpt4 hariç) kullanarak çeşitli planlar için mesaj çağrı kotaları. Limitin üzerindeki mesajlar OpenAI API Anahtarınızı kullanır.',
titlePerMonth: '{{count,number}} mesaj/ay',
titlePerMonth: '{{count,number}} mesaj / ay',
},
annotatedResponse: {
title: 'Ek Açıklama Kota Sınırları',
@@ -78,7 +78,7 @@ const translation = {
freeTrialTipPrefix: 'Kaydolun ve bir',
priceTip: 'iş alanı başına/',
documentsRequestQuota: '{{count,number}}/dakika Bilgi İsteği Oran Limiti',
apiRateLimitUnit: '{{count,number}}/ay',
apiRateLimitUnit: '{{count,number}}',
documents: '{{count,number}} Bilgi Belgesi',
comparePlanAndFeatures: 'Planları ve özellikleri karşılaştır',
self: 'Kendi Barındırılan',

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Кредити повідомлень',
tooltip: 'Квоти на виклик повідомлень для різних планів з використанням моделей OpenAI (крім gpt4). Повідомлення понад ліміт використовуватимуть ваш ключ API OpenAI.',
titlePerMonth: '{{count,number}} повідомлень/місяць',
titlePerMonth: '{{count,number}} повідомлень / місяць',
},
annotatedResponse: {
title: 'Ліміти квоти відповідей з анотаціями',
@@ -84,7 +84,7 @@ const translation = {
priceTip: 'за робочим простором/',
unlimitedApiRate: 'Немає обмеження на швидкість API',
freeTrialTipSuffix: 'Кредитна картка не потрібна',
apiRateLimitUnit: '{{count,number}}/місяць',
apiRateLimitUnit: '{{count,number}}',
getStarted: 'Почати',
freeTrialTip: 'безкоштовна пробна версія з 200 запитів до OpenAI.',
documents: '{{count,number}} Документів знань',

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Số Lượng Tin Nhắn',
tooltip: 'Hạn mức triệu hồi tin nhắn cho các kế hoạch sử dụng mô hình OpenAI (ngoại trừ gpt4). Các tin nhắn vượt quá giới hạn sẽ sử dụng Khóa API OpenAI của bạn.',
titlePerMonth: '{{count,number}} tin nhắn/tháng',
titlePerMonth: '{{count,number}} tin nhắn / tháng',
},
annotatedResponse: {
title: 'Hạn Mức Quota Phản hồi Đã được Ghi chú',
@@ -90,7 +90,7 @@ const translation = {
teamMember_other: '{{count,number}} thành viên trong nhóm',
documents: '{{count,number}} Tài liệu Kiến thức',
getStarted: 'Bắt đầu',
apiRateLimitUnit: '{{count,number}}/tháng',
apiRateLimitUnit: '{{count,number}}',
freeTrialTipSuffix: 'Không cần thẻ tín dụng',
documentsRequestQuotaTooltip: 'Chỉ định tổng số hành động mà một không gian làm việc có thể thực hiện mỗi phút trong cơ sở tri thức, bao gồm tạo mới tập dữ liệu, xóa, cập nhật, tải tài liệu lên, thay đổi, lưu trữ và truy vấn cơ sở tri thức. Chỉ số này được sử dụng để đánh giá hiệu suất của các yêu cầu cơ sở tri thức. Ví dụ, nếu một người dùng Sandbox thực hiện 10 lần kiểm tra liên tiếp trong một phút, không gian làm việc của họ sẽ bị hạn chế tạm thời không thực hiện các hành động sau trong phút tiếp theo: tạo mới tập dữ liệu, xóa, cập nhật và tải tài liệu lên hoặc thay đổi.',
startBuilding: 'Bắt đầu xây dựng',

View File

@@ -7,8 +7,16 @@ const translation = {
documentsUploadQuota: '文档上传配额',
vectorSpace: '知识库数据存储空间',
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
triggerEvents: '触发事件',
triggerEvents: '触发事件',
perMonth: '每月',
resetsIn: '{{count,number}} 天后重置',
},
triggerLimitModal: {
title: '升级以解锁更多触发事件额度',
description: '当前套餐的工作流触发事件额度已达上限。',
dismiss: '知道了',
upgrade: '升级',
usageTitle: '触发事件额度',
},
upgradeBtn: {
plain: '查看套餐',
@@ -60,10 +68,10 @@ const translation = {
documentsTooltip: '从知识库的数据源导入的文档数量配额。',
vectorSpace: '{{size}} 知识库数据存储空间',
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
documentsRequestQuota: '{{count,number}}/分钟 知识请求频率限制',
documentsRequestQuota: '{{count,number}} 知识请求 / 分钟',
documentsRequestQuotaTooltip: '指每分钟内一个空间在知识库中可执行的操作总数包括数据集的创建、删除、更新文档的上传、修改、归档以及知识库查询等用于评估知识库请求的性能。例如Sandbox 用户在 1 分钟内连续执行 10 次命中测试,其工作区将在接下来的 1 分钟内无法继续执行以下操作:数据集的创建、删除、更新,文档的上传、修改等操作。',
apiRateLimit: 'API 请求频率限制',
apiRateLimitUnit: '{{count,number}} 次/月',
apiRateLimitUnit: '{{count,number}} 次',
unlimitedApiRate: 'API 请求频率无限制',
apiRateLimitTooltip: 'API 请求频率限制涵盖所有通过 Dify API 发起的调用,例如文本生成、聊天对话、工作流执行和文档处理等。',
documentProcessingPriority: '文档处理',
@@ -74,18 +82,20 @@ const translation = {
'top-priority': '最高优先级',
},
triggerEvents: {
sandbox: '{{count,number}} 触发事件',
professional: '{{count,number}} 触发事件/月',
unlimited: '无限触发事件',
sandbox: '{{count,number}} 触发事件',
professional: '{{count,number}} 触发事件数 / 月',
unlimited: '无限触发事件',
tooltip: '通过插件、定时触发器、Webhook 等来自动触发工作流的事件数。',
},
workflowExecution: {
standard: '标准工作流执行',
faster: '更快的工作流执行',
priority: '优先工作流执行',
standard: '标准工作流执行队列',
faster: '快速工作流执行队列',
priority: '优先工作流执行队列',
tooltip: '工作流的执行队列优先级与运行速度。',
},
startNodes: {
limited: '每个工作流最多 {{count}} 个起始节点',
unlimited: '每个工作流无限起始节点',
limited: '最多 {{count}} 个起始节点 / 工作流',
unlimited: '无限起始节点 / 工作流',
},
logsHistory: '{{days}}日志历史',
customTools: '自定义工具',
@@ -114,7 +124,7 @@ const translation = {
memberAfter: '个成员',
messageRequest: {
title: '{{count,number}} 条消息额度',
titlePerMonth: '{{count,number}} 条消息额度/月',
titlePerMonth: '{{count,number}} 条消息额度 / 月',
tooltip: '消息额度旨在帮助您便捷地试用 Dify 中的各类 OpenAI 模型。不同模型会消耗不同额度。额度用尽后,您可以切换为使用自己的 OpenAI API 密钥。',
},
annotatedResponse: {

View File

@@ -122,6 +122,11 @@ const translation = {
noHistory: '没有历史版本',
tagBound: '使用此标签的应用数量',
},
publishLimit: {
startNodeTitlePrefix: '升级以',
startNodeTitleSuffix: '解锁无限开始节点',
startNodeDesc: '当前套餐最多支持 2 个开始节点。升级套餐即可发布此工作流。',
},
env: {
envPanelTitle: '环境变量',
envDescription: '环境变量是一种存储敏感信息的方法,如 API 密钥、数据库密码等。它们被存储在工作流程中,而不是代码中,以便在不同环境中共享。',

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: '訊息額度',
tooltip: '為不同方案提供基於 OpenAI 模型的訊息響應額度。',
titlePerMonth: '{{count,number}} 消息/月',
titlePerMonth: '{{count,number}} 消息 / 月',
},
annotatedResponse: {
title: '標註回覆數',
@@ -74,7 +74,7 @@ const translation = {
receiptInfo: '只有團隊所有者和團隊管理員才能訂閱和檢視賬單資訊',
annotationQuota: '註釋配額',
self: '自我主持',
apiRateLimitUnit: '{{count,number}}/月',
apiRateLimitUnit: '{{count,number}}',
freeTrialTipPrefix: '註冊並獲得一個',
annualBilling: '年度計費',
freeTrialTipSuffix: '無需信用卡',

View File

@@ -116,6 +116,11 @@ const translation = {
currentWorkflow: '當前工作流程',
moreActions: '更多動作',
},
publishLimit: {
startNodeTitlePrefix: '升級以',
startNodeTitleSuffix: '解鎖無限開始節點',
startNodeDesc: '目前方案最多允許 2 個開始節點,升級後才能發布此工作流程。',
},
env: {
envPanelTitle: '環境變數',
envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與 DSL 文件分開。',

View File

@@ -10,3 +10,10 @@ export const isAfter = (date: ConfigType, compare: ConfigType) => {
export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => {
return dayjs(date).format(dateFormat)
}
export const getDaysUntilEndOfMonth = (date: ConfigType = dayjs()) => {
const current = dayjs(date).startOf('day')
const endOfMonth = dayjs(date).endOf('month').startOf('day')
const diff = endOfMonth.diff(current, 'day')
return Math.max(diff, 0)
}