diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 64ce869c5d..a11af3b816 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -49,6 +49,7 @@ import { fetchInstalledAppList } from '@/service/explore' import { AppModeEnum } from '@/types/app' import type { PublishWorkflowParams } from '@/types/workflow' import { basePath } from '@/utils/var' +import UpgradeBtn from '@/app/components/billing/upgrade-btn' const ACCESS_MODE_MAP: Record = { [AccessMode.ORGANIZATION]: { @@ -106,6 +107,7 @@ export type AppPublisherProps = { workflowToolAvailable?: boolean missingStartNode?: boolean hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist). + startNodeLimitExceeded?: boolean } const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P'] @@ -127,6 +129,7 @@ const AppPublisher = ({ workflowToolAvailable = true, missingStartNode = false, hasTriggerNode = false, + startNodeLimitExceeded = false, }: AppPublisherProps) => { const { t } = useTranslation() @@ -246,6 +249,13 @@ const AppPublisher = ({ const hasPublishedVersion = !!publishedAt const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined + const showStartNodeLimitHint = Boolean(startNodeLimitExceeded) + const upgradeHighlightStyle = useMemo(() => ({ + background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)', + WebkitBackgroundClip: 'text', + backgroundClip: 'text', + WebkitTextFillColor: 'transparent', + }), []) return ( <> @@ -304,29 +314,49 @@ const AppPublisher = ({ /> ) : ( - + ) + } + + {showStartNodeLimitHint && ( +
+

+ {t('workflow.publishLimit.startNodeTitlePrefix')} + {t('workflow.publishLimit.startNodeTitleSuffix')} +

+

+ {t('workflow.publishLimit.startNodeDesc')} +

+ +
+ )} + ) } diff --git a/web/app/components/billing/upgrade-btn/index.tsx b/web/app/components/billing/upgrade-btn/index.tsx index f3ae95a10b..766a5883d6 100644 --- a/web/app/components/billing/upgrade-btn/index.tsx +++ b/web/app/components/billing/upgrade-btn/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { FC } from 'react' +import type { CSSProperties, FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' import PremiumBadge from '../../base/premium-badge' @@ -9,6 +9,7 @@ import { useModalContext } from '@/context/modal-context' type Props = { className?: string + style?: CSSProperties isFull?: boolean size?: 'md' | 'lg' isPlain?: boolean @@ -18,6 +19,8 @@ type Props = { } const UpgradeBtn: FC = ({ + className, + style, isPlain = false, isShort = false, onClick: _onClick, @@ -42,7 +45,11 @@ const UpgradeBtn: FC = ({ if (isPlain) { return ( - ) @@ -54,6 +61,8 @@ const UpgradeBtn: FC = ({ color='blue' allowHover={true} onClick={onClick} + className={className} + style={style} >
diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index d229006177..28a2f43fe5 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -40,6 +40,8 @@ import useTheme from '@/hooks/use-theme' import cn from '@/utils/classnames' import { useIsChatMode } from '@/app/components/workflow/hooks' import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' +import { useProviderContext } from '@/context/provider-context' +import { Plan } from '@/app/components/billing/type' const FeaturesTrigger = () => { const { t } = useTranslation() @@ -50,6 +52,7 @@ const FeaturesTrigger = () => { const appID = appDetail?.id const setAppDetail = useAppStore(s => s.setAppDetail) const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly() + const { plan, isFetchedPlan } = useProviderContext() const publishedAt = useStore(s => s.publishedAt) const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const toolPublished = useStore(s => s.toolPublished) @@ -95,6 +98,15 @@ const FeaturesTrigger = () => { const hasTriggerNode = useMemo(() => ( nodes.some(node => isTriggerNode(node.data.type as BlockEnum)) ), [nodes]) + const startNodeLimitExceeded = useMemo(() => { + const entryCount = nodes.reduce((count, node) => { + const nodeType = node.data.type as BlockEnum + if (nodeType === BlockEnum.Start || isTriggerNode(nodeType)) + return count + 1 + return count + }, 0) + return isFetchedPlan && plan.type === Plan.sandbox && entryCount > 2 + }, [nodes, plan.type, isFetchedPlan]) const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() const invalidateAppTriggers = useInvalidateAppTriggers() @@ -196,7 +208,8 @@ const FeaturesTrigger = () => { crossAxisOffset: 4, missingStartNode: !startNode, hasTriggerNode, - publishDisabled: !hasWorkflowNodes, + startNodeLimitExceeded, + publishDisabled: !hasWorkflowNodes || startNodeLimitExceeded, }} /> diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 92a0b110c7..dd6ca77e7e 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -123,6 +123,11 @@ const translation = { noHistory: 'No History', tagBound: 'Number of apps using this tag', }, + publishLimit: { + startNodeTitlePrefix: 'Upgrade to', + startNodeTitleSuffix: 'unlock unlimited start nodes', + startNodeDesc: 'You’ve 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.', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 07241b8c4f..139388e280 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -119,6 +119,11 @@ const translation = { tagBound: 'このタグを使用しているアプリの数', moreActions: 'さらにアクション', }, + publishLimit: { + startNodeTitlePrefix: 'アップグレードして', + startNodeTitleSuffix: '開始ノードの上限を解除', + startNodeDesc: '現在のプランでは開始ノードは2個までです。公開するにはプランをアップグレードしてください。', + }, env: { envPanelTitle: '環境変数', envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 18e76caa64..a7f2b03bfa 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -122,6 +122,11 @@ const translation = { noHistory: '没有历史版本', tagBound: '使用此标签的应用数量', }, + publishLimit: { + startNodeTitlePrefix: '升级以', + startNodeTitleSuffix: '解锁无限开始节点', + startNodeDesc: '当前套餐最多支持 2 个开始节点。升级套餐即可发布此工作流。', + }, env: { envPanelTitle: '环境变量', envDescription: '环境变量是一种存储敏感信息的方法,如 API 密钥、数据库密码等。它们被存储在工作流程中,而不是代码中,以便在不同环境中共享。', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index ce053d6e5b..5917eb95ed 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -116,6 +116,11 @@ const translation = { currentWorkflow: '當前工作流程', moreActions: '更多動作', }, + publishLimit: { + startNodeTitlePrefix: '升級以', + startNodeTitleSuffix: '解鎖無限開始節點', + startNodeDesc: '目前方案最多允許 2 個開始節點,升級後才能發布此工作流程。', + }, env: { envPanelTitle: '環境變數', envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與 DSL 文件分開。',