Compare commits

..
Author SHA1 Message Date
CodingOnStar dacd480c6f Update Amplitude integration and dependencies
- Replaced @amplitude/unified with @amplitude/analytics-browser and added @amplitude/plugin-session-replay-browser for enhanced tracking capabilities.
- Modified AmplitudeProvider to include session replay functionality with a configurable sample rate.
- Updated user identification in AppContextProvider to use user email instead of ID for tracking.
- Bumped various dependencies in package.json and pnpm-lock.yaml for compatibility and improvements.
2025-11-25 13:49:13 +08:00
CodingOnStar d960d3ee3e Add Amplitude analytics integration and tracking (#27890)
- Introduced AmplitudeProvider component for initializing Amplitude analytics.
- Integrated user tracking in AppContextProvider to report user and workspace information.
- Added event tracking for workflow log filter selections.
- Updated package.json and pnpm-lock.yaml to include @amplitude/unified dependency.
2025-11-14 14:45:36 +08:00
lyzno1andCodingOnStar 898e65d645 fix: card view render (#28189) 2025-11-14 14:43:41 +08:00
lyzno1andCodingOnStar 818966e53f feat: add icon gallery stories (#28214)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
2025-11-14 14:43:41 +08:00
非法操作andCodingOnStar e285917e37 fix: knowledge pipeline can not published (#28203) 2025-11-14 14:43:41 +08:00
Anubhav SinghandCodingOnStar d18b2c0e82 Upgrade weave version to fix weave configuration failure (#28197) 2025-11-14 14:43:41 +08:00
MariesCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
d205b4c10e chore: bump to 1.10.0 (#28186)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-14 14:43:41 +08:00
yangzheliandCodingOnStar 4bd114a90a refactor(web): remove redundant add-tool-modal components and related code (#27996) 2025-11-14 14:43:41 +08:00
hofferandCodingOnStar 84a9ddc2d8 fix: StreamableHTTPTransport got invalid json exception when receive a ping event from mcp server #28111 (#28116) 2025-11-14 14:43:41 +08:00
Asuka MinatoCodingOnStargemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Copilot
e658a6a80d TypedBase + TypedDict (#28137)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-14 14:43:41 +08:00
NeatGuyCodingandCodingOnStar 6f93e2f780 minor fix(rag): return early when pushing empty tasks to avoid Redis DataError (#28027)
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
2025-11-14 14:43:41 +08:00
NeatGuyCodingandCodingOnStar 37fc386af2 fix(web): fix unit promotion in formatNumberAbbreviated (#27918)
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
2025-11-14 14:43:41 +08:00
GuanMuandCodingOnStar 9aa9c87e0d fix: adjust padding in AgentNode and NodeComponent for consistent layout (#28175) 2025-11-14 14:43:41 +08:00
GuanMuandCodingOnStar 56e9129efb fix: update start-worker alias to include additional queues for bette… (#28179) 2025-11-14 14:43:41 +08:00
GuanMuandCodingOnStar 1e2311128c fix: update package manager version to 10.22.0 (#28181) 2025-11-14 14:43:41 +08:00
zhsamaandCodingOnStar 08edf20672 feat: enhance start node metadata to be undeletable in chat mode (#28173) 2025-11-14 14:43:40 +08:00
yihongCodingOnStarCopilotautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>crazywoola
28626590d1 fix: skip tests if no database run (#28102)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
李龙飞CodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>crazywoola
8dc5d98174 Fix: Correctly handle merged cells in DOCX tables to prevent content duplication and loss (#27871)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
zhsamaCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
9c10731494 feat: add draft trigger detection to app model and UI (#28163)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
breath57andCodingOnStar eaed72a40c fix(knowledge-base): regenerate child chunks not working completely (#27934) 2025-11-14 14:43:40 +08:00
JoelandCodingOnStar 168a1524b2 fix: can still invite when api is pending (#28161) 2025-11-14 14:43:40 +08:00
8e01eb4561 fix(moderation): change OpenAI moderation model to omni-moderation-la… (#28119)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
96865ebc8c fix: fixed error when clear value of INTEGER and FLOAT type (#27954)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
7ca5e8812e fix: missing pipeline_templates.json when HOSTED_FETCH_PIPELINE_TEMPLATES_MODE is builtin (#27946)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
katakyoandCodingOnStar 201fa723a5 fix: update input field width for retry configuration in RetryOnPanel (#28142) 2025-11-14 14:43:40 +08:00
ye4241andCodingOnStar 32b5ae3b71 fix: correct spelling of "模板" in translation files (#28151) 2025-11-14 14:43:40 +08:00
非法操作andCodingOnStar f3bfccbd67 fix: inconsistent behaviour of zoom in button and shortcut (#27944) 2025-11-14 14:43:40 +08:00
Asuka MinatoCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
a4e3a39dd2 add doc (#28016)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
yangzheliandCodingOnStar 9e753b031a refactor(web): reuse the same edit-custom-collection-modal component, and fix the pop up error (#28003) 2025-11-14 14:43:40 +08:00
mnasrautinnoandCodingOnStar 15f3d68bc5 fix: app's ai site text to speech api (#28091) 2025-11-14 14:43:40 +08:00
MariesCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
b9202d1e0a fix: simplify graph structure validation in WorkflowService (#28146)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
-LAN-andCodingOnStar a6e9da1d89 chore: disable workflow logs auto-cleanup by default (#28136)
This PR changes the default value of `WORKFLOW_LOG_CLEANUP_ENABLED` from `true` to `false` across all configuration files.

## Motivation

Setting the default to `false` provides safer default behavior by:

- Preventing unintended data loss for new installations
- Giving users explicit control over when to enable log cleanup
- Following the opt-in principle for data deletion features

Users who need automatic cleanup can enable it by setting `WORKFLOW_LOG_CLEANUP_ENABLED=true` in their configuration.
2025-11-14 14:43:40 +08:00
Asuka MinatoCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Copilot
125f37b562 convert to TypeBase (#27935)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-14 14:43:40 +08:00
Gritty_devandCodingOnStar eea3f14777 test: create some hooks and utils test script, modified clipboard test script (#27928) 2025-11-14 14:43:40 +08:00
Gen SatoandCodingOnStar 0e43f5ad4d Add file type validation to paste upload (#28017) 2025-11-14 14:43:40 +08:00
非法操作andCodingOnStar e3f85c1658 fix: variable assigner can't assign float number (#28068) 2025-11-14 14:43:40 +08:00
Bowen LiangandCodingOnStar 10571fcf7c fix: determine cpu cores determination in baseedpyright-check script on macos (#28058) 2025-11-14 14:43:39 +08:00
+4
YeuolyCodingOnStarStreamlyzno1zhsamaHarrylyzno1yesseniahjlarrygemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>WTW0313Copilot
244bd08db2 feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-14 14:43:37 +08:00
JyongCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>Garfield Dai
73ef99c25a add transform-datasource-credentials command online check (#28124)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
2025-11-14 14:43:13 +08:00
QuantumGhostCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
d5a30c667b feat(api): Introduce WorkflowResumptionContext for pause state management (#28122)
Certain metadata (including but not limited to `InvokeFrom`, `call_depth`, and `streaming`)  is required when resuming a paused workflow. However, these fields are not part of `GraphRuntimeState` and were not saved in the previous
 implementation of  `PauseStatePersistenceLayer`.

This commit addresses this limitation by introducing a `WorkflowResumptionContext` model that wraps both the `*GenerateEntity` and `GraphRuntimeState`. This approach provides:

- A structured container for all necessary resumption data
- Better separation of concerns between execution state and persistence
- Enhanced extensibility for future metadata additions
- Clearer naming that distinguishes from `GraphRuntimeState`

The `WorkflowResumptionContext` model makes extending the pause state easier while maintaining backward compatibility and proper version management for the entire execution state ecosystem.

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-14 14:43:13 +08:00
JoelandCodingOnStar 9648020ab4 chore: improve the user experience of not login into apps (#28120) 2025-11-14 14:43:13 +08:00
JyongandCodingOnStar 76be5c054b fix document index test (#28113) 2025-11-14 14:43:13 +08:00
湛露先生CodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>-LAN-
c76f370f97 When graph_engine worker run exception, keep the node_id for deep res… (#26205)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2025-11-14 14:43:13 +08:00
JoelandCodingOnStar 853f5ff09e chore: not SaaS version can query long log time range (#28109) 2025-11-14 14:43:13 +08:00
JyongandCodingOnStar 2ee55d8bc8 fix document enable (#28081) 2025-11-14 14:43:12 +08:00
lyzno1andCodingOnStar 57a7e3c20f chore: add type-check to pre-commit (#28005) 2025-11-14 14:43:12 +08:00
3160242fca fix(api): Trace Hierarchy, Span Status, and Broken Workflow for Arize & Phoenix Integration (#27937)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
crazywoolaCodingOnStarCopilotgemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
001e4f8c69 chore: remove unused enterprise bot from the readme (#28073)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
48c95f4ccf chore: translate i18n files and update type definitions (#28054)
Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
WillandCodingOnStar 1418512f24 chore: Remove trailing space from migration filename (#28040) 2025-11-14 14:43:12 +08:00
JoelCodingOnStargemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Copilot
f834f8a160 Chore: change query log time range (#28052)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
QuantumGhostCodingOnStargemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Copilotautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
feaad67531 feat(api): Introduce Broadcast Channel (#27835)
This PR introduces a `BroadcastChannel` abstraction with broadcasting and at-most once delivery semantics, serving as the communication component between celery worker and API server.

It also includes a reference implementation backed by Redis PubSub.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
WillandCodingOnStar 9592b72aa3 fix workflow default updated_at (#28047) 2025-11-14 14:43:12 +08:00
huangzhuo1949CodingOnStarhuangzhuoautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>crazywoolagemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
6325ed9cd3 fix: elasticsearch_vector version (#28028)
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
WillandCodingOnStar ab39f39db0 feat: Add Audio Content Support for MCP Tools (#27979) 2025-11-14 14:43:12 +08:00
NeatGuyCodingCodingOnStargemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
becabd84a9 Fix typo in weaviate comment, improve time test precision, and add security tests for get-icon utility (#27919)
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
dependabot[bot]CodingOnStardependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
62de0e97e5 chore(deps): bump scipy-stubs from 1.16.2.3 to 1.16.3.0 in /api (#28025)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
Asuka MinatoCodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
aa0fdbf759 add onupdate=func.current_timestamp() (#28014)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-14 14:43:12 +08:00
c3b48cca11 fix: the model list encountered two children with the same key (#27956)
Co-authored-by: haokai <haokai@shuwen.com>
2025-11-14 14:43:12 +08:00
wangxiaoleiandCodingOnStar 874ee69f2b fix: fix https://github.com/langgenius/dify/issues/27939 (#27985) 2025-11-14 14:43:12 +08:00
OneZero-YandCodingOnStar 8f499d9894 docs: clarify how to obtain workflow_id for version execution (#28007)
Signed-off-by: OneZero-Y <aukovyps@163.com>
2025-11-14 14:43:12 +08:00
kenwoodjwandCodingOnStar f16a42ee52 fix: bump brotli to 1.2.0 resloved CVE-2025-6176 (#27950)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-11-14 14:43:12 +08:00
hj24CodingOnStarautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>-LAN-
9b43b9f1e9 refactor: implement tenant self queue for rag tasks (#27559)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2025-11-14 14:43:12 +08:00
f8e01d7df8 fix(api): fix VariablePool.get adding unexpected keys to variable_dictionary (#26767)
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-14 14:43:11 +08:00
NFishandCodingOnStar ead5a78faa fix: prevent fetch version info in enterprise edition (#27923) 2025-11-14 14:43:10 +08:00
JyongandCodingOnStar fd2ff963a9 fix jina reader transform (#27922) 2025-11-14 14:42:39 +08:00
CodingOnStar 7492b39ff0 feat: add registration and create_app tracking 2025-11-14 14:40:53 +08:00
CodingOnStar 8b5533e1a4 Enhance Amplitude tracking by validating user ID and properties
- Updated middleware to include Amplitude domains.
- Improved setUserId function to handle empty user IDs and log warnings in development.
- Enhanced setUserProperties function to filter out invalid properties and log results in development.
- Refactored user tracking in AppContextProvider to streamline setting user and workspace properties.
2025-11-07 11:22:26 +08:00
CodingOnStar fa7a0fd3d1 Implement event tracking for app creation and user actions across various components 2025-11-07 11:11:45 +08:00
CodingOnStar 2a20a96123 Add Amplitude analytics integration and tracking (#27890)
- Introduced AmplitudeProvider component for initializing Amplitude analytics.
- Integrated user tracking in AppContextProvider to report user and workspace information.
- Added event tracking for workflow log filter selections.
- Updated package.json and pnpm-lock.yaml to include @amplitude/unified dependency.
2025-11-07 10:52:50 +08:00
84 changed files with 2204 additions and 5542 deletions
-78
View File
@@ -38,12 +38,6 @@ class EmailType(StrEnum):
EMAIL_REGISTER = auto()
EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = auto()
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = auto()
TRIGGER_EVENTS_LIMIT_SANDBOX = auto()
TRIGGER_EVENTS_LIMIT_PROFESSIONAL = auto()
TRIGGER_EVENTS_USAGE_WARNING_SANDBOX = auto()
TRIGGER_EVENTS_USAGE_WARNING_PROFESSIONAL = auto()
API_RATE_LIMIT_LIMIT_SANDBOX = auto()
API_RATE_LIMIT_WARNING_SANDBOX = auto()
class EmailLanguage(StrEnum):
@@ -451,78 +445,6 @@ def create_default_email_config() -> EmailI18nConfig:
branded_template_path="clean_document_job_mail_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_LIMIT_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youve reached your Sandbox Trigger Events limit",
template_path="trigger_events_limit_template_en-US.html",
branded_template_path="without-brand/trigger_events_limit_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 Sandbox 触发事件额度已用尽",
template_path="trigger_events_limit_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_limit_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_LIMIT_PROFESSIONAL: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youve reached your monthly Trigger Events limit",
template_path="trigger_events_limit_template_en-US.html",
branded_template_path="without-brand/trigger_events_limit_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的月度触发事件额度已用尽",
template_path="trigger_events_limit_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_limit_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_USAGE_WARNING_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youre nearing your Sandbox Trigger Events limit",
template_path="trigger_events_usage_warning_template_en-US.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 Sandbox 触发事件额度接近上限",
template_path="trigger_events_usage_warning_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_zh-CN.html",
),
},
EmailType.TRIGGER_EVENTS_USAGE_WARNING_PROFESSIONAL: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youre nearing your Monthly Trigger Events limit",
template_path="trigger_events_usage_warning_template_en-US.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的月度触发事件额度接近上限",
template_path="trigger_events_usage_warning_template_zh-CN.html",
branded_template_path="without-brand/trigger_events_usage_warning_template_zh-CN.html",
),
},
EmailType.API_RATE_LIMIT_LIMIT_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youve reached your API Rate Limit",
template_path="api_rate_limit_limit_template_en-US.html",
branded_template_path="without-brand/api_rate_limit_limit_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 API 速率额度已用尽",
template_path="api_rate_limit_limit_template_zh-CN.html",
branded_template_path="without-brand/api_rate_limit_limit_template_zh-CN.html",
),
},
EmailType.API_RATE_LIMIT_WARNING_SANDBOX: {
EmailLanguage.EN_US: EmailTemplate(
subject="Youre nearing your API Rate Limit",
template_path="api_rate_limit_warning_template_en-US.html",
branded_template_path="without-brand/api_rate_limit_warning_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的 API 速率额度接近上限",
template_path="api_rate_limit_warning_template_zh-CN.html",
branded_template_path="without-brand/api_rate_limit_warning_template_zh-CN.html",
),
},
EmailType.EMAIL_REGISTER: {
EmailLanguage.EN_US: EmailTemplate(
subject="Register Your {application_title} Account",
@@ -1,178 +0,0 @@
<!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>
@@ -1,177 +0,0 @@
<!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>
@@ -1,179 +0,0 @@
<!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>
@@ -1,177 +0,0 @@
<!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>
@@ -1,184 +0,0 @@
<!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>
@@ -1,184 +0,0 @@
<!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>
@@ -1,185 +0,0 @@
<!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>
@@ -1,184 +0,0 @@
<!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>
@@ -1,173 +0,0 @@
<!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>
@@ -1,172 +0,0 @@
<!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>
@@ -1,174 +0,0 @@
<!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>
@@ -1,172 +0,0 @@
<!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>
@@ -1,179 +0,0 @@
<!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>
@@ -1,179 +0,0 @@
<!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>
@@ -1,180 +0,0 @@
<!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>
@@ -1,179 +0,0 @@
<!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>
@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useMemo } from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/app-card'
@@ -24,7 +24,6 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppWorkflow } from '@/service/use-workflow'
import type { BlockEnum } from '@/app/components/workflow/types'
import { isTriggerNode } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
export type ICardViewProps = {
appId: string
@@ -34,7 +33,6 @@ export type ICardViewProps = {
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
@@ -55,35 +53,6 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
})
}, [isWorkflowApp, currentWorkflow])
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
const disableAppCards = !shouldRenderAppCards
const triggerDocUrl = docLink('/guides/workflow/node/start')
const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className='flex flex-col gap-1'>
<div className='text-xs text-text-secondary'>
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
</div>
<div
className='cursor-pointer text-xs font-medium text-text-accent hover:underline'
onClick={(event) => {
event.stopPropagation()
window.open(triggerDocUrl, '_blank')
}}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</div>
), [t, triggerDocUrl])
const disableWebAppTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
: null
const disableApiTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
: null
const disableMcpTooltip = disableAppCards
? buildTriggerModeMessage(t('tools.mcp.server.title'))
: null
const updateAppDetail = async () => {
try {
@@ -155,48 +124,39 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
if (!appDetail)
return <Loading />
const appCards = (
<>
<AppCard
appInfo={appDetail}
cardType="webapp"
isInPanel={isInPanel}
triggerModeDisabled={disableAppCards}
triggerModeMessage={disableWebAppTooltip}
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig}
/>
<AppCard
cardType="api"
appInfo={appDetail}
isInPanel={isInPanel}
triggerModeDisabled={disableAppCards}
triggerModeMessage={disableApiTooltip}
onChangeStatus={onChangeApiStatus}
/>
{showMCPCard && (
<MCPServiceCard
appInfo={appDetail}
triggerModeDisabled={disableAppCards}
triggerModeMessage={disableMcpTooltip}
/>
)}
</>
)
const triggerCardNode = showTriggerCard ? (
<TriggerCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
/>
) : null
return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
{disableAppCards && triggerCardNode}
{appCards}
{!disableAppCards && triggerCardNode}
{
shouldRenderAppCards && (
<>
<AppCard
appInfo={appDetail}
cardType="webapp"
isInPanel={isInPanel}
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig}
/>
<AppCard
cardType="api"
appInfo={appDetail}
isInPanel={isInPanel}
onChangeStatus={onChangeApiStatus}
/>
{showMCPCard && (
<MCPServiceCard
appInfo={appDetail}
/>
)}
</>
)
}
{showTriggerCard && (
<TriggerCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
/>
)}
</div>
)
}
+2
View File
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
import SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import AmplitudeProvider from '@/app/components/base/amplitude'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import Header from '@/app/components/header'
import { EventEmitterContextProvider } from '@/context/event-emitter'
@@ -17,6 +18,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
@@ -4,6 +4,7 @@ import Header from './header'
import SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import AmplitudeProvider from '@/app/components/base/amplitude'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
@@ -13,6 +14,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitor>
<AppContextProvider>
<EventEmitterContextProvider>
+22 -52
View File
@@ -49,7 +49,6 @@ import { fetchInstalledAppList } from '@/service/explore'
import { AppModeEnum } from '@/types/app'
import type { PublishWorkflowParams } from '@/types/workflow'
import { basePath } from '@/utils/var'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
[AccessMode.ORGANIZATION]: {
@@ -107,7 +106,6 @@ export type AppPublisherProps = {
workflowToolAvailable?: boolean
missingStartNode?: boolean
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist).
startNodeLimitExceeded?: boolean
}
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
@@ -129,7 +127,6 @@ const AppPublisher = ({
workflowToolAvailable = true,
missingStartNode = false,
hasTriggerNode = false,
startNodeLimitExceeded = false,
}: AppPublisherProps) => {
const { t } = useTranslation()
@@ -249,13 +246,6 @@ const AppPublisher = ({
const hasPublishedVersion = !!publishedAt
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined
const showStartNodeLimitHint = Boolean(startNodeLimitExceeded)
const upgradeHighlightStyle = useMemo(() => ({
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}), [])
return (
<>
@@ -314,49 +304,29 @@ const AppPublisher = ({
/>
)
: (
<>
<Button
variant='primary'
className='mt-3 w-full'
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{
published
? t('workflow.common.published')
: (
<div className='flex gap-1'>
<span>{t('workflow.common.publishUpdate')}</span>
<div className='flex gap-0.5'>
{PUBLISH_SHORTCUT.map(key => (
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
{getKeyboardKeyNameBySystem(key)}
</span>
))}
</div>
<Button
variant='primary'
className='mt-3 w-full'
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{
published
? t('workflow.common.published')
: (
<div className='flex gap-1'>
<span>{t('workflow.common.publishUpdate')}</span>
<div className='flex gap-0.5'>
{PUBLISH_SHORTCUT.map(key => (
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
{getKeyboardKeyNameBySystem(key)}
</span>
))}
</div>
)
}
</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>
)
}
</Button>
)
}
</div>
@@ -28,6 +28,7 @@ import Input from '@/app/components/base/input'
import { AppModeEnum } from '@/types/app'
import { DSLImportMode } from '@/models/app'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { trackEvent } from '@/app/components/base/amplitude'
type AppsProps = {
onSuccess?: () => void
@@ -141,6 +142,16 @@ const Apps = ({
icon_background,
description,
})
// Track app creation from template
trackEvent('create_app_with_template', {
app_mode: mode,
template_id: currApp?.app.id,
template_name: currApp?.app.name,
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
has_description: description,
})
setIsShowCreateModal(false)
Toast.notify({
type: 'success',
@@ -30,6 +30,7 @@ import { getRedirection } from '@/utils/app-redirection'
import FullScreenModal from '@/app/components/base/fullscreen-modal'
import useTheme from '@/hooks/use-theme'
import { useDocLink } from '@/context/i18n'
import { trackEvent } from '@/app/components/base/amplitude'
type CreateAppProps = {
onSuccess: () => void
@@ -82,6 +83,14 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
mode: appMode,
})
// Track app creation success
trackEvent('create_app', {
app_mode: appMode,
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
description,
})
notify({ type: 'success', message: t('app.newApp.appCreated') })
onSuccess()
onClose()
@@ -28,6 +28,7 @@ import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { noop } from 'lodash-es'
import { trackEvent } from '@/app/components/base/amplitude'
type CreateFromDSLModalProps = {
show: boolean
@@ -112,6 +113,13 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
return
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
// Track app creation from DSL import
trackEvent('create_app_with_dsl', {
app_mode,
creation_method: currentTab === CreateFromDSLModalTab.FROM_FILE ? 'dsl_file' : 'dsl_url',
has_warnings: status === DSLImportStatus.COMPLETED_WITH_WARNINGS,
})
if (onSuccess)
onSuccess()
if (onClose)
+9 -12
View File
@@ -42,7 +42,6 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload
import cn from '@/utils/classnames'
import { noop } from 'lodash-es'
import PromptLogModal from '../../base/prompt-log-modal'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
type AppStoreState = ReturnType<typeof useAppStore.getState>
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
@@ -780,17 +779,15 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
}
</div>
{showMessageLogModal && (
<WorkflowContextProvider>
<MessageLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
defaultTab={currentLogModalActiveTab}
/>
</WorkflowContextProvider>
<MessageLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
defaultTab={currentLogModalActiveTab}
/>
)}
{!isChatMode && showPromptLogModal && (
<PromptLogModal
+17 -41
View File
@@ -51,8 +51,6 @@ export type IAppCardProps = {
isInPanel?: boolean
cardType?: 'api' | 'webapp'
customBgColor?: string
triggerModeDisabled?: boolean // true when Trigger Node mode needs UI locked to avoid conflicting actions
triggerModeMessage?: React.ReactNode // contextual copy explaining why the card is disabled in trigger mode
onChangeStatus: (val: boolean) => Promise<void>
onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
onGenerateCode?: () => Promise<void>
@@ -63,8 +61,6 @@ function AppCard({
isInPanel,
cardType = 'webapp',
customBgColor,
triggerModeDisabled = false,
triggerModeMessage = '',
onChangeStatus,
onSaveSiteConfig,
onGenerateCode,
@@ -115,7 +111,7 @@ function AppCard({
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
const missingStartNode = isWorkflowApp && !hasStartNode
const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode
const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api)
const isMinimalState = appUnpublished || missingStartNode
const { app_base_url, access_token } = appInfo.site ?? {}
@@ -193,20 +189,7 @@ function AppCard({
className={
`${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`}
>
<div className={`${customBgColor ?? 'bg-background-default'} relative rounded-xl ${triggerModeDisabled ? 'opacity-60' : ''}`}>
{triggerModeDisabled && (
triggerModeMessage
? (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
</Tooltip>
)
: <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
)}
<div className={`${customBgColor ?? 'bg-background-default'} rounded-xl`}>
<div className={`flex w-full flex-col items-start justify-center gap-3 self-stretch p-3 ${isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle'}`}>
<div className='flex w-full items-center gap-3 self-stretch'>
<AppBasic
@@ -231,23 +214,18 @@ function AppCard({
</div>
<Tooltip
popupContent={
toggleDisabled ? (
triggerModeDisabled && triggerModeMessage
? triggerModeMessage
: (appUnpublished || missingStartNode) ? (
<>
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('appOverview.overview.appInfo.enableTooltip.description')}
</div>
<div
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</>
)
: ''
toggleDisabled && (appUnpublished || missingStartNode) ? (
<>
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('appOverview.overview.appInfo.enableTooltip.description')}
</div>
<div
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</>
) : ''
}
position="right"
@@ -351,11 +329,9 @@ function AppCard({
{!isApp && <SecretKeyButton appId={appInfo.id} />}
{OPERATIONS_MAP[cardType].map((op) => {
const disabled
= triggerModeDisabled
? true
: op.opName === t('appOverview.overview.appInfo.settings.entry')
? false
: !runningStatus
= op.opName === t('appOverview.overview.appInfo.settings.entry')
? false
: !runningStatus
return (
<Button
className="mr-1 min-w-[88px]"
@@ -8,6 +8,7 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import type { QueryParam } from './index'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'
import { trackEvent } from '../../base/amplitude/utils'
dayjs.extend(quarterOfYear)
const today = dayjs()
@@ -37,6 +38,9 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
value={queryParams.status || 'all'}
onSelect={(item) => {
setQueryParams({ ...queryParams, status: item.value as string })
trackEvent('workflow_log_filter_status_selected', {
workflow_log_filter_status: item.value as string,
})
}}
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
items={[{ value: 'all', name: 'All' },
+1 -4
View File
@@ -1,6 +1,6 @@
import React from 'react'
import Link from 'next/link'
import { RiDiscordFill, RiDiscussLine, RiGithubFill } from '@remixicon/react'
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
type CustomLinkProps = {
@@ -38,9 +38,6 @@ const Footer = () => {
<CustomLink href='https://discord.gg/FngNHpbcY7'>
<RiDiscordFill className='h-5 w-5 text-text-tertiary' />
</CustomLink>
<CustomLink href='https://forum.dify.ai'>
<RiDiscussLine className='h-5 w-5 text-text-tertiary' />
</CustomLink>
</div>
</footer>
)
@@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
export type IAmplitudeProps = {
apiKey?: string
sessionReplaySampleRate?: number
}
const AmplitudeProvider: FC<IAmplitudeProps> = ({
apiKey = '702e89332ab88a7f14e665f417244e9d',
sessionReplaySampleRate = 1,
}) => {
useEffect(() => {
// // Only enable in non-CE edition
// if (IS_CE_EDITION) {
// console.warn('[Amplitude] Amplitude is disabled in CE edition')
// return
// }
// Initialize Amplitude
amplitude.init(apiKey, {
defaultTracking: {
sessions: true,
pageViews: true,
formInteractions: true,
fileDownloads: true,
},
// Enable debug logs in development environment
logLevel: amplitude.Types.LogLevel.Warn,
})
// Add Session Replay plugin
const sessionReplay = sessionReplayPlugin({
sampleRate: sessionReplaySampleRate,
})
amplitude.add(sessionReplay)
}, [apiKey, sessionReplaySampleRate])
// This is a client component that renders nothing
return null
}
export default React.memo(AmplitudeProvider)
@@ -0,0 +1,2 @@
export { default } from './AmplitudeProvider'
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'
@@ -0,0 +1,37 @@
import * as amplitude from '@amplitude/analytics-browser'
/**
* Track custom event
* @param eventName Event name
* @param eventProperties Event properties (optional)
*/
export const trackEvent = (eventName: string, eventProperties?: Record<string, any>) => {
amplitude.track(eventName, eventProperties)
}
/**
* Set user ID
* @param userId User ID
*/
export const setUserId = (userId: string) => {
amplitude.setUserId(userId)
}
/**
* Set user properties
* @param properties User properties
*/
export const setUserProperties = (properties: Record<string, any>) => {
const identifyEvent = new amplitude.Identify()
Object.entries(properties).forEach(([key, value]) => {
identifyEvent.set(key, value)
})
amplitude.identify(identifyEvent)
}
/**
* Reset user (e.g., when user logs out)
*/
export const resetUser = () => {
amplitude.reset()
}
@@ -6,7 +6,6 @@ import { useStore } from '@/app/components/app/store'
import type { WorkflowRunDetailResponse } from '@/models/log'
import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow'
import { BlockEnum } from '@/app/components/workflow/types'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
const SAMPLE_APP_DETAIL = {
id: 'app-demo-1',
@@ -144,12 +143,10 @@ const MessageLogPreview = (props: MessageLogModalProps) => {
return (
<div className="relative min-h-[640px] w-full bg-background-default-subtle p-6">
<WorkflowContextProvider>
<MessageLogModal
{...props}
currentLogItem={mockCurrentLogItem}
/>
</WorkflowContextProvider>
<MessageLogModal
{...props}
currentLogItem={mockCurrentLogItem}
/>
</div>
)
}
-4
View File
@@ -90,8 +90,4 @@ export const defaultPlan = {
apiRateLimit: ALL_PLANS.sandbox.apiRateLimit,
triggerEvents: ALL_PLANS.sandbox.triggerEvents,
},
reset: {
apiRateLimit: null,
triggerEvents: null,
},
}
+9 -22
View File
@@ -6,16 +6,15 @@ import { useRouter } from 'next/navigation'
import {
RiBook2Line,
RiFileEditLine,
RiFlashlightLine,
RiGraduationCapLine,
RiGroupLine,
RiSpeedLine,
} from '@remixicon/react'
import { Plan, SelfHostedPlan } from '../type'
import { NUM_INFINITE } from '../config'
import { getDaysUntilEndOfMonth } from '@/utils/time'
import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn'
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import Button from '@/app/components/base/button'
@@ -45,20 +44,9 @@ const PlanComp: FC<Props> = ({
const {
usage,
total,
reset,
} = plan
const triggerEventsResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
? reset.triggerEvents ?? undefined
: undefined
const apiRateLimitResetInDays = (() => {
if (total.apiRateLimit === NUM_INFINITE)
return undefined
if (typeof reset.apiRateLimit === 'number')
return reset.apiRateLimit
if (type === Plan.sandbox)
return getDaysUntilEndOfMonth()
return undefined
})()
const perMonthUnit = ` ${t('billing.usagePage.perMonth')}`
const triggerEventUnit = plan.type === Plan.sandbox ? undefined : perMonthUnit
const [showModal, setShowModal] = React.useState(false)
const { mutateAsync } = useEducationVerify()
@@ -91,6 +79,7 @@ const PlanComp: FC<Props> = ({
<div className='grow'>
<div className='mb-1 flex items-center gap-1'>
<div className='system-md-semibold-uppercase text-text-primary'>{t(`billing.plans.${type}.name`)}</div>
<div className='system-2xs-medium-uppercase rounded-[5px] border border-divider-deep px-1 py-0.5 text-text-tertiary'>{t('billing.currentPlan')}</div>
</div>
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
@@ -135,20 +124,18 @@ const PlanComp: FC<Props> = ({
total={total.annotatedResponse}
/>
<UsageInfo
Icon={TriggerAll}
Icon={RiFlashlightLine}
name={t('billing.usagePage.triggerEvents')}
usage={usage.triggerEvents}
total={total.triggerEvents}
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
resetInDays={triggerEventsResetInDays}
unit={triggerEventUnit}
/>
<UsageInfo
Icon={ApiAggregate}
Icon={RiSpeedLine}
name={t('billing.plansCommon.apiRateLimit')}
usage={usage.apiRateLimit}
total={total.apiRateLimit}
tooltip={total.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
resetInDays={apiRateLimitResetInDays}
unit={perMonthUnit}
/>
</div>
@@ -46,10 +46,16 @@ const List = ({
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
/>
<Item
label={
planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}`
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
/>
<Item
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
/>
<Divider bgStyle='gradient' />
<Item
label={
planInfo.triggerEvents === NUM_INFINITE
@@ -58,14 +64,6 @@ const List = ({
? t('billing.plansCommon.triggerEvents.sandbox', { count: planInfo.triggerEvents })
: t('billing.plansCommon.triggerEvents.professional', { count: planInfo.triggerEvents })
}
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
/>
<Item
label={
plan === Plan.sandbox
? t('billing.plansCommon.startNodes.limited', { count: 2 })
: t('billing.plansCommon.startNodes.unlimited')
}
/>
<Item
label={
@@ -75,7 +73,13 @@ const List = ({
? t('billing.plansCommon.workflowExecution.faster')
: 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' />
<Item
@@ -85,14 +89,6 @@ const List = ({
<Item
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
/>
<Item
label={
planInfo.apiRateLimit === NUM_INFINITE
? t('billing.plansCommon.unlimitedApiRate')
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')} / ${t('billing.plansCommon.month')}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
/>
<Divider bgStyle='gradient' />
<Item
label={t('billing.plansCommon.modelProviders')}
@@ -1,30 +0,0 @@
.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;
}
@@ -1,97 +0,0 @@
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,
},
}
@@ -1,90 +0,0 @@
'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)
-7
View File
@@ -55,11 +55,6 @@ export type SelfHostedPlanInfo = {
export type UsagePlanInfo = Pick<PlanInfo, 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota' | 'apiRateLimit' | 'triggerEvents'> & { vectorSpace: number }
export type UsageResetInfo = {
apiRateLimit?: number | null
triggerEvents?: number | null
}
export enum DocumentProcessingPriority {
standard = 'standard',
priority = 'priority',
@@ -96,12 +91,10 @@ export type CurrentPlanInfoBackend = {
api_rate_limit?: {
size: number
limit: number // total. 0 means unlimited
reset_in_days?: number
}
trigger_events?: {
size: number
limit: number // total. 0 means unlimited
reset_in_days?: number
}
docs_processing: DocumentProcessingPriority
can_replace_logo: boolean
@@ -1,5 +1,5 @@
'use client'
import type { CSSProperties, FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import PremiumBadge from '../../base/premium-badge'
@@ -9,24 +9,19 @@ import { useModalContext } from '@/context/modal-context'
type Props = {
className?: string
style?: CSSProperties
isFull?: boolean
size?: 'md' | 'lg'
isPlain?: boolean
isShort?: boolean
onClick?: () => void
loc?: string
labelKey?: string
}
const UpgradeBtn: FC<Props> = ({
className,
style,
isPlain = false,
isShort = false,
onClick: _onClick,
loc,
labelKey,
}) => {
const { t } = useTranslation()
const { setShowPricingModal } = useModalContext()
@@ -45,17 +40,10 @@ const UpgradeBtn: FC<Props> = ({
}
}
const defaultBadgeLabel = t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)
const label = labelKey ? t(labelKey) : defaultBadgeLabel
if (isPlain) {
return (
<Button
className={className}
style={style}
onClick={onClick}
>
{labelKey ? label : t('billing.upgradeBtn.plain')}
<Button onClick={onClick}>
{t('billing.upgradeBtn.plain')}
</Button>
)
}
@@ -66,13 +54,11 @@ const UpgradeBtn: FC<Props> = ({
color='blue'
allowHover={true}
onClick={onClick}
className={className}
style={style}
>
<SparklesSoft className='flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{label}
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
</span>
</div>
</PremiumBadge>
+17 -26
View File
@@ -16,12 +16,10 @@ type Props = {
total: number
unit?: string
unitPosition?: 'inline' | 'suffix'
resetHint?: string
resetInDays?: number
hideIcon?: boolean
}
const WARNING_THRESHOLD = 80
const LOW = 50
const MIDDLE = 80
const UsageInfo: FC<Props> = ({
className,
@@ -32,39 +30,28 @@ const UsageInfo: FC<Props> = ({
total,
unit,
unitPosition = 'suffix',
resetHint,
resetInDays,
hideIcon = false,
}) => {
const { t } = useTranslation()
const percent = usage / total * 100
const color = percent >= 100
? 'bg-components-progress-error-progress'
: (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid')
const color = (() => {
if (percent < LOW)
return 'bg-components-progress-bar-progress-solid'
if (percent < MIDDLE)
return 'bg-components-progress-warning-progress'
return 'bg-components-progress-error-progress'
})()
const isUnlimited = total === NUM_INFINITE
let totalDisplay: string | number = isUnlimited ? t('billing.plansCommon.unlimited') : total
if (!isUnlimited && unit && unitPosition === 'inline')
totalDisplay = `${total}${unit}`
const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix'
const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('billing.usagePage.resetsIn', { count: resetInDays }) : undefined)
const rightInfo = resetText
? (
<div className='system-xs-regular ml-auto flex-1 text-right text-text-tertiary'>
{resetText}
</div>
)
: (showUnit && (
<div className='system-xs-medium ml-auto text-text-tertiary'>
{unit}
</div>
))
return (
<div className={cn('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
{!hideIcon && Icon && (
<Icon className='h-4 w-4 text-text-tertiary' />
)}
<Icon className='h-4 w-4 text-text-tertiary' />
<div className='flex items-center gap-1'>
<div className='system-xs-medium text-text-tertiary'>{name}</div>
{tooltip && (
@@ -83,7 +70,11 @@ const UsageInfo: FC<Props> = ({
<div className='system-md-regular text-text-quaternary'>/</div>
<div>{totalDisplay}</div>
</div>
{rightInfo}
{showUnit && (
<div className='system-xs-medium ml-auto text-text-tertiary'>
{unit}
</div>
)}
</div>
<ProgressBar
percent={percent}
@@ -36,9 +36,5 @@ export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE),
triggerEvents: resolveLimit(data.trigger_events?.limit, planPreset?.triggerEvents),
},
reset: {
apiRateLimit: data.api_rate_limit?.reset_in_days ?? null,
triggerEvents: data.trigger_events?.reset_in_days ?? null,
},
}
}
@@ -30,14 +30,10 @@ import { useDocLink } from '@/context/i18n'
export type IAppCardProps = {
appInfo: AppDetailResponse & Partial<AppSSO>
triggerModeDisabled?: boolean // align with Trigger Node vs User Input exclusivity
triggerModeMessage?: React.ReactNode // display-only message explaining the trigger restriction
}
function MCPServiceCard({
appInfo,
triggerModeDisabled = false,
triggerModeMessage = '',
}: IAppCardProps) {
const { t } = useTranslation()
const docLink = useDocLink()
@@ -83,7 +79,7 @@ function MCPServiceCard({
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
const missingStartNode = isWorkflowApp && !hasStartNode
const hasInsufficientPermissions = !isCurrentWorkspaceEditor
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode
const isMinimalState = appUnpublished || missingStartNode
const [activated, setActivated] = useState(serverActivated)
@@ -148,18 +144,7 @@ function MCPServiceCard({
return (
<>
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight', isMinimalState && 'h-12')}>
<div className={cn('relative rounded-xl bg-background-default', triggerModeDisabled && 'opacity-60')}>
{triggerModeDisabled && (
triggerModeMessage ? (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
</Tooltip>
) : <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
)}
<div className='rounded-xl bg-background-default'>
<div className={cn('flex w-full flex-col items-start justify-center gap-3 self-stretch p-3', isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle')}>
<div className='flex w-full items-center gap-3 self-stretch'>
<div className='flex grow items-center'>
@@ -197,7 +182,7 @@ function MCPServiceCard({
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</>
) : triggerModeMessage || ''
) : ''
) : ''
}
position="right"
@@ -40,8 +40,6 @@ import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames'
import { useIsChatMode } from '@/app/components/workflow/hooks'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
const FeaturesTrigger = () => {
const { t } = useTranslation()
@@ -52,7 +50,6 @@ const FeaturesTrigger = () => {
const appID = appDetail?.id
const setAppDetail = useAppStore(s => s.setAppDetail)
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const { plan, isFetchedPlan } = useProviderContext()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
@@ -98,15 +95,6 @@ const FeaturesTrigger = () => {
const hasTriggerNode = useMemo(() => (
nodes.some(node => isTriggerNode(node.data.type as BlockEnum))
), [nodes])
const startNodeLimitExceeded = useMemo(() => {
const entryCount = nodes.reduce((count, node) => {
const nodeType = node.data.type as BlockEnum
if (nodeType === BlockEnum.Start || isTriggerNode(nodeType))
return count + 1
return count
}, 0)
return isFetchedPlan && plan.type === Plan.sandbox && entryCount > 2
}, [nodes, plan.type, isFetchedPlan])
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
const invalidateAppTriggers = useInvalidateAppTriggers()
@@ -208,8 +196,7 @@ const FeaturesTrigger = () => {
crossAxisOffset: 4,
missingStartNode: !startNode,
hasTriggerNode,
startNodeLimitExceeded,
publishDisabled: !hasWorkflowNodes || startNodeLimitExceeded,
publishDisabled: !hasWorkflowNodes,
}}
/>
</>
+8
View File
@@ -11,6 +11,7 @@ import Toast from '@/app/components/base/toast'
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
import { trackEvent } from '@/app/components/base/amplitude'
export default function CheckCode() {
const { t, i18n } = useTranslation()
@@ -43,6 +44,13 @@ export default function CheckCode() {
setIsLoading(true)
const ret = await emailLoginWithCode({ email, code, token, language })
if (ret.result === 'success') {
// Track login success event
trackEvent('user_login_success', {
method: 'email_code',
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
is_invite: !!invite_token,
})
if (invite_token) {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
@@ -12,6 +12,7 @@ import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
import type { ResponseError } from '@/service/fetch'
import { trackEvent } from '@/app/components/base/amplitude'
type MailAndPasswordAuthProps = {
isInvite: boolean
@@ -63,6 +64,13 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
body: loginData,
})
if (res.result === 'success') {
// Track login success event
trackEvent('user_login_success', {
method: 'email_password',
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
is_invite: isInvite,
})
if (isInvite) {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
-1
View File
@@ -42,7 +42,6 @@ export default function CheckCode() {
}
setIsLoading(true)
const res = await verifyCode({ email, code, token })
console.log(res)
if ((res as MailValidityResponse).is_valid) {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent((res as MailValidityResponse).token))
+7
View File
@@ -9,6 +9,7 @@ import Input from '@/app/components/base/input'
import { validPassword } from '@/config'
import type { MailRegisterResponse } from '@/service/use-common'
import { useMailRegister } from '@/service/use-common'
import { trackEvent } from '@/app/components/base/amplitude'
const ChangePasswordForm = () => {
const { t } = useTranslation()
@@ -54,6 +55,12 @@ const ChangePasswordForm = () => {
})
const { result } = res as MailRegisterResponse
if (result === 'success') {
// Track registration success event
trackEvent('user_registration_success', {
method: 'email',
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
})
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
+28
View File
@@ -10,6 +10,7 @@ import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { noop } from 'lodash-es'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { useGlobalPublicStore } from './global-public-context'
export type AppContextValue = {
@@ -159,6 +160,33 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
}, [currentWorkspace?.id])
// #endregion Zendesk conversation fields
// #region Amplitude user tracking
useEffect(() => {
// Report user info to Amplitude when loaded
if (userProfile?.id) {
setUserId(userProfile.email)
setUserProperties({
email: userProfile.email,
name: userProfile.name,
has_password: userProfile.is_password_set,
})
}
}, [userProfile?.id, userProfile?.email, userProfile?.name, userProfile?.is_password_set])
useEffect(() => {
// Report workspace info to Amplitude when loaded
if (currentWorkspace?.id && userProfile?.id) {
setUserProperties({
workspace_id: currentWorkspace.id,
workspace_name: currentWorkspace.name,
workspace_plan: currentWorkspace.plan,
workspace_status: currentWorkspace.status,
workspace_role: currentWorkspace.role,
})
}
}, [currentWorkspace?.id, currentWorkspace?.name, currentWorkspace?.plan, currentWorkspace?.status, currentWorkspace?.role, userProfile?.id])
// #endregion Amplitude user tracking
return (
<AppContext.Provider value={{
userProfile,
@@ -1,130 +0,0 @@
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,
}
}
-181
View File
@@ -1,181 +0,0 @@
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))
})
})
-43
View File
@@ -36,12 +36,6 @@ import { noop } from 'lodash-es'
import dynamic from 'next/dynamic'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import {
type TriggerEventsLimitModalPayload,
useTriggerEventsLimitModal,
} from './hooks/use-trigger-events-limit-modal'
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
ssr: false,
@@ -80,9 +74,6 @@ const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugi
const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
ssr: false,
})
const TriggerEventsLimitModal = dynamic(() => import('@/app/components/billing/trigger-events-limit-modal'), {
ssr: false,
})
export type ModalState<T> = {
payload: T
@@ -122,7 +113,6 @@ export type ModalContextState = {
}> | null>>
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
}
const PRICING_MODAL_QUERY_PARAM = 'pricing'
const PRICING_MODAL_QUERY_VALUE = 'open'
@@ -140,7 +130,6 @@ const ModalContext = createContext<ModalContextState>({
setShowOpeningModal: noop,
setShowUpdatePluginModal: noop,
setShowEducationExpireNoticeModal: noop,
setShowTriggerEventsLimitModal: noop,
})
export const useModalContext = () => useContext(ModalContext)
@@ -179,7 +168,6 @@ export const ModalContextProvider = ({
}> | null>(null)
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const [showPricingModal, setShowPricingModal] = useState(
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
@@ -240,17 +228,6 @@ export const ModalContextProvider = ({
window.history.replaceState(null, '', url.toString())
}, [showPricingModal])
const { plan, isFetchedPlan } = useProviderContext()
const {
showTriggerEventsLimitModal,
setShowTriggerEventsLimitModal,
persistTriggerEventsLimitModalDismiss,
} = useTriggerEventsLimitModal({
plan,
isFetchedPlan,
currentWorkspaceId: currentWorkspace?.id,
})
const handleCancelModerationSettingModal = () => {
setShowModerationSettingModal(null)
if (showModerationSettingModal?.onCancelCallback)
@@ -357,7 +334,6 @@ export const ModalContextProvider = ({
setShowOpeningModal,
setShowUpdatePluginModal,
setShowEducationExpireNoticeModal,
setShowTriggerEventsLimitModal,
}}>
<>
{children}
@@ -479,25 +455,6 @@ export const ModalContextProvider = ({
onClose={() => setShowEducationExpireNoticeModal(null)}
/>
)}
{
!!showTriggerEventsLimitModal && (
<TriggerEventsLimitModal
show
usage={showTriggerEventsLimitModal.payload.usage}
total={showTriggerEventsLimitModal.payload.total}
planType={showTriggerEventsLimitModal.payload.planType}
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
onDismiss={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
}}
onUpgrade={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
handleShowPricingModal()
}}
/>
)}
</>
</ModalContext.Provider>
)
+1 -2
View File
@@ -17,7 +17,7 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { Plan, UsageResetInfo } from '@/app/components/billing/type'
import type { Plan } from '@/app/components/billing/type'
import type { UsagePlanInfo } from '@/app/components/billing/type'
import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils'
@@ -40,7 +40,6 @@ type ProviderContextState = {
type: Plan
usage: UsagePlanInfo
total: UsagePlanInfo
reset: UsageResetInfo
}
isFetchedPlan: boolean
enableBilling: boolean
+2 -2
View File
@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Nachrichtenguthaben',
tooltip: 'Nachrichtenaufrufkontingente für verschiedene Tarife unter Verwendung von OpenAI-Modellen (außer gpt4).Nachrichten über dem Limit verwenden Ihren OpenAI-API-Schlüssel.',
titlePerMonth: '{{count,number}} Nachrichten / Monat',
titlePerMonth: '{{count,number}} Nachrichten/Monat',
},
annotatedResponse: {
title: 'Kontingentgrenzen für Annotationen',
@@ -83,7 +83,7 @@ const translation = {
cloud: 'Cloud-Dienst',
apiRateLimitTooltip: 'Die API-Datenbeschränkung gilt für alle Anfragen, die über die Dify-API gemacht werden, einschließlich Textgenerierung, Chat-Konversationen, Workflow-Ausführungen und Dokumentenverarbeitung.',
getStarted: 'Loslegen',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/Monat',
documentsTooltip: 'Vorgabe für die Anzahl der Dokumente, die aus der Wissensdatenquelle importiert werden.',
apiRateLimit: 'API-Datenlimit',
documents: '{{count,number}} Wissensdokumente',
-3
View File
@@ -138,9 +138,6 @@ const translation = {
running: 'In Service',
disable: 'Disabled',
},
disableTooltip: {
triggerMode: 'The {{feature}} feature is not supported in Trigger Node mode.',
},
},
analysis: {
title: 'Analysis',
+7 -17
View File
@@ -9,16 +9,8 @@ const translation = {
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
triggerEvents: 'Trigger Events',
perMonth: 'per month',
resetsIn: 'Resets in {{count,number}} days',
},
teamMembers: 'Team Members',
triggerLimitModal: {
title: 'Upgrade to unlock more trigger events',
description: 'Youve reached the limit of workflow event triggers for this plan.',
dismiss: 'Dismiss',
upgrade: 'Upgrade',
usageTitle: 'TRIGGER EVENTS',
},
upgradeBtn: {
plain: 'View Plan',
encourage: 'Upgrade Now',
@@ -69,11 +61,11 @@ const translation = {
documentsTooltip: 'Quota on the number of documents imported from the Knowledge Data Source.',
vectorSpace: '{{size}} Knowledge Data Storage',
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
documentsRequestQuota: '{{count,number}} Knowledge Request / min',
documentsRequestQuota: '{{count,number}}/min Knowledge Request Rate Limit',
documentsRequestQuotaTooltip: 'Specifies the total number of actions a workspace can perform per minute within the knowledge base, including dataset creation, deletion, updates, document uploads, modifications, archiving, and knowledge base queries. This metric is used to evaluate the performance of knowledge base requests. For example, if a Sandbox user performs 10 consecutive hit tests within one minute, their workspace will be temporarily restricted from performing the following actions for the next minute: dataset creation, deletion, updates, and document uploads or modifications. ',
apiRateLimit: 'API Rate Limit',
apiRateLimitUnit: '{{count,number}}',
unlimitedApiRate: 'No Dify API Rate Limit',
apiRateLimitUnit: '{{count,number}}/month',
unlimitedApiRate: 'No API Rate Limit',
apiRateLimitTooltip: 'API Rate Limit applies to all requests made through the Dify API, including text generation, chat conversations, workflow executions, and document processing.',
documentProcessingPriority: ' Document Processing',
documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
@@ -84,19 +76,17 @@ const translation = {
},
triggerEvents: {
sandbox: '{{count,number}} Trigger Events',
professional: '{{count,number}} Trigger Events / month',
professional: '{{count,number}} Trigger Events/month',
unlimited: 'Unlimited Trigger Events',
tooltip: 'The number of events that automatically start workflows through Plugin, Schedule, or Webhook triggers.',
},
workflowExecution: {
standard: 'Standard Workflow Execution',
faster: 'Faster Workflow Execution',
priority: 'Priority Workflow Execution',
tooltip: 'Workflow execution queue priority and speed.',
},
startNodes: {
limited: 'Up to {{count}} Start Nodes / workflow',
unlimited: 'Unlimited Start Nodes / workflow',
limited: 'Up to {{count}} Start Nodes per Workflow',
unlimited: 'Unlimited Start Nodes per Workflow',
},
logsHistory: '{{days}} Log history',
customTools: 'Custom Tools',
@@ -125,7 +115,7 @@ const translation = {
memberAfter: 'Member',
messageRequest: {
title: '{{count,number}} message credits',
titlePerMonth: '{{count,number}} message credits / month',
titlePerMonth: '{{count,number}} message credits/month',
tooltip: 'Message credits are provided to help you easily try out different OpenAI models in Dify. Credits are consumed based on the model type. Once theyre used up, you can switch to your own OpenAI API key.',
},
annotatedResponse: {
-5
View File
@@ -123,11 +123,6 @@ const translation = {
noHistory: 'No History',
tagBound: 'Number of apps using this tag',
},
publishLimit: {
startNodeTitlePrefix: 'Upgrade to',
startNodeTitleSuffix: 'unlock unlimited start nodes',
startNodeDesc: 'Youve reached the limit of 2 start nodes for your current plan. Upgrade to publish this workflow.',
},
env: {
envPanelTitle: 'Environment Variables',
envDescription: 'Environment variables can be used to store private information and credentials. They are read-only and can be separated from the DSL file during export.',
+2 -2
View File
@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Créditos de Mensajes',
tooltip: 'Cuotas de invocación de mensajes para varios planes utilizando modelos de OpenAI (excepto gpt4). Los mensajes que excedan el límite utilizarán tu clave API de OpenAI.',
titlePerMonth: '{{count,number}} mensajes / mes',
titlePerMonth: '{{count,number}} mensajes/mes',
},
annotatedResponse: {
title: 'Límites de Cuota de Anotación',
@@ -76,7 +76,7 @@ const translation = {
priceTip: 'por espacio de trabajo/',
teamMember_one: '{{count, número}} Miembro del Equipo',
getStarted: 'Comenzar',
apiRateLimitUnit: '{{count, número}}',
apiRateLimitUnit: '{{count, número}}/mes',
freeTrialTipSuffix: 'No se requiere tarjeta de crédito',
unlimitedApiRate: 'Sin límite de tasa de API',
apiRateLimit: 'Límite de tasa de API',
+1 -1
View File
@@ -73,7 +73,7 @@ const translation = {
},
ragAPIRequestTooltip: 'به تعداد درخواست‌های API که فقط قابلیت‌های پردازش پایگاه دانش Dify را فراخوانی می‌کنند اشاره دارد.',
receiptInfo: 'فقط صاحب تیم و مدیر تیم می‌توانند اشتراک تهیه کنند و اطلاعات صورتحساب را مشاهده کنند',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/ماه',
cloud: 'سرویس ابری',
documents: '{{count,number}} سندهای دانش',
self: 'خود میزبان',
+2 -2
View File
@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Crédits de message',
tooltip: 'Quotas d\'invocation de messages pour divers plans utilisant les modèles OpenAI (sauf gpt4). Les messages dépassant la limite utiliseront votre clé API OpenAI.',
titlePerMonth: '{{count,number}} messages / mois',
titlePerMonth: '{{count,number}} messages/mois',
},
annotatedResponse: {
title: 'Limites de quota d\'annotation',
@@ -73,7 +73,7 @@ const translation = {
ragAPIRequestTooltip: 'Fait référence au nombre d\'appels API invoquant uniquement les capacités de traitement de la base de connaissances de Dify.',
receiptInfo: 'Seuls le propriétaire de l\'équipe et l\'administrateur de l\'équipe peuvent s\'abonner et consulter les informations de facturation',
annotationQuota: 'Quota dannotation',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/mois',
priceTip: 'par espace de travail/',
freeTrialTipSuffix: 'Aucune carte de crédit requise',
teamWorkspace: '{{count,number}} Espace de travail d\'équipe',
+2 -2
View File
@@ -70,7 +70,7 @@ const translation = {
title: 'संदेश क्रेडिट्स',
tooltip:
'विभिन्न योजनाओं के लिए संदेश आह्वान कोटा OpenAI मॉडलों का उपयोग करके (gpt4 को छोड़कर)। सीमा से अधिक संदेश आपके OpenAI API कुंजी का उपयोग करेंगे।',
titlePerMonth: '{{count,number}} संदेश / महीना',
titlePerMonth: '{{count,number}} संदेश/महीना',
},
annotatedResponse: {
title: 'एनोटेशन कोटा सीमाएं',
@@ -96,7 +96,7 @@ const translation = {
freeTrialTip: '200 ओपनएआई कॉल्स का मुफ्त परीक्षण।',
documents: '{{count,number}} ज्ञान दस्तावेज़',
freeTrialTipSuffix: 'कोई क्रेडिट कार्ड की आवश्यकता नहीं है',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/माह',
teamWorkspace: '{{count,number}} टीम कार्यक्षेत्र',
apiRateLimitTooltip: 'Dify API के माध्यम से की गई सभी अनुरोधों पर API दर सीमा लागू होती है, जिसमें टेक्स्ट जनरेशन, चैट वार्तालाप, कार्यप्रवाह निष्पादन और दस्तावेज़ प्रसंस्करण शामिल हैं।',
teamMember_one: '{{count,number}} टीम सदस्य',
+2 -2
View File
@@ -70,7 +70,7 @@ const translation = {
title: 'Crediti Messaggi',
tooltip:
'Quote di invocazione dei messaggi per vari piani utilizzando i modelli OpenAI (eccetto gpt4). I messaggi oltre il limite utilizzeranno la tua chiave API OpenAI.',
titlePerMonth: '{{count,number}} messaggi / mese',
titlePerMonth: '{{count,number}} messaggi/mese',
},
annotatedResponse: {
title: 'Limiti di Quota di Annotazione',
@@ -88,7 +88,7 @@ const translation = {
freeTrialTipPrefix: 'Iscriviti e ricevi un',
teamMember_one: '{{count,number}} membro del team',
documents: '{{count,number}} Documenti di Conoscenza',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/mese',
documentsRequestQuota: '{{count,number}}/min Limite di richiesta di conoscenza',
teamMember_other: '{{count,number}} membri del team',
freeTrialTip: 'prova gratuita di 200 chiamate OpenAI.',
-3
View File
@@ -138,9 +138,6 @@ const translation = {
running: '稼働中',
disable: '無効',
},
disableTooltip: {
triggerMode: 'トリガーノードモードでは{{feature}}機能を使用できません。',
},
},
analysis: {
title: '分析',
+5 -29
View File
@@ -7,16 +7,8 @@ const translation = {
documentsUploadQuota: 'ドキュメント・アップロード・クォータ',
vectorSpace: 'ナレッジベースのデータストレージ',
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
triggerEvents: 'トリガーイベント',
triggerEvents: 'トリガーイベント',
perMonth: '月あたり',
resetsIn: '{{count,number}}日後にリセット',
},
triggerLimitModal: {
title: 'さらにトリガーイベントを利用するにはアップグレードしてください',
description: 'このプランのワークフロー・トリガーイベントの上限に達しました。',
dismiss: '閉じる',
upgrade: 'アップグレード',
usageTitle: 'TRIGGER EVENTS',
},
upgradeBtn: {
plain: 'プランをアップグレード',
@@ -67,10 +59,10 @@ const translation = {
documentsTooltip: 'ナレッジデータソースからインポートされたドキュメントの数に対するクォータ。',
vectorSpace: '{{size}}のナレッジベースのデータストレージ',
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
documentsRequestQuota: '{{count,number}} のナレッジリクエスト上限 / 分',
documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限',
documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが 1 分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが 1 分間に 10 回連続でヒットテストを実行した場合、そのワークスペースは次の 1 分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。',
apiRateLimit: 'API リクエスト制限',
apiRateLimitUnit: '{{count,number}}',
apiRateLimit: 'API レート制限',
apiRateLimitUnit: '{{count,number}}/月',
unlimitedApiRate: '無制限の API コール',
apiRateLimitTooltip: 'API レート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API 経由のすべてのリクエストに適用されます。',
documentProcessingPriority: '文書処理',
@@ -80,22 +72,6 @@ const translation = {
'priority': '優先',
'top-priority': '最優先',
},
triggerEvents: {
sandbox: '{{count,number}} トリガーイベント数',
professional: '{{count,number}} トリガーイベント数 / 月',
unlimited: '無制限のトリガーイベント数',
tooltip: 'プラグイントリガー、タイマートリガー、または Webhook トリガーによって自動的にワークフローを起動するイベントの回数です。',
},
workflowExecution: {
standard: '標準ワークフロー実行キュー',
faster: '高速ワークフロー実行キュー',
priority: '優先度の高いワークフロー実行キュー',
tooltip: 'ワークフローの実行キューの優先度と実行速度。',
},
startNodes: {
limited: '各ワークフローにつき、開始ノードは最大{{count}}つまで設定可能',
unlimited: '各ワークフローの開始ノード数は無制限',
},
logsHistory: '{{days}}のログ履歴',
customTools: 'カスタムツール',
unavailable: '利用不可',
@@ -123,7 +99,7 @@ const translation = {
memberAfter: 'メンバー',
messageRequest: {
title: '{{count,number}}メッセージクレジット',
titlePerMonth: '{{count,number}}メッセージクレジット / 月',
titlePerMonth: '{{count,number}}メッセージクレジット/月',
tooltip: 'メッセージクレジットは、Dify でさまざまな OpenAI モデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身の OpenAI API キーに切り替えていただけます。',
},
annotatedResponse: {
-5
View File
@@ -119,11 +119,6 @@ const translation = {
tagBound: 'このタグを使用しているアプリの数',
moreActions: 'さらにアクション',
},
publishLimit: {
startNodeTitlePrefix: 'アップグレードして',
startNodeTitleSuffix: '開始ノードの上限を解除',
startNodeDesc: '現在のプランでは開始ノードは2個までです。公開するにはプランをアップグレードしてください。',
},
env: {
envPanelTitle: '環境変数',
envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。',
+2 -2
View File
@@ -68,7 +68,7 @@ const translation = {
title: '메시지 크레딧',
tooltip:
'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.',
titlePerMonth: '{{count,number}} 메시지 / 월',
titlePerMonth: '{{count,number}} 메시지/월',
},
annotatedResponse: {
title: '주석 응답 쿼터',
@@ -88,7 +88,7 @@ const translation = {
freeTrialTip: '200 회의 OpenAI 호출 무료 체험을 받으세요. ',
annualBilling: '연간 청구',
getStarted: '시작하기',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/월',
freeTrialTipSuffix: '신용카드 없음',
teamWorkspace: '{{count,number}} 팀 작업 공간',
self: '자체 호스팅',
+2 -2
View File
@@ -68,7 +68,7 @@ const translation = {
title: 'Limity kredytów wiadomości',
tooltip:
'Limity wywołań wiadomości dla różnych planów używających modeli OpenAI (z wyjątkiem gpt4). Wiadomości przekraczające limit będą korzystać z twojego klucza API OpenAI.',
titlePerMonth: '{{count,number}} wiadomości / miesiąc',
titlePerMonth: '{{count,number}} wiadomości/miesiąc',
},
annotatedResponse: {
title: 'Limity kredytów na adnotacje',
@@ -91,7 +91,7 @@ const translation = {
freeTrialTipPrefix: 'Zarejestruj się i zdobądź',
teamMember_other: '{{count,number}} członków zespołu',
teamWorkspace: '{{count,number}} Zespół Workspace',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/miesiąc',
cloud: 'Usługa chmurowa',
teamMember_one: '{{count,number}} Członek zespołu',
priceTip: 'na przestrzeń roboczą/',
+2 -2
View File
@@ -61,7 +61,7 @@ const translation = {
messageRequest: {
title: 'Créditos de Mensagem',
tooltip: 'Cotas de invocação de mensagens para vários planos usando modelos da OpenAI (exceto gpt4). Mensagens além do limite usarão sua Chave de API da OpenAI.',
titlePerMonth: '{{count,number}} mensagens / mês',
titlePerMonth: '{{count,number}} mensagens/mês',
},
annotatedResponse: {
title: 'Limites de Cota de Anotação',
@@ -80,7 +80,7 @@ const translation = {
documentsRequestQuota: '{{count,number}}/min Limite de Taxa de Solicitação de Conhecimento',
cloud: 'Serviço de Nuvem',
teamWorkspace: '{{count,number}} Espaço de Trabalho da Equipe',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/mês',
freeTrialTipSuffix: 'Nenhum cartão de crédito necessário',
teamMember_other: '{{count,number}} Membros da Equipe',
comparePlanAndFeatures: 'Compare planos e recursos',
+2 -2
View File
@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Credite de mesaje',
tooltip: 'Cote de invocare a mesajelor pentru diferite planuri utilizând modele OpenAI (cu excepția gpt4). Mesajele peste limită vor utiliza cheia API OpenAI.',
titlePerMonth: '{{count,number}} mesaje / lună',
titlePerMonth: '{{count,number}} mesaje/lună',
},
annotatedResponse: {
title: 'Limite de cotă de anotare',
@@ -82,7 +82,7 @@ const translation = {
documentsTooltip: 'Cota pe numărul de documente importate din Sursele de Date de Cunoștințe.',
getStarted: 'Întrebați-vă',
cloud: 'Serviciu de cloud',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/lună',
comparePlanAndFeatures: 'Compară planurile și caracteristicile',
documentsRequestQuota: '{{count,number}}/min Limita de rată a cererilor de cunoștințe',
documents: '{{count,number}} Documente de Cunoaștere',
+2 -2
View File
@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Кредиты на сообщения',
tooltip: 'Квоты вызова сообщений для различных тарифных планов, использующих модели OpenAI (кроме gpt4). Сообщения, превышающие лимит, будут использовать ваш ключ API OpenAI.',
titlePerMonth: '{{count,number}} сообщений / месяц',
titlePerMonth: '{{count,number}} сообщений/месяц',
},
annotatedResponse: {
title: 'Ограничения квоты аннотаций',
@@ -78,7 +78,7 @@ const translation = {
apiRateLimit: 'Ограничение скорости API',
self: 'Самостоятельно размещенный',
teamMember_other: '{{count,number}} Члены команды',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/месяц',
unlimitedApiRate: 'Нет ограничений на количество запросов к API',
freeTrialTip: 'бесплатная пробная версия из 200 вызовов OpenAI.',
freeTrialTipSuffix: 'Кредитная карта не требуется',
+2 -2
View File
@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Krediti za sporočila',
tooltip: 'Kvota za klice sporočil pri različnih načrtih z uporabo modelov OpenAI (razen GPT-4). Sporočila preko omejitve bodo uporabljala vaš OpenAI API ključ.',
titlePerMonth: '{{count,number}} sporočil / mesec',
titlePerMonth: '{{count,number}} sporočil/mesec',
},
annotatedResponse: {
title: 'Omejitve kvote za označevanje',
@@ -86,7 +86,7 @@ const translation = {
teamMember_one: '{{count,number}} član ekipe',
teamMember_other: '{{count,number}} Članov ekipe',
documentsRequestQuota: '{{count,number}}/min Omejitev stopnje zahtev po znanju',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/mesec',
priceTip: 'na delovnem prostoru/',
freeTrialTipPrefix: 'Prijavite se in prejmite',
cloud: 'Oblačna storitev',
+2 -2
View File
@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'เครดิตข้อความ',
tooltip: 'โควต้าการเรียกใช้ข้อความสําหรับแผนต่างๆ โดยใช้โมเดล OpenAI (ยกเว้น gpt4) ข้อความที่เกินขีดจํากัดจะใช้คีย์ OpenAI API ของคุณ',
titlePerMonth: '{{count,number}} ข้อความ / เดือน',
titlePerMonth: '{{count,number}} ข้อความ/เดือน',
},
annotatedResponse: {
title: 'ขีดจํากัดโควต้าคําอธิบายประกอบ',
@@ -82,7 +82,7 @@ const translation = {
teamMember_one: '{{count,number}} สมาชิกทีม',
unlimitedApiRate: 'ไม่มีข้อจำกัดอัตราการเรียก API',
self: 'โฮสต์ด้วยตัวเอง',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/เดือน',
teamMember_other: '{{count,number}} สมาชิกทีม',
teamWorkspace: '{{count,number}} ทีมทำงาน',
priceTip: 'ต่อพื้นที่ทำงาน/',
+2 -2
View File
@@ -65,7 +65,7 @@ const translation = {
messageRequest: {
title: 'Mesaj Kredileri',
tooltip: 'OpenAI modellerini (gpt4 hariç) kullanarak çeşitli planlar için mesaj çağrı kotaları. Limitin üzerindeki mesajlar OpenAI API Anahtarınızı kullanır.',
titlePerMonth: '{{count,number}} mesaj / ay',
titlePerMonth: '{{count,number}} mesaj/ay',
},
annotatedResponse: {
title: 'Ek Açıklama Kota Sınırları',
@@ -78,7 +78,7 @@ const translation = {
freeTrialTipPrefix: 'Kaydolun ve bir',
priceTip: 'iş alanı başına/',
documentsRequestQuota: '{{count,number}}/dakika Bilgi İsteği Oran Limiti',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/ay',
documents: '{{count,number}} Bilgi Belgesi',
comparePlanAndFeatures: 'Planları ve özellikleri karşılaştır',
self: 'Kendi Barındırılan',
+2 -2
View File
@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Кредити повідомлень',
tooltip: 'Квоти на виклик повідомлень для різних планів з використанням моделей OpenAI (крім gpt4). Повідомлення понад ліміт використовуватимуть ваш ключ API OpenAI.',
titlePerMonth: '{{count,number}} повідомлень / місяць',
titlePerMonth: '{{count,number}} повідомлень/місяць',
},
annotatedResponse: {
title: 'Ліміти квоти відповідей з анотаціями',
@@ -84,7 +84,7 @@ const translation = {
priceTip: 'за робочим простором/',
unlimitedApiRate: 'Немає обмеження на швидкість API',
freeTrialTipSuffix: 'Кредитна картка не потрібна',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/місяць',
getStarted: 'Почати',
freeTrialTip: 'безкоштовна пробна версія з 200 запитів до OpenAI.',
documents: '{{count,number}} Документів знань',
+2 -2
View File
@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: 'Số Lượng Tin Nhắn',
tooltip: 'Hạn mức triệu hồi tin nhắn cho các kế hoạch sử dụng mô hình OpenAI (ngoại trừ gpt4). Các tin nhắn vượt quá giới hạn sẽ sử dụng Khóa API OpenAI của bạn.',
titlePerMonth: '{{count,number}} tin nhắn / tháng',
titlePerMonth: '{{count,number}} tin nhắn/tháng',
},
annotatedResponse: {
title: 'Hạn Mức Quota Phản hồi Đã được Ghi chú',
@@ -90,7 +90,7 @@ const translation = {
teamMember_other: '{{count,number}} thành viên trong nhóm',
documents: '{{count,number}} Tài liệu Kiến thức',
getStarted: 'Bắt đầu',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/thá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.',
startBuilding: 'Bắt đầu xây dựng',
-3
View File
@@ -138,9 +138,6 @@ const translation = {
running: '运行中',
disable: '已停用',
},
disableTooltip: {
triggerMode: '触发节点模式下不支持{{feature}}功能。',
},
},
analysis: {
title: '分析',
+12 -22
View File
@@ -7,16 +7,8 @@ const translation = {
documentsUploadQuota: '文档上传配额',
vectorSpace: '知识库数据存储空间',
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
triggerEvents: '触发事件',
triggerEvents: '触发事件',
perMonth: '每月',
resetsIn: '{{count,number}} 天后重置',
},
triggerLimitModal: {
title: '升级以解锁更多触发事件额度',
description: '当前套餐的工作流触发事件额度已达上限。',
dismiss: '知道了',
upgrade: '升级',
usageTitle: '触发事件额度',
},
upgradeBtn: {
plain: '查看套餐',
@@ -68,10 +60,10 @@ const translation = {
documentsTooltip: '从知识库的数据源导入的文档数量配额。',
vectorSpace: '{{size}} 知识库数据存储空间',
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
documentsRequestQuota: '{{count,number}} 知识请求 / 分钟',
documentsRequestQuota: '{{count,number}}/分钟 知识请求频率限制',
documentsRequestQuotaTooltip: '指每分钟内,一个空间在知识库中可执行的操作总数,包括数据集的创建、删除、更新,文档的上传、修改、归档,以及知识库查询等,用于评估知识库请求的性能。例如,Sandbox 用户在 1 分钟内连续执行 10 次命中测试,其工作区将在接下来的 1 分钟内无法继续执行以下操作:数据集的创建、删除、更新,文档的上传、修改等操作。',
apiRateLimit: 'API 请求频率限制',
apiRateLimitUnit: '{{count,number}} 次',
apiRateLimitUnit: '{{count,number}} 次/月',
unlimitedApiRate: 'API 请求频率无限制',
apiRateLimitTooltip: 'API 请求频率限制涵盖所有通过 Dify API 发起的调用,例如文本生成、聊天对话、工作流执行和文档处理等。',
documentProcessingPriority: '文档处理',
@@ -82,20 +74,18 @@ const translation = {
'top-priority': '最高优先级',
},
triggerEvents: {
sandbox: '{{count,number}} 触发事件',
professional: '{{count,number}} 触发事件数 / 月',
unlimited: '无限触发事件',
tooltip: '通过插件、定时触发器、Webhook 等来自动触发工作流的事件数。',
sandbox: '{{count,number}} 触发事件',
professional: '{{count,number}} 触发事件/月',
unlimited: '无限触发事件',
},
workflowExecution: {
standard: '标准工作流执行队列',
faster: '快速工作流执行队列',
priority: '优先工作流执行队列',
tooltip: '工作流的执行队列优先级与运行速度。',
standard: '标准工作流执行',
faster: '更快的工作流执行',
priority: '优先工作流执行',
},
startNodes: {
limited: '最多 {{count}} 个起始节点 / 工作流',
unlimited: '无限起始节点 / 工作流',
limited: '每个工作流最多 {{count}} 个起始节点',
unlimited: '每个工作流无限起始节点',
},
logsHistory: '{{days}}日志历史',
customTools: '自定义工具',
@@ -124,7 +114,7 @@ const translation = {
memberAfter: '个成员',
messageRequest: {
title: '{{count,number}} 条消息额度',
titlePerMonth: '{{count,number}} 条消息额度 / 月',
titlePerMonth: '{{count,number}} 条消息额度/月',
tooltip: '消息额度旨在帮助您便捷地试用 Dify 中的各类 OpenAI 模型。不同模型会消耗不同额度。额度用尽后,您可以切换为使用自己的 OpenAI API 密钥。',
},
annotatedResponse: {
-5
View File
@@ -122,11 +122,6 @@ const translation = {
noHistory: '没有历史版本',
tagBound: '使用此标签的应用数量',
},
publishLimit: {
startNodeTitlePrefix: '升级以',
startNodeTitleSuffix: '解锁无限开始节点',
startNodeDesc: '当前套餐最多支持 2 个开始节点。升级套餐即可发布此工作流。',
},
env: {
envPanelTitle: '环境变量',
envDescription: '环境变量是一种存储敏感信息的方法,如 API 密钥、数据库密码等。它们被存储在工作流程中,而不是代码中,以便在不同环境中共享。',
+2 -2
View File
@@ -64,7 +64,7 @@ const translation = {
messageRequest: {
title: '訊息額度',
tooltip: '為不同方案提供基於 OpenAI 模型的訊息響應額度。',
titlePerMonth: '{{count,number}} 消息 / 月',
titlePerMonth: '{{count,number}} 消息/月',
},
annotatedResponse: {
title: '標註回覆數',
@@ -74,7 +74,7 @@ const translation = {
receiptInfo: '只有團隊所有者和團隊管理員才能訂閱和檢視賬單資訊',
annotationQuota: '註釋配額',
self: '自我主持',
apiRateLimitUnit: '{{count,number}}',
apiRateLimitUnit: '{{count,number}}/月',
freeTrialTipPrefix: '註冊並獲得一個',
annualBilling: '年度計費',
freeTrialTipSuffix: '無需信用卡',
-5
View File
@@ -116,11 +116,6 @@ const translation = {
currentWorkflow: '當前工作流程',
moreActions: '更多動作',
},
publishLimit: {
startNodeTitlePrefix: '升級以',
startNodeTitleSuffix: '解鎖無限開始節點',
startNodeDesc: '目前方案最多允許 2 個開始節點,升級後才能發布此工作流程。',
},
env: {
envPanelTitle: '環境變數',
envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與 DSL 文件分開。',
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com'
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com'
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
+2
View File
@@ -44,6 +44,8 @@
"knip": "knip"
},
"dependencies": {
"@amplitude/analytics-browser": "^2.31.3",
"@amplitude/plugin-session-replay-browser": "^1.23.6",
"@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.26.28",
"@formatjs/intl-localematcher": "^0.5.10",
+1837 -1577
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -10,10 +10,3 @@ export const isAfter = (date: ConfigType, compare: ConfigType) => {
export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => {
return dayjs(date).format(dateFormat)
}
export const getDaysUntilEndOfMonth = (date: ConfigType = dayjs()) => {
const current = dayjs(date).startOf('day')
const endOfMonth = dayjs(date).endOf('month').startOf('day')
const diff = endOfMonth.diff(current, 'day')
return Math.max(diff, 0)
}