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 = auto()
EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = auto() EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = auto()
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = 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): class EmailLanguage(StrEnum):
@@ -445,6 +451,78 @@ def create_default_email_config() -> EmailI18nConfig:
branded_template_path="clean_document_job_mail_template_zh-CN.html", 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: { EmailType.EMAIL_REGISTER: {
EmailLanguage.EN_US: EmailTemplate( EmailLanguage.EN_US: EmailTemplate(
subject="Register Your {application_title} Account", 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 { AppModeEnum } from '@/types/app'
import type { PublishWorkflowParams } from '@/types/workflow' import type { PublishWorkflowParams } from '@/types/workflow'
import { basePath } from '@/utils/var' import { basePath } from '@/utils/var'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = { const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
[AccessMode.ORGANIZATION]: { [AccessMode.ORGANIZATION]: {
@@ -106,6 +107,7 @@ export type AppPublisherProps = {
workflowToolAvailable?: boolean workflowToolAvailable?: boolean
missingStartNode?: boolean missingStartNode?: boolean
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist). 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'] const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
@@ -127,6 +129,7 @@ const AppPublisher = ({
workflowToolAvailable = true, workflowToolAvailable = true,
missingStartNode = false, missingStartNode = false,
hasTriggerNode = false, hasTriggerNode = false,
startNodeLimitExceeded = false,
}: AppPublisherProps) => { }: AppPublisherProps) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -246,6 +249,13 @@ const AppPublisher = ({
const hasPublishedVersion = !!publishedAt const hasPublishedVersion = !!publishedAt
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined 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 ( return (
<> <>
@@ -304,29 +314,49 @@ const AppPublisher = ({
/> />
) )
: ( : (
<Button <>
variant='primary' <Button
className='mt-3 w-full' variant='primary'
onClick={() => handlePublish()} className='mt-3 w-full'
disabled={publishDisabled || published} onClick={() => handlePublish()}
> disabled={publishDisabled || published}
{ >
published {
? t('workflow.common.published') published
: ( ? t('workflow.common.published')
<div className='flex gap-1'> : (
<span>{t('workflow.common.publishUpdate')}</span> <div className='flex gap-1'>
<div className='flex gap-0.5'> <span>{t('workflow.common.publishUpdate')}</span>
{PUBLISH_SHORTCUT.map(key => ( <div className='flex gap-0.5'>
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'> {PUBLISH_SHORTCUT.map(key => (
{getKeyboardKeyNameBySystem(key)} <span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
</span> {getKeyboardKeyNameBySystem(key)}
))} </span>
))}
</div>
</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> </div>

View File

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

View File

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

View File

@@ -46,16 +46,10 @@ const List = ({
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })} label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')} 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 <Item
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')} label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
/> />
<Divider bgStyle='gradient' />
<Item <Item
label={ label={
planInfo.triggerEvents === NUM_INFINITE planInfo.triggerEvents === NUM_INFINITE
@@ -64,6 +58,14 @@ const List = ({
? t('billing.plansCommon.triggerEvents.sandbox', { count: planInfo.triggerEvents }) ? t('billing.plansCommon.triggerEvents.sandbox', { count: planInfo.triggerEvents })
: t('billing.plansCommon.triggerEvents.professional', { 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 <Item
label={ label={
@@ -73,13 +75,7 @@ const List = ({
? t('billing.plansCommon.workflowExecution.faster') ? t('billing.plansCommon.workflowExecution.faster')
: t('billing.plansCommon.workflowExecution.priority') : t('billing.plansCommon.workflowExecution.priority')
} }
/> tooltip={t('billing.plansCommon.workflowExecution.tooltip') as string}
<Item
label={
plan === Plan.sandbox
? t('billing.plansCommon.startNodes.limited', { count: 2 })
: t('billing.plansCommon.startNodes.unlimited')
}
/> />
<Divider bgStyle='gradient' /> <Divider bgStyle='gradient' />
<Item <Item
@@ -89,6 +85,14 @@ const List = ({
<Item <Item
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })} 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' /> <Divider bgStyle='gradient' />
<Item <Item
label={t('billing.plansCommon.modelProviders')} 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 UsagePlanInfo = Pick<PlanInfo, 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota' | 'apiRateLimit' | 'triggerEvents'> & { vectorSpace: number }
export type UsageResetInfo = {
apiRateLimit?: number | null
triggerEvents?: number | null
}
export enum DocumentProcessingPriority { export enum DocumentProcessingPriority {
standard = 'standard', standard = 'standard',
priority = 'priority', priority = 'priority',
@@ -91,10 +96,12 @@ export type CurrentPlanInfoBackend = {
api_rate_limit?: { api_rate_limit?: {
size: number size: number
limit: number // total. 0 means unlimited limit: number // total. 0 means unlimited
reset_in_days?: number
} }
trigger_events?: { trigger_events?: {
size: number size: number
limit: number // total. 0 means unlimited limit: number // total. 0 means unlimited
reset_in_days?: number
} }
docs_processing: DocumentProcessingPriority docs_processing: DocumentProcessingPriority
can_replace_logo: boolean can_replace_logo: boolean

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import type { FC } from 'react' import type { CSSProperties, FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PremiumBadge from '../../base/premium-badge' import PremiumBadge from '../../base/premium-badge'
@@ -9,19 +9,24 @@ import { useModalContext } from '@/context/modal-context'
type Props = { type Props = {
className?: string className?: string
style?: CSSProperties
isFull?: boolean isFull?: boolean
size?: 'md' | 'lg' size?: 'md' | 'lg'
isPlain?: boolean isPlain?: boolean
isShort?: boolean isShort?: boolean
onClick?: () => void onClick?: () => void
loc?: string loc?: string
labelKey?: string
} }
const UpgradeBtn: FC<Props> = ({ const UpgradeBtn: FC<Props> = ({
className,
style,
isPlain = false, isPlain = false,
isShort = false, isShort = false,
onClick: _onClick, onClick: _onClick,
loc, loc,
labelKey,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { setShowPricingModal } = useModalContext() 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) { if (isPlain) {
return ( return (
<Button onClick={onClick}> <Button
{t('billing.upgradeBtn.plain')} className={className}
style={style}
onClick={onClick}
>
{labelKey ? label : t('billing.upgradeBtn.plain')}
</Button> </Button>
) )
} }
@@ -54,11 +66,13 @@ const UpgradeBtn: FC<Props> = ({
color='blue' color='blue'
allowHover={true} allowHover={true}
onClick={onClick} 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' /> <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'> <div className='system-xs-medium'>
<span className='p-1'> <span className='p-1'>
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)} {label}
</span> </span>
</div> </div>
</PremiumBadge> </PremiumBadge>

View File

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

View File

@@ -36,5 +36,9 @@ export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE), apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE),
triggerEvents: resolveLimit(data.trigger_events?.limit, planPreset?.triggerEvents), 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 cn from '@/utils/classnames'
import { useIsChatMode } from '@/app/components/workflow/hooks' import { useIsChatMode } from '@/app/components/workflow/hooks'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' 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 FeaturesTrigger = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -50,6 +52,7 @@ const FeaturesTrigger = () => {
const appID = appDetail?.id const appID = appDetail?.id
const setAppDetail = useAppStore(s => s.setAppDetail) const setAppDetail = useAppStore(s => s.setAppDetail)
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly() const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const { plan, isFetchedPlan } = useProviderContext()
const publishedAt = useStore(s => s.publishedAt) const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished) const toolPublished = useStore(s => s.toolPublished)
@@ -95,6 +98,15 @@ const FeaturesTrigger = () => {
const hasTriggerNode = useMemo(() => ( const hasTriggerNode = useMemo(() => (
nodes.some(node => isTriggerNode(node.data.type as BlockEnum)) nodes.some(node => isTriggerNode(node.data.type as BlockEnum))
), [nodes]) ), [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 resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
const invalidateAppTriggers = useInvalidateAppTriggers() const invalidateAppTriggers = useInvalidateAppTriggers()
@@ -196,7 +208,8 @@ const FeaturesTrigger = () => {
crossAxisOffset: 4, crossAxisOffset: 4,
missingStartNode: !startNode, missingStartNode: !startNode,
hasTriggerNode, 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 dynamic from 'next/dynamic'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal' import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' 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'), { const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
ssr: false, 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'), { const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
ssr: false, ssr: false,
}) })
const TriggerEventsLimitModal = dynamic(() => import('@/app/components/billing/trigger-events-limit-modal'), {
ssr: false,
})
export type ModalState<T> = { export type ModalState<T> = {
payload: T payload: T
@@ -113,6 +122,7 @@ export type ModalContextState = {
}> | null>> }> | null>>
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>> setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>> setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
} }
const PRICING_MODAL_QUERY_PARAM = 'pricing' const PRICING_MODAL_QUERY_PARAM = 'pricing'
const PRICING_MODAL_QUERY_VALUE = 'open' const PRICING_MODAL_QUERY_VALUE = 'open'
@@ -130,6 +140,7 @@ const ModalContext = createContext<ModalContextState>({
setShowOpeningModal: noop, setShowOpeningModal: noop,
setShowUpdatePluginModal: noop, setShowUpdatePluginModal: noop,
setShowEducationExpireNoticeModal: noop, setShowEducationExpireNoticeModal: noop,
setShowTriggerEventsLimitModal: noop,
}) })
export const useModalContext = () => useContext(ModalContext) export const useModalContext = () => useContext(ModalContext)
@@ -168,6 +179,7 @@ export const ModalContextProvider = ({
}> | null>(null) }> | null>(null)
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null) const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null) const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const [showPricingModal, setShowPricingModal] = useState( const [showPricingModal, setShowPricingModal] = useState(
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE, searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
@@ -228,6 +240,17 @@ export const ModalContextProvider = ({
window.history.replaceState(null, '', url.toString()) window.history.replaceState(null, '', url.toString())
}, [showPricingModal]) }, [showPricingModal])
const { plan, isFetchedPlan } = useProviderContext()
const {
showTriggerEventsLimitModal,
setShowTriggerEventsLimitModal,
persistTriggerEventsLimitModalDismiss,
} = useTriggerEventsLimitModal({
plan,
isFetchedPlan,
currentWorkspaceId: currentWorkspace?.id,
})
const handleCancelModerationSettingModal = () => { const handleCancelModerationSettingModal = () => {
setShowModerationSettingModal(null) setShowModerationSettingModal(null)
if (showModerationSettingModal?.onCancelCallback) if (showModerationSettingModal?.onCancelCallback)
@@ -334,6 +357,7 @@ export const ModalContextProvider = ({
setShowOpeningModal, setShowOpeningModal,
setShowUpdatePluginModal, setShowUpdatePluginModal,
setShowEducationExpireNoticeModal, setShowEducationExpireNoticeModal,
setShowTriggerEventsLimitModal,
}}> }}>
<> <>
{children} {children}
@@ -455,6 +479,25 @@ export const ModalContextProvider = ({
onClose={() => setShowEducationExpireNoticeModal(null)} 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> </ModalContext.Provider>
) )

View File

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

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Nachrichtenguthaben', 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.', 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: { annotatedResponse: {
title: 'Kontingentgrenzen für Annotationen', title: 'Kontingentgrenzen für Annotationen',
@@ -83,7 +83,7 @@ const translation = {
cloud: 'Cloud-Dienst', 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.', 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', getStarted: 'Loslegen',
apiRateLimitUnit: '{{count,number}}/Monat', apiRateLimitUnit: '{{count,number}}',
documentsTooltip: 'Vorgabe für die Anzahl der Dokumente, die aus der Wissensdatenquelle importiert werden.', documentsTooltip: 'Vorgabe für die Anzahl der Dokumente, die aus der Wissensdatenquelle importiert werden.',
apiRateLimit: 'API-Datenlimit', apiRateLimit: 'API-Datenlimit',
documents: '{{count,number}} Wissensdokumente', 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.', 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', triggerEvents: 'Trigger Events',
perMonth: 'per month', perMonth: 'per month',
resetsIn: 'Resets in {{count,number}} days',
}, },
teamMembers: 'Team Members', 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: { upgradeBtn: {
plain: 'View Plan', plain: 'View Plan',
encourage: 'Upgrade Now', encourage: 'Upgrade Now',
@@ -61,11 +69,11 @@ const translation = {
documentsTooltip: 'Quota on the number of documents imported from the Knowledge Data Source.', documentsTooltip: 'Quota on the number of documents imported from the Knowledge Data Source.',
vectorSpace: '{{size}} Knowledge Data Storage', 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.', 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. ', 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', apiRateLimit: 'API Rate Limit',
apiRateLimitUnit: '{{count,number}}/month', apiRateLimitUnit: '{{count,number}}',
unlimitedApiRate: 'No API Rate Limit', 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.', 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', documentProcessingPriority: ' Document Processing',
documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.', documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
@@ -76,17 +84,19 @@ const translation = {
}, },
triggerEvents: { triggerEvents: {
sandbox: '{{count,number}} Trigger Events', sandbox: '{{count,number}} Trigger Events',
professional: '{{count,number}} Trigger Events/month', professional: '{{count,number}} Trigger Events / month',
unlimited: 'Unlimited Trigger Events', unlimited: 'Unlimited Trigger Events',
tooltip: 'The number of events that automatically start workflows through Plugin, Schedule, or Webhook triggers.',
}, },
workflowExecution: { workflowExecution: {
standard: 'Standard Workflow Execution', standard: 'Standard Workflow Execution',
faster: 'Faster Workflow Execution', faster: 'Faster Workflow Execution',
priority: 'Priority Workflow Execution', priority: 'Priority Workflow Execution',
tooltip: 'Workflow execution queue priority and speed.',
}, },
startNodes: { startNodes: {
limited: 'Up to {{count}} Start Nodes per Workflow', limited: 'Up to {{count}} Start Nodes / workflow',
unlimited: 'Unlimited Start Nodes per Workflow', unlimited: 'Unlimited Start Nodes / workflow',
}, },
logsHistory: '{{days}} Log history', logsHistory: '{{days}} Log history',
customTools: 'Custom Tools', customTools: 'Custom Tools',
@@ -115,7 +125,7 @@ const translation = {
memberAfter: 'Member', memberAfter: 'Member',
messageRequest: { messageRequest: {
title: '{{count,number}} message credits', 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.', 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: { annotatedResponse: {

View File

@@ -123,6 +123,11 @@ const translation = {
noHistory: 'No History', noHistory: 'No History',
tagBound: 'Number of apps using this tag', 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: { env: {
envPanelTitle: 'Environment Variables', 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.', 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: { messageRequest: {
title: 'Créditos de Mensajes', 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.', 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: { annotatedResponse: {
title: 'Límites de Cuota de Anotación', title: 'Límites de Cuota de Anotación',
@@ -76,7 +76,7 @@ const translation = {
priceTip: 'por espacio de trabajo/', priceTip: 'por espacio de trabajo/',
teamMember_one: '{{count, número}} Miembro del Equipo', teamMember_one: '{{count, número}} Miembro del Equipo',
getStarted: 'Comenzar', getStarted: 'Comenzar',
apiRateLimitUnit: '{{count, número}}/mes', apiRateLimitUnit: '{{count, número}}',
freeTrialTipSuffix: 'No se requiere tarjeta de crédito', freeTrialTipSuffix: 'No se requiere tarjeta de crédito',
unlimitedApiRate: 'Sin límite de tasa de API', unlimitedApiRate: 'Sin límite de tasa de API',
apiRateLimit: 'Límite de tasa de API', apiRateLimit: 'Límite de tasa de API',

View File

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

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Crédits de message', 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.', 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: { annotatedResponse: {
title: 'Limites de quota d\'annotation', 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.', 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', 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', annotationQuota: 'Quota dannotation',
apiRateLimitUnit: '{{count,number}}/mois', apiRateLimitUnit: '{{count,number}}',
priceTip: 'par espace de travail/', priceTip: 'par espace de travail/',
freeTrialTipSuffix: 'Aucune carte de crédit requise', freeTrialTipSuffix: 'Aucune carte de crédit requise',
teamWorkspace: '{{count,number}} Espace de travail d\'équipe', teamWorkspace: '{{count,number}} Espace de travail d\'équipe',

View File

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

View File

@@ -70,7 +70,7 @@ const translation = {
title: 'Crediti Messaggi', title: 'Crediti Messaggi',
tooltip: 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.', '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: { annotatedResponse: {
title: 'Limiti di Quota di Annotazione', title: 'Limiti di Quota di Annotazione',
@@ -88,7 +88,7 @@ const translation = {
freeTrialTipPrefix: 'Iscriviti e ricevi un', freeTrialTipPrefix: 'Iscriviti e ricevi un',
teamMember_one: '{{count,number}} membro del team', teamMember_one: '{{count,number}} membro del team',
documents: '{{count,number}} Documenti di Conoscenza', documents: '{{count,number}} Documenti di Conoscenza',
apiRateLimitUnit: '{{count,number}}/mese', apiRateLimitUnit: '{{count,number}}',
documentsRequestQuota: '{{count,number}}/min Limite di richiesta di conoscenza', documentsRequestQuota: '{{count,number}}/min Limite di richiesta di conoscenza',
teamMember_other: '{{count,number}} membri del team', teamMember_other: '{{count,number}} membri del team',
freeTrialTip: 'prova gratuita di 200 chiamate OpenAI.', freeTrialTip: 'prova gratuita di 200 chiamate OpenAI.',

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ const translation = {
title: 'Limity kredytów wiadomości', title: 'Limity kredytów wiadomości',
tooltip: 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.', '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: { annotatedResponse: {
title: 'Limity kredytów na adnotacje', title: 'Limity kredytów na adnotacje',
@@ -91,7 +91,7 @@ const translation = {
freeTrialTipPrefix: 'Zarejestruj się i zdobądź', freeTrialTipPrefix: 'Zarejestruj się i zdobądź',
teamMember_other: '{{count,number}} członków zespołu', teamMember_other: '{{count,number}} członków zespołu',
teamWorkspace: '{{count,number}} Zespół Workspace', teamWorkspace: '{{count,number}} Zespół Workspace',
apiRateLimitUnit: '{{count,number}}/miesiąc', apiRateLimitUnit: '{{count,number}}',
cloud: 'Usługa chmurowa', cloud: 'Usługa chmurowa',
teamMember_one: '{{count,number}} Członek zespołu', teamMember_one: '{{count,number}} Członek zespołu',
priceTip: 'na przestrzeń roboczą/', priceTip: 'na przestrzeń roboczą/',

View File

@@ -61,7 +61,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Créditos de Mensagem', 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.', 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: { annotatedResponse: {
title: 'Limites de Cota de Anotação', 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', documentsRequestQuota: '{{count,number}}/min Limite de Taxa de Solicitação de Conhecimento',
cloud: 'Serviço de Nuvem', cloud: 'Serviço de Nuvem',
teamWorkspace: '{{count,number}} Espaço de Trabalho da Equipe', 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', freeTrialTipSuffix: 'Nenhum cartão de crédito necessário',
teamMember_other: '{{count,number}} Membros da Equipe', teamMember_other: '{{count,number}} Membros da Equipe',
comparePlanAndFeatures: 'Compare planos e recursos', comparePlanAndFeatures: 'Compare planos e recursos',

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Credite de mesaje', 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.', 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: { annotatedResponse: {
title: 'Limite de cotă de anotare', 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.', documentsTooltip: 'Cota pe numărul de documente importate din Sursele de Date de Cunoștințe.',
getStarted: 'Întrebați-vă', getStarted: 'Întrebați-vă',
cloud: 'Serviciu de cloud', cloud: 'Serviciu de cloud',
apiRateLimitUnit: '{{count,number}}/lună', apiRateLimitUnit: '{{count,number}}',
comparePlanAndFeatures: 'Compară planurile și caracteristicile', comparePlanAndFeatures: 'Compară planurile și caracteristicile',
documentsRequestQuota: '{{count,number}}/min Limita de rată a cererilor de cunoștințe', documentsRequestQuota: '{{count,number}}/min Limita de rată a cererilor de cunoștințe',
documents: '{{count,number}} Documente de Cunoaștere', documents: '{{count,number}} Documente de Cunoaștere',

View File

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

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Krediti za sporočila', 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č.', 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: { annotatedResponse: {
title: 'Omejitve kvote za označevanje', title: 'Omejitve kvote za označevanje',
@@ -86,7 +86,7 @@ const translation = {
teamMember_one: '{{count,number}} član ekipe', teamMember_one: '{{count,number}} član ekipe',
teamMember_other: '{{count,number}} Članov ekipe', teamMember_other: '{{count,number}} Članov ekipe',
documentsRequestQuota: '{{count,number}}/min Omejitev stopnje zahtev po znanju', documentsRequestQuota: '{{count,number}}/min Omejitev stopnje zahtev po znanju',
apiRateLimitUnit: '{{count,number}}/mesec', apiRateLimitUnit: '{{count,number}}',
priceTip: 'na delovnem prostoru/', priceTip: 'na delovnem prostoru/',
freeTrialTipPrefix: 'Prijavite se in prejmite', freeTrialTipPrefix: 'Prijavite se in prejmite',
cloud: 'Oblačna storitev', cloud: 'Oblačna storitev',

View File

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

View File

@@ -65,7 +65,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Mesaj Kredileri', 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.', 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: { annotatedResponse: {
title: 'Ek Açıklama Kota Sınırları', title: 'Ek Açıklama Kota Sınırları',
@@ -78,7 +78,7 @@ const translation = {
freeTrialTipPrefix: 'Kaydolun ve bir', freeTrialTipPrefix: 'Kaydolun ve bir',
priceTip: 'iş alanı başına/', priceTip: 'iş alanı başına/',
documentsRequestQuota: '{{count,number}}/dakika Bilgi İsteği Oran Limiti', documentsRequestQuota: '{{count,number}}/dakika Bilgi İsteği Oran Limiti',
apiRateLimitUnit: '{{count,number}}/ay', apiRateLimitUnit: '{{count,number}}',
documents: '{{count,number}} Bilgi Belgesi', documents: '{{count,number}} Bilgi Belgesi',
comparePlanAndFeatures: 'Planları ve özellikleri karşılaştır', comparePlanAndFeatures: 'Planları ve özellikleri karşılaştır',
self: 'Kendi Barındırılan', self: 'Kendi Barındırılan',

View File

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

View File

@@ -64,7 +64,7 @@ const translation = {
messageRequest: { messageRequest: {
title: 'Số Lượng Tin Nhắn', 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.', 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: { annotatedResponse: {
title: 'Hạn Mức Quota Phản hồi Đã được Ghi chú', 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', teamMember_other: '{{count,number}} thành viên trong nhóm',
documents: '{{count,number}} Tài liệu Kiến thức', documents: '{{count,number}} Tài liệu Kiến thức',
getStarted: 'Bắt đầu', getStarted: 'Bắt đầu',
apiRateLimitUnit: '{{count,number}}/tháng', apiRateLimitUnit: '{{count,number}}',
freeTrialTipSuffix: 'Không cần thẻ tín dụng', 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.', 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', startBuilding: 'Bắt đầu xây dựng',

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,3 +10,10 @@ export const isAfter = (date: ConfigType, compare: ConfigType) => {
export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => { export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => {
return dayjs(date).format(dateFormat) 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)
}