From 47637da7346b1b4f05ef36582d66712180d1a9f6 Mon Sep 17 00:00:00 2001 From: NFish Date: Mon, 6 Jan 2025 10:47:38 +0800 Subject: [PATCH] wip: adjust self hosted page style --- web/app/components/billing/config.ts | 15 -- web/app/components/billing/pricing/index.tsx | 26 ++- .../components/billing/pricing/plan-item.tsx | 108 +-------- .../billing/pricing/select-plan-range.tsx | 18 +- .../billing/pricing/self-hosted-plan-item.tsx | 212 ++++++++++++++++++ web/app/components/billing/type.ts | 26 ++- web/i18n/en-US/billing.ts | 46 +++- 7 files changed, 304 insertions(+), 147 deletions(-) create mode 100644 web/app/components/billing/pricing/self-hosted-plan-item.tsx diff --git a/web/app/components/billing/config.ts b/web/app/components/billing/config.ts index 74afcc09fc..bc33b899e7 100644 --- a/web/app/components/billing/config.ts +++ b/web/app/components/billing/config.ts @@ -54,21 +54,6 @@ export const ALL_PLANS: Record = { annotatedResponse: 5000, logHistory: NUM_INFINITE, }, - enterprise: { - level: 4, - price: 0, - modelProviders: supportModelProviders, - teamMembers: NUM_INFINITE, - buildApps: NUM_INFINITE, - documents: 50, - vectorSpace: NUM_INFINITE, - documentsRequestQuota: 5000, - documentProcessingPriority: Priority.topPriority, - logHistory: NUM_INFINITE, - customTools: NUM_INFINITE, - messageRequest: contractSales, - annotatedResponse: NUM_INFINITE, - }, } export const defaultPlan = { diff --git a/web/app/components/billing/pricing/index.tsx b/web/app/components/billing/pricing/index.tsx index 98f7008069..056099e3b7 100644 --- a/web/app/components/billing/pricing/index.tsx +++ b/web/app/components/billing/pricing/index.tsx @@ -5,10 +5,11 @@ import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react' import Link from 'next/link' -import { Plan } from '../type' +import { Plan, SelfHostedPlan } from '../type' import TabSlider from '../../base/tab-slider' import SelectPlanRange, { PlanRange } from './select-plan-range' import PlanItem from './plan-item' +import SelfHostedPlanItem from './self-hosted-plan-item' import { useProviderContext } from '@/context/provider-context' import GridMask from '@/app/components/base/grid-mask' import { useAppContext } from '@/context/app-context' @@ -26,7 +27,7 @@ const Pricing: FC = ({ const canPay = isCurrentWorkspaceManager const [planRange, setPlanRange] = React.useState(PlanRange.monthly) - const [currentPlan, setCurrentPlan] = React.useState('cloud') + const [currentPlan, setCurrentPlan] = React.useState('self') return createPortal(
= ({ { value: 'self', text:
self hosted
}]} onChange={v => setCurrentPlan(v)} /> - + />}
@@ -90,9 +91,18 @@ const Pricing: FC = ({ /> } {currentPlan === 'self' && <> - + + @@ -102,7 +112,7 @@ const Pricing: FC = ({
- Compare plans & features + {t('billing.plansCommon.comparePlanAndFeatures')}
diff --git a/web/app/components/billing/pricing/plan-item.tsx b/web/app/components/billing/pricing/plan-item.tsx index 24f588d94a..f08cc2f3ab 100644 --- a/web/app/components/billing/pricing/plan-item.tsx +++ b/web/app/components/billing/pricing/plan-item.tsx @@ -4,7 +4,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine } from '@remixicon/react' import { Plan } from '../type' -import { ALL_PLANS, NUM_INFINITE, contactSalesUrl } from '../config' +import { ALL_PLANS, NUM_INFINITE } from '../config' import Toast from '../../base/toast' import Tooltip from '../../base/tooltip' import Divider from '../../base/divider' @@ -60,43 +60,24 @@ const style = { description: 'text-util-colors-indigo-indigo-600', btnStyle: 'bg-components-button-indigo-bg hover:bg-components-button-indigo-bg-hover border border-components-button-primary-border text-components-button-primary-text', }, - [Plan.enterprise]: { - icon: 'text-[#DC6803]', - description: '', - btnStyle: 'hover:shadow-lg hover:!text-white hover:!bg-[#F79009] hover:!border-[#DC6803] active:!text-white active:!bg-[#DC6803] active:!border-[#DC6803]', - }, } const PlanItem: FC = ({ plan, currentPlan, planRange, - canPay, }) => { const { t } = useTranslation() - // const { locale } = useContext(I18n) - - // const isZh = locale === LanguagesSupported[1] const [loading, setLoading] = React.useState(false) const i18nPrefix = `billing.plans.${plan}` const isFreePlan = plan === Plan.sandbox - const isEnterprisePlan = plan === Plan.enterprise const isMostPopularPlan = plan === Plan.professional const planInfo = ALL_PLANS[plan] const isYear = planRange === PlanRange.yearly const isCurrent = plan === currentPlan - const isPlanDisabled = planInfo.level <= ALL_PLANS[currentPlan].level || (!canPay && plan !== Plan.enterprise) + const isPlanDisabled = planInfo.level <= ALL_PLANS[currentPlan].level const { isCurrentWorkspaceManager } = useAppContext() - // const messagesRequest = (() => { - // const value = planInfo.messageRequest[isZh ? 'zh' : 'en'] - // if (value === contractSales) - // return t('billing.plansCommon.contractSales') - // return value - // })() const btnText = (() => { - if (!canPay && plan !== Plan.enterprise) - return t('billing.plansCommon.contractOwner') - if (isCurrent) return t('billing.plansCommon.currentPlan') @@ -104,83 +85,9 @@ const PlanItem: FC = ({ [Plan.sandbox]: t('billing.plansCommon.startForFree'), [Plan.professional]: t('billing.plansCommon.getStarted'), [Plan.team]: t('billing.plansCommon.getStarted'), - [Plan.enterprise]: t('billing.plansCommon.talkToSales'), })[plan] })() - // const comingSoon = ( - //
{t('billing.plansCommon.comingSoon')}
- // ) - // const supportContent = (() => { - // switch (plan) { - // case Plan.sandbox: - // return (
- //
{t('billing.plansCommon.supportItems.communityForums')}
- //
{t('billing.plansCommon.supportItems.agentMode')}
- //
- //
- //
 {t('billing.plansCommon.supportItems.workflow')}
- //
- //
- //
) - // case Plan.professional: - // return ( - //
- //
{t('billing.plansCommon.supportItems.emailSupport')}
- //
- //
+ {t('billing.plansCommon.supportItems.logoChange')}
- //
- //
- //
+ {t('billing.plansCommon.supportItems.bulkUpload')}
- //
- //
- // + - //
{t('billing.plansCommon.supportItems.llmLoadingBalancing')}
- // {t('billing.plansCommon.supportItems.llmLoadingBalancingTooltip')}
- // } - // /> - //
- //
- //
- // + - //
 {t('billing.plansCommon.supportItems.ragAPIRequest')}
- // {t('billing.plansCommon.ragAPIRequestTooltip')}
- // } - // /> - //
- //
{comingSoon}
- //
- // - // ) - // case Plan.team: - // return ( - //
- //
{t('billing.plansCommon.supportItems.priorityEmail')}
- //
- //
+ {t('billing.plansCommon.supportItems.SSOAuthentication')}
- //
{comingSoon}
- //
- //
- // ) - // case Plan.enterprise: - // return ( - //
- //
{t('billing.plansCommon.supportItems.personalizedSupport')}
- //
- //
+ {t('billing.plansCommon.supportItems.dedicatedAPISupport')}
- //
- //
- //
+ {t('billing.plansCommon.supportItems.customIntegration')}
- //
- //
- // ) - // default: - // return '' - // } - // })() + const handleGetPayUrl = async () => { if (loading) return @@ -191,10 +98,6 @@ const PlanItem: FC = ({ if (isFreePlan) return - if (isEnterprisePlan) { - window.location.href = contactSalesUrl - return - } // Only workspace manager can buy plan if (!isCurrentWorkspaceManager) { Toast.notify({ @@ -236,10 +139,7 @@ const PlanItem: FC = ({ {isFreePlan && (
{t('billing.plansCommon.free')}
)} - {isEnterprisePlan && ( -
{t('billing.plansCommon.contactSales')}
- )} - {!isFreePlan && !isEnterprisePlan && ( + {!isFreePlan && (
${isYear ? planInfo.price * 10 : planInfo.price}
diff --git a/web/app/components/billing/pricing/select-plan-range.tsx b/web/app/components/billing/pricing/select-plan-range.tsx index d4ce96ae44..317a038530 100644 --- a/web/app/components/billing/pricing/select-plan-range.tsx +++ b/web/app/components/billing/pricing/select-plan-range.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' import Switch from '../../base/switch' -import cn from '@/utils/classnames' export enum PlanRange { monthly = 'monthly', yearly = 'yearly', @@ -14,26 +13,15 @@ type Props = { onChange: (value: PlanRange) => void } -const ITem: FC<{ isActive: boolean; value: PlanRange; text: string; onClick: (value: PlanRange) => void }> = ({ isActive, value, text, onClick }) => { - return ( -
onClick(value)} - > - {text} -
- ) -} - const ArrowIcon = ( - + - - + + diff --git a/web/app/components/billing/pricing/self-hosted-plan-item.tsx b/web/app/components/billing/pricing/self-hosted-plan-item.tsx new file mode 100644 index 0000000000..98faf2c688 --- /dev/null +++ b/web/app/components/billing/pricing/self-hosted-plan-item.tsx @@ -0,0 +1,212 @@ +'use client' +import type { FC, ReactNode } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { RiAsterisk, RiBrain2Line, RiBuildingLine, RiCheckLine, RiQuestionLine, RiVipDiamondLine } from '@remixicon/react' +import { SelfHostedPlan } from '../type' +import { contactSalesUrl } from '../config' +import Toast from '../../base/toast' +import Tooltip from '../../base/tooltip' +import { PlanRange } from './select-plan-range' +import cn from '@/utils/classnames' +import { useAppContext } from '@/context/app-context' +import { fetchSubscriptionUrls } from '@/service/billing' + +type Props = { + plan: SelfHostedPlan + planRange: PlanRange + canPay: boolean +} + +const AWSMarketplaceLogo = () => { + return + + + + + + + + + + + + + + + + + + + + + + +} + +const AzureIcon = () => { + return + + + + + + + + + + + + + + + + + + + + + + +} + +const GoogleCloudIcon = () => { + return + + + + + +} + +const KeyValue = ({ label, tooltip }: { icon: ReactNode; label: string; tooltip?: string }) => { + return ( +
+
+ +
+
{label}
+ {tooltip && ( + +
+ +
+
+ )} +
+ ) +} + +const style = { + [SelfHostedPlan.community]: { + icon: , + description: 'text-util-colors-gray-gray-600', + btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary', + }, + [SelfHostedPlan.premium]: { + icon: , + description: 'text-text-warning', + btnStyle: 'bg-third-party-aws hover:bg-third-party-aws-hover border border-components-button-primary-border text-text-primary-on-surface shadow-xs', + }, + [SelfHostedPlan.enterprise]: { + icon: , + description: '', + btnStyle: 'hover:shadow-lg hover:!text-white hover:!bg-[#F79009] hover:!border-[#DC6803] active:!text-white active:!bg-[#DC6803] active:!border-[#DC6803]', + }, +} +const PlanItem: FC = ({ + plan, + planRange, +}) => { + const { t } = useTranslation() + const isFreePlan = plan === SelfHostedPlan.community + const isPremiumPlan = plan === SelfHostedPlan.premium + const [loading, setLoading] = React.useState(false) + const i18nPrefix = `billing.plans.${plan}` + const isEnterprisePlan = plan === SelfHostedPlan.enterprise + const isYear = planRange === PlanRange.yearly + const { isCurrentWorkspaceManager } = useAppContext() + const features = t(`${i18nPrefix}.features`, { returnObjects: true }) as string[] + const handleGetPayUrl = async () => { + if (loading) + return + + if (isEnterprisePlan) { + window.location.href = contactSalesUrl + return + } + // Only workspace manager can buy plan + if (!isCurrentWorkspaceManager) { + Toast.notify({ + type: 'error', + message: t('billing.buyPermissionDeniedTip'), + className: 'z-[1001]', + }) + return + } + setLoading(true) + try { + const res = await fetchSubscriptionUrls(plan, isYear ? 'year' : 'month') + // Adb Block additional tracking block the gtag, so we need to redirect directly + window.location.href = res.url + } + finally { + setLoading(false) + } + } + return ( +
+
+ {style[plan].icon} +
+
{t(`${i18nPrefix}.name`)}
+
+
{t(`${i18nPrefix}.description`)}
+
+
+
+
{t(`${i18nPrefix}.price`)}
+ {!isFreePlan &&
+
+ {t(`${i18nPrefix}.priceTip`)} +
+
} +
+
+ +
+ {t(`${i18nPrefix}.btnText`)} +
+
{t(`${i18nPrefix}.includesTitle`)}
+
+ {features.map(v => + } + label={v} + />)} +
+ {isPremiumPlan &&
+
+
+ +
+
+ +
+
+ {t('billing.plans.premium.comingSoon')} +
} +
+ ) +} +export default React.memo(PlanItem) diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index b70c88da43..29dd4985f7 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -2,9 +2,7 @@ export enum Plan { sandbox = 'sandbox', professional = 'professional', team = 'team', - enterprise = 'enterprise', } - export enum Priority { standard = 'standard', priority = 'priority', @@ -26,7 +24,29 @@ export type PlanInfo = { annotatedResponse: number } -export type UsagePlanInfo = Pick +export enum SelfHostedPlan { + community = 'community', + premium = 'premium', + enterprise = 'enterprise', +} + +export type SelfHostedPlanInfo = { + level: number + price: number + modelProviders: string + teamWorkspace: number + teamMembers: number + buildApps: number + documents: number + vectorSpace: string + documentsRequestQuota: number + documentProcessingPriority: Priority + logHistory: number + messageRequest: number + annotatedResponse: number +} + +export type UsagePlanInfo = Pick export enum DocumentProcessingPriority { standard = 'standard', diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts index 5bdb49bae9..e2e18b756f 100644 --- a/web/i18n/en-US/billing.ts +++ b/web/i18n/en-US/billing.ts @@ -23,6 +23,7 @@ const translation = { save: 'Save ', free: 'Free', annualBilling: 'Annual Billing', + comparePlanAndFeatures: 'Compare plans & features', priceTip: 'per workspace/', currentPlan: 'Current Plan', contractSales: 'Contact sales', @@ -102,10 +103,51 @@ const translation = { description: 'For Medium-sized Teams', includesTitle: 'Everything in Professional plan, plus:', }, + community: { + name: 'Community', + description: 'For Individual Users, Small Teams, or Non-commercial Projects', + price: 'Free', + btnText: 'Get Started with Community', + includesTitle: 'Free Features:', + features: [ + 'All Core Features Released Under the Public Repository', + 'Single Workspace', + 'Complies with Dify Open Source License', + ], + }, + premium: { + name: 'Premium', + description: 'For Mid-sized Organizations and Teams', + price: 'Scalable', + priceTip: 'Based on Cloud Marketplace', + btnText: 'Get Premium in AWS Marketplace', + includesTitle: 'Everything from Community, plus:', + comingSoon: 'Microsoft Azure & Google Cloud Support Coming Soon', + features: [ + 'Self-managed Reliability by Various Cloud Providers', + 'Single Workspace', + 'WebApp Logo & Branding Customization', + 'Priority Email & Chat Support', + ], + }, enterprise: { name: 'Enterprise', - description: 'Get full capabilities and support for large-scale mission-critical systems.', - includesTitle: 'Everything in Team plan, plus:', + description: 'For Enterprise Require Organization-wide Security, Compliance, Scalability, Control and More Advanced Features', + price: 'Custom', + priceTip: 'Annual Billing Only', + btnText: 'Contact Sales', + includesTitle: 'Everything from Premium, plus:', + features: [ + 'Enterprise-grade Scalable Deployment Solutions', + 'Commercial License Authorization', + 'Exclusive Enterprise Features', + 'Multiple Workspaces & Enterprise Management', + 'SSO', + 'Negotiated SLAs by Dify Partners', + 'Advanced Security & Controls', + 'Updates and Maintenance by Dify Officially', + 'Professional Technical Support', + ], }, }, vectorSpace: {