Fix: new upgrade page (#12417)

This commit is contained in:
NFish
2025-03-06 10:27:13 +08:00
committed by GitHub
parent a4b2c10fb8
commit 9962118dbd
50 changed files with 3958 additions and 607 deletions

View File

@@ -1,98 +1,82 @@
import { Plan, type PlanInfo, Priority } from '@/app/components/billing/type'
const supportModelProviders = 'OpenAI/Anthropic/Azure OpenAI/ Llama2/Hugging Face/Replicate'
const supportModelProviders = 'OpenAI/Anthropic/Llama2/Azure OpenAI/Hugging Face/Replicate'
export const NUM_INFINITE = 99999999
export const contractSales = 'contractSales'
export const unAvailable = 'unAvailable'
export const contactSalesUrl = 'mailto:business@dify.ai'
export const contactSalesUrl = 'https://vikgc6bnu1s.typeform.com/to/mowuXTQH'
export const getStartedWithCommunityUrl = 'https://github.com/langgenius/dify'
export const getWithPremiumUrl = 'https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6'
export const ALL_PLANS: Record<Plan, PlanInfo> = {
sandbox: {
level: 1,
price: 0,
modelProviders: supportModelProviders,
teamWorkspace: 1,
teamMembers: 1,
buildApps: 10,
vectorSpace: 5,
documentsUploadQuota: 50,
buildApps: 5,
documents: 50,
vectorSpace: '50MB',
documentsUploadQuota: 0,
documentsRequestQuota: 10,
documentProcessingPriority: Priority.standard,
logHistory: 30,
customTools: unAvailable,
messageRequest: {
en: '200 messages',
zh: '200 条信息',
},
messageRequest: 200,
annotatedResponse: 10,
logHistory: 30,
},
professional: {
level: 2,
price: 59,
modelProviders: supportModelProviders,
teamWorkspace: 1,
teamMembers: 3,
buildApps: 50,
vectorSpace: 200,
documentsUploadQuota: 500,
documents: 500,
vectorSpace: '5GB',
documentsUploadQuota: 0,
documentsRequestQuota: 100,
documentProcessingPriority: Priority.priority,
logHistory: NUM_INFINITE,
customTools: 10,
messageRequest: {
en: '5,000 messages/month',
zh: '5,000 条信息/月',
},
messageRequest: 5000,
annotatedResponse: 2000,
logHistory: NUM_INFINITE,
},
team: {
level: 3,
price: 159,
modelProviders: supportModelProviders,
teamMembers: NUM_INFINITE,
buildApps: NUM_INFINITE,
vectorSpace: 1000,
documentsUploadQuota: 1000,
teamWorkspace: 1,
teamMembers: 50,
buildApps: 200,
documents: 1000,
vectorSpace: '20GB',
documentsUploadQuota: 0,
documentsRequestQuota: 1000,
documentProcessingPriority: Priority.topPriority,
logHistory: NUM_INFINITE,
customTools: NUM_INFINITE,
messageRequest: {
en: '10,000 messages/month',
zh: '10,000 条信息/月',
},
messageRequest: 10000,
annotatedResponse: 5000,
},
enterprise: {
level: 4,
price: 0,
modelProviders: supportModelProviders,
teamMembers: NUM_INFINITE,
buildApps: NUM_INFINITE,
vectorSpace: NUM_INFINITE,
documentsUploadQuota: NUM_INFINITE,
documentProcessingPriority: Priority.topPriority,
logHistory: NUM_INFINITE,
customTools: NUM_INFINITE,
messageRequest: {
en: contractSales,
zh: contractSales,
},
annotatedResponse: NUM_INFINITE,
},
}
export const defaultPlan = {
type: Plan.sandbox,
usage: {
documents: 50,
vectorSpace: 1,
buildApps: 1,
teamMembers: 1,
annotatedResponse: 1,
documentsUploadQuota: 1,
documentsUploadQuota: 0,
},
total: {
documents: 50,
vectorSpace: 10,
buildApps: 10,
teamMembers: 1,
annotatedResponse: 10,
documentsUploadQuota: 50,
documentsUploadQuota: 0,
},
}

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Plan } from '../type'
import { Plan, SelfHostedPlan } from '../type'
import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn'
@@ -26,7 +26,7 @@ const typeStyle = {
textClassNames: 'text-[#3538CD]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #E0EAFF',
},
[Plan.enterprise]: {
[SelfHostedPlan.enterprise]: {
textClassNames: 'text-[#DC6803]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #FFEED3',
},
@@ -89,7 +89,7 @@ const PlanComp: FC<Props> = ({
<UsageInfo
className='py-3'
Icon={User01}
name={t('billing.plansCommon.teamMembers')}
name={t('billing.usagePage.teamMembers')}
usage={usage.teamMembers}
total={total.teamMembers}
/>
@@ -98,14 +98,14 @@ const PlanComp: FC<Props> = ({
<UsageInfo
className='py-3'
Icon={MessageFastPlus}
name={t('billing.plansCommon.annotationQuota')}
name={t('billing.usagePage.annotationQuota')}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
/>
<UsageInfo
className='py-3'
Icon={FileUpload}
name={t('billing.plansCommon.documentsUploadQuota')}
name={t('billing.usagePage.documentsUploadQuota')}
usage={usage.documentsUploadQuota}
total={total.documentsUploadQuota}
/>

View File

@@ -3,13 +3,18 @@ import type { FC } from 'react'
import React from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { Plan } from '../type'
import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react'
import Link from 'next/link'
import { useKeyPress } from 'ahooks'
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'
import classNames from '@/utils/classnames'
type Props = {
onCancel: () => void
@@ -24,56 +29,112 @@ const Pricing: FC<Props> = ({
const canPay = isCurrentWorkspaceManager
const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
const [currentPlan, setCurrentPlan] = React.useState<string>('cloud')
useKeyPress(['esc'], onCancel)
return createPortal(
<div
className='fixed inset-0 flex bg-white z-[1000] overflow-auto'
className='fixed inset-0 top-0 right-0 bottom-0 left-0 p-4 bg-background-overlay-backdrop backdrop-blur-[6px] z-[1000]'
onClick={e => e.stopPropagation()}
>
<GridMask wrapperClassName='grow'>
<div className='grow width-[0] mt-6 p-6 flex flex-col items-center'>
<div className='mb-3 leading-[38px] text-[30px] font-semibold text-gray-900'>
{t('billing.plansCommon.title')}
</div>
<SelectPlanRange
value={planRange}
onChange={setPlanRange}
/>
<div className='mt-8 pb-6 w-full justify-center flex-nowrap flex space-x-3'>
<PlanItem
currentPlan={plan.type}
plan={Plan.sandbox}
planRange={planRange}
canPay={canPay}
/>
<PlanItem
currentPlan={plan.type}
plan={Plan.professional}
planRange={planRange}
canPay={canPay}
/>
<PlanItem
currentPlan={plan.type}
plan={Plan.team}
planRange={planRange}
canPay={canPay}
/>
<PlanItem
currentPlan={plan.type}
plan={Plan.enterprise}
planRange={planRange}
canPay={canPay}
/>
</div>
<div className='w-full h-full relative overflow-auto rounded-2xl border border-effects-highlight bg-saas-background'>
<div
className='fixed top-7 right-7 flex items-center justify-center w-9 h-9 bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover rounded-[10px] cursor-pointer z-[1001]'
onClick={onCancel}
>
<RiCloseLine className='size-5 text-components-button-tertiary-text' />
</div>
</GridMask>
<GridMask wrapperClassName='w-full min-h-full' canvasClassName='min-h-full'>
<div className='pt-12 px-8 pb-7 flex flex-col items-center'>
<div className='mb-2 title-5xl-bold text-text-primary'>
{t('billing.plansCommon.title')}
</div>
<div className='system-sm-regular text-text-secondary'>
<span>{t('billing.plansCommon.freeTrialTipPrefix')}</span>
<span className='text-gradient font-semibold'>{t('billing.plansCommon.freeTrialTip')}</span>
<span>{t('billing.plansCommon.freeTrialTipSuffix')}</span>
</div>
</div>
<div className='w-[1152px] mx-auto'>
<div className='py-2 flex items-center justify-between h-[64px]'>
<TabSlider
value={currentPlan}
itemWidth={170}
className='inline-flex'
options={[
{
value: 'cloud',
text: <div className={
classNames('inline-flex items-center system-md-semibold-uppercase text-text-secondary',
currentPlan === 'cloud' && 'text-text-accent-light-mode-only')} >
<RiCloudFill className='size-4 mr-2' />{t('billing.plansCommon.cloud')}</div>,
},
{
value: 'self',
text: <div className={
classNames('inline-flex items-center system-md-semibold-uppercase text-text-secondary',
currentPlan === 'self' && 'text-text-accent-light-mode-only')}>
<RiTerminalBoxFill className='size-4 mr-2' />{t('billing.plansCommon.self')}</div>,
}]}
onChange={v => setCurrentPlan(v)} />
<div
className='fixed top-6 right-6 flex items-center justify-center w-10 h-10 bg-black/[0.05] rounded-full backdrop-blur-[2px] cursor-pointer z-[1001]'
onClick={onCancel}
>
<RiCloseLine className='w-4 h-4 text-gray-900' />
</div>
</div>,
{currentPlan === 'cloud' && <SelectPlanRange
value={planRange}
onChange={setPlanRange}
/>}
</div>
<div className='pt-3 pb-8'>
<div className='flex justify-center flex-nowrap gap-x-4'>
{currentPlan === 'cloud' && <>
<PlanItem
currentPlan={plan.type}
plan={Plan.sandbox}
planRange={planRange}
canPay={canPay}
/>
<PlanItem
currentPlan={plan.type}
plan={Plan.professional}
planRange={planRange}
canPay={canPay}
/>
<PlanItem
currentPlan={plan.type}
plan={Plan.team}
planRange={planRange}
canPay={canPay}
/>
</>}
{currentPlan === 'self' && <>
<SelfHostedPlanItem
plan={SelfHostedPlan.community}
planRange={planRange}
canPay={canPay}
/>
<SelfHostedPlanItem
plan={SelfHostedPlan.premium}
planRange={planRange}
canPay={canPay}
/>
<SelfHostedPlanItem
plan={SelfHostedPlan.enterprise}
planRange={planRange}
canPay={canPay}
/>
</>}
</div>
</div>
</div>
<div className='py-4 flex items-center justify-center'>
<div className='px-3 py-2 flex items-center justify-center gap-x-0.5 text-components-button-secondary-accent-text rounded-lg hover:bg-state-accent-hover hover:cursor-pointer'>
<Link href='https://dify.ai/pricing#plans-and-features' className='system-sm-medium'>{t('billing.plansCommon.comparePlanAndFeatures')}</Link>
<RiArrowRightUpLine className='size-4' />
</div>
</div>
</GridMask>
</div >
</div >,
document.body,
)
}

View File

@@ -1,18 +1,18 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
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, contractSales, unAvailable } from '../config'
import { ALL_PLANS, NUM_INFINITE } from '../config'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
import Divider from '../../base/divider'
import { ArCube1, Group2, Keyframe, SparklesSoft } from '../../base/icons/src/public/billing'
import { PlanRange } from './select-plan-range'
import cn from '@/utils/classnames'
import { useAppContext } from '@/context/app-context'
import { fetchSubscriptionUrls } from '@/service/billing'
import { LanguagesSupported } from '@/i18n/language'
import I18n from '@/context/i18n'
type Props = {
currentPlan: Plan
@@ -21,162 +21,76 @@ type Props = {
canPay: boolean
}
const KeyValue = ({ label, value, tooltip }: { label: string; value: string | number | JSX.Element; tooltip?: string }) => {
const KeyValue = ({ icon, label, tooltip }: { icon: ReactNode; label: string; tooltip?: ReactNode }) => {
return (
<div className='mt-3.5 leading-[125%] text-[13px] font-medium'>
<div className='flex items-center text-gray-500 space-x-1'>
<div>{label}</div>
{tooltip && (
<Tooltip
popupContent={
<div className='w-[200px]'>{tooltip}</div>
}
/>
)}
<div className='flex text-text-tertiary'>
<div className='size-4 flex items-center justify-center'>
{icon}
</div>
<div className='mt-0.5 text-gray-900'>{value}</div>
<div className='ml-2 mr-0.5 text-text-primary system-sm-regular'>{label}</div>
{tooltip && (
<Tooltip
asChild
popupContent={tooltip}
popupClassName='w-[200px]'
>
<div className='size-4 flex items-center justify-center'>
<RiQuestionLine className='text-text-quaternary' />
</div>
</Tooltip>
)}
</div>
)
}
const priceClassName = 'leading-[32px] text-[28px] font-bold text-gray-900'
const priceClassName = 'leading-[125%] text-[28px] font-bold text-text-primary'
const style = {
[Plan.sandbox]: {
bg: 'bg-[#F2F4F7]',
title: 'text-gray-900',
hoverAndActive: '',
icon: <ArCube1 className='text-text-primary size-7' />,
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',
btnDisabledStyle: 'bg-components-button-secondary-bg-disabled hover:bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled',
},
[Plan.professional]: {
bg: 'bg-[#E0F2FE]',
title: 'text-[#026AA2]',
hoverAndActive: 'hover:shadow-lg hover:!text-white hover:!bg-[#0086C9] hover:!border-[#026AA2] active:!text-white active:!bg-[#026AA2] active:!border-[#026AA2]',
icon: <Keyframe className='text-util-colors-blue-brand-blue-brand-600 size-7' />,
description: 'text-util-colors-blue-brand-blue-brand-600',
btnStyle: 'bg-components-button-primary-bg hover:bg-components-button-primary-bg-hover border border-components-button-primary-border text-components-button-primary-text',
btnDisabledStyle: 'bg-components-button-primary-bg-disabled hover:bg-components-button-primary-bg-disabled border-components-button-primary-border-disabled text-components-button-primary-text-disabled',
},
[Plan.team]: {
bg: 'bg-[#E0EAFF]',
title: 'text-[#3538CD]',
hoverAndActive: 'hover:shadow-lg hover:!text-white hover:!bg-[#444CE7] hover:!border-[#3538CD] active:!text-white active:!bg-[#3538CD] active:!border-[#3538CD]',
},
[Plan.enterprise]: {
bg: 'bg-[#FFEED3]',
title: 'text-[#DC6803]',
hoverAndActive: 'hover:shadow-lg hover:!text-white hover:!bg-[#F79009] hover:!border-[#DC6803] active:!text-white active:!bg-[#DC6803] active:!border-[#DC6803]',
icon: <Group2 className='text-util-colors-indigo-indigo-600 size-7' />,
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',
btnDisabledStyle: 'bg-components-button-indigo-bg-disabled hover:bg-components-button-indigo-bg-disabled border-components-button-indigo-border-disabled text-components-button-primary-text-disabled',
},
}
const PlanItem: FC<Props> = ({
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')
return ({
[Plan.sandbox]: t('billing.plansCommon.startForFree'),
[Plan.professional]: <>{t('billing.plansCommon.getStartedWith')}<span className='capitalize'>&nbsp;{plan}</span></>,
[Plan.team]: <>{t('billing.plansCommon.getStartedWith')}<span className='capitalize'>&nbsp;{plan}</span></>,
[Plan.enterprise]: t('billing.plansCommon.talkToSales'),
[Plan.professional]: t('billing.plansCommon.getStarted'),
[Plan.team]: t('billing.plansCommon.getStarted'),
})[plan]
})()
const comingSoon = (
<div className='leading-[12px] text-[9px] font-semibold text-[#3538CD] uppercase'>{t('billing.plansCommon.comingSoon')}</div>
)
const supportContent = (() => {
switch (plan) {
case Plan.sandbox:
return (<div className='space-y-3.5'>
<div>{t('billing.plansCommon.supportItems.communityForums')}</div>
<div>{t('billing.plansCommon.supportItems.agentMode')}</div>
<div className='flex items-center space-x-1'>
<div className='flex items-center'>
<div className='mr-0.5'>&nbsp;{t('billing.plansCommon.supportItems.workflow')}</div>
</div>
</div>
</div>)
case Plan.professional:
return (
<div>
<div>{t('billing.plansCommon.supportItems.emailSupport')}</div>
<div className='mt-3.5 flex items-center space-x-1'>
<div>+ {t('billing.plansCommon.supportItems.logoChange')}</div>
</div>
<div className='mt-3.5 flex items-center space-x-1'>
<div>+ {t('billing.plansCommon.supportItems.bulkUpload')}</div>
</div>
<div className='mt-3.5 flex items-center space-x-1'>
<span>+ </span>
<div>{t('billing.plansCommon.supportItems.llmLoadingBalancing')}</div>
<Tooltip
popupContent={
<div className='w-[200px]'>{t('billing.plansCommon.supportItems.llmLoadingBalancingTooltip')}</div>
}
/>
</div>
<div className='mt-3.5 flex items-center space-x-1'>
<div className='flex items-center'>
+
<div className='mr-0.5'>&nbsp;{t('billing.plansCommon.supportItems.ragAPIRequest')}</div>
<Tooltip
popupContent={
<div className='w-[200px]'>{t('billing.plansCommon.ragAPIRequestTooltip')}</div>
}
/>
</div>
<div>{comingSoon}</div>
</div>
</div>
)
case Plan.team:
return (
<div>
<div>{t('billing.plansCommon.supportItems.priorityEmail')}</div>
<div className='mt-3.5 flex items-center space-x-1'>
<div>+ {t('billing.plansCommon.supportItems.SSOAuthentication')}</div>
<div>{comingSoon}</div>
</div>
</div>
)
case Plan.enterprise:
return (
<div>
<div>{t('billing.plansCommon.supportItems.personalizedSupport')}</div>
<div className='mt-3.5 flex items-center space-x-1'>
<div>+ {t('billing.plansCommon.supportItems.dedicatedAPISupport')}</div>
</div>
<div className='mt-3.5 flex items-center space-x-1'>
<div>+ {t('billing.plansCommon.supportItems.customIntegration')}</div>
</div>
</div>
)
default:
return ''
}
})()
const handleGetPayUrl = async () => {
if (loading)
return
@@ -187,10 +101,6 @@ const PlanItem: FC<Props> = ({
if (isFreePlan)
return
if (isEnterprisePlan) {
window.location.href = contactSalesUrl
return
}
// Only workspace manager can buy plan
if (!isCurrentWorkspaceManager) {
Toast.notify({
@@ -211,90 +121,103 @@ const PlanItem: FC<Props> = ({
}
}
return (
<div className={cn(isMostPopularPlan ? 'bg-[#0086C9] p-0.5' : 'pt-7', 'flex flex-col min-w-[290px] w-[290px] rounded-xl')}>
{isMostPopularPlan && (
<div className='flex items-center h-7 justify-center leading-[12px] text-xs font-medium text-[#F5F8FF]'>{t('billing.plansCommon.mostPopular')}</div>
)}
<div className={cn(style[plan].bg, 'grow px-6 py-6 rounded-[10px]')}>
<div className={cn(style[plan].title, 'mb-1 leading-[125%] text-lg font-semibold')}>{t(`${i18nPrefix}.name`)}</div>
<div className={cn(isFreePlan ? 'mb-5 text-[#FB6514]' : 'mb-4 text-gray-500', 'h-8 leading-[125%] text-[13px] font-normal')}>{t(`${i18nPrefix}.description`)}</div>
<div className={cn('flex flex-col w-[373px] p-6 border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn rounded-2xl',
isMostPopularPlan ? 'shadow-lg backdrop-blur-[5px] border-effects-highlight' : 'hover:shadow-lg hover:backdrop-blur-[5px] hover:border-effects-highlight',
)}>
<div className='flex flex-col gap-y-1'>
{style[plan].icon}
<div className='flex items-center'>
<div className='leading-[125%] text-lg font-semibold uppercase text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 px-1 py-[3px] flex items-center justify-center rounded-full border-[0.5px] shadow-xs bg-price-premium-badge-background text-components-premium-badge-grey-text-stop-0'>
<div className='pl-0.5'>
<SparklesSoft className='size-3' />
</div>
<span className='px-0.5 system-2xs-semibold-uppercase bg-clip-text bg-price-premium-text-background text-transparent'>{t('billing.plansCommon.mostPopular')}</span>
</div>}
</div>
<div className={cn(style[plan].description, 'system-sm-regular')}>{t(`${i18nPrefix}.description`)}</div>
</div>
<div className='my-5'>
{/* Price */}
{isFreePlan && (
<div className={priceClassName}>{t('billing.plansCommon.free')}</div>
)}
{isEnterprisePlan && (
<div className={priceClassName}>{t('billing.plansCommon.contactSales')}</div>
)}
{!isFreePlan && !isEnterprisePlan && (
<div className='flex items-end h-9'>
{!isFreePlan && (
<div className='flex items-end'>
<div className={priceClassName}>${isYear ? planInfo.price * 10 : planInfo.price}</div>
<div className='ml-1'>
{isYear && <div className='leading-[18px] text-xs font-medium text-[#F26725]'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>}
<div className='leading-[18px] text-[15px] font-normal text-gray-500'>/{t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</div>
<div className='ml-1 flex flex-col'>
{isYear && <div className='leading-[14px] text-[14px] font-normal italic text-text-warning'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>}
<div className='leading-normal text-[14px] font-normal text-text-tertiary'>
{t('billing.plansCommon.priceTip')}
{t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</div>
</div>
</div>
)}
</div>
<div
className={cn(isMostPopularPlan && !isCurrent && '!bg-[#444CE7] !text-white !border !border-[#3538CD] shadow-sm', isPlanDisabled ? 'opacity-30' : `${style[plan].hoverAndActive} cursor-pointer`, 'mt-4 flex h-11 items-center justify-center border-[2px] border-gray-900 rounded-3xl text-sm font-semibold text-gray-900')}
onClick={handleGetPayUrl}
>
{btnText}
</div>
<div className='my-4 h-[1px] bg-black/5'></div>
<div className='leading-[125%] text-[13px] font-normal text-gray-900'>
{t(`${i18nPrefix}.includesTitle`)}
</div>
<div
className={cn('flex py-3 px-5 rounded-full justify-center items-center h-[42px]',
style[plan].btnStyle,
isPlanDisabled && style[plan].btnDisabledStyle,
isPlanDisabled ? 'cursor-not-allowed' : 'cursor-pointer')}
onClick={handleGetPayUrl}
>
{btnText}
</div>
<div className='flex flex-col gap-y-3 mt-6'>
<KeyValue
label={t('billing.plansCommon.messageRequest.title')}
value={messagesRequest}
icon={<RiChatAiLine />}
label={isFreePlan
? t('billing.plansCommon.messageRequest.title', { count: planInfo.messageRequest })
: t('billing.plansCommon.messageRequest.titlePerMonth', { count: planInfo.messageRequest })}
tooltip={t('billing.plansCommon.messageRequest.tooltip') as string}
/>
<KeyValue
icon={<RiBrain2Line />}
label={t('billing.plansCommon.modelProviders')}
value={planInfo.modelProviders}
/>
<KeyValue
label={t('billing.plansCommon.teamMembers')}
value={planInfo.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.teamMembers}
icon={<RiFolder6Line />}
label={t('billing.plansCommon.teamWorkspace', { count: planInfo.teamWorkspace })}
/>
<KeyValue
label={t('billing.plansCommon.buildApps')}
value={planInfo.buildApps === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.buildApps}
icon={<RiGroupLine />}
label={t('billing.plansCommon.teamMember', { count: planInfo.teamMembers })}
/>
<KeyValue
label={t('billing.plansCommon.vectorSpace')}
value={planInfo.vectorSpace === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : (planInfo.vectorSpace >= 1000 ? `${planInfo.vectorSpace / 1000}G` : `${planInfo.vectorSpace}MB`)}
tooltip={t('billing.plansCommon.vectorSpaceBillingTooltip') as string}
icon={<RiApps2Line />}
label={t('billing.plansCommon.buildApps', { count: planInfo.buildApps })}
/>
<Divider bgStyle='gradient' />
<KeyValue
icon={<RiBook2Line />}
label={t('billing.plansCommon.documents', { count: planInfo.documents })}
tooltip={t('billing.plansCommon.documentsTooltip') as string}
/>
<KeyValue
label={t('billing.plansCommon.documentsUploadQuota')}
value={planInfo.vectorSpace === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.documentsUploadQuota}
/>
<KeyValue
label={t('billing.plansCommon.documentProcessingPriority')}
value={t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`) as string}
icon={<RiHardDrive3Line />}
label={t('billing.plansCommon.vectorSpace', { size: planInfo.vectorSpace })}
tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
/>
<KeyValue
label={t('billing.plansCommon.annotatedResponse.title')}
value={planInfo.annotatedResponse === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.annotatedResponse}`}
icon={<RiSeoLine />}
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
/>
<KeyValue
icon={<RiProgress3Line />}
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
/>
<Divider bgStyle='gradient' />
<KeyValue
icon={<RiFileEditLine />}
label={t('billing.plansCommon.annotatedResponse.title', { count: planInfo.annotatedResponse })}
tooltip={t('billing.plansCommon.annotatedResponse.tooltip') as string}
/>
<KeyValue
label={t('billing.plansCommon.logsHistory')}
value={planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}`}
/>
<KeyValue
label={t('billing.plansCommon.customTools')}
value={planInfo.customTools === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : (planInfo.customTools === unAvailable ? t('billing.plansCommon.unavailable') as string : `${planInfo.customTools}`)}
/>
<KeyValue
label={t('billing.plansCommon.support')}
value={supportContent}
icon={<RiHistoryLine />}
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
/>
</div>
</div>

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import Switch from '../../base/switch'
export enum PlanRange {
monthly = 'monthly',
yearly = 'yearly',
@@ -13,22 +13,20 @@ type Props = {
onChange: (value: PlanRange) => void
}
const ITem: FC<{ isActive: boolean; value: PlanRange; text: string; onClick: (value: PlanRange) => void }> = ({ isActive, value, text, onClick }) => {
return (
<div
className={cn(isActive ? 'bg-[#155EEF] text-white' : 'text-gray-900', 'flex items-center px-8 h-11 rounded-[32px] cursor-pointer text-[15px] font-medium')}
onClick={() => onClick(value)}
>
{text}
</div>
)
}
const ArrowIcon = (
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="38" viewBox="0 0 26 38" fill="none">
<path d="M20.5005 3.49991C23.5 18 18.7571 25.2595 2.92348 31.9599" stroke="#F26725" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.21996 32.2756L8.37216 33.5812" stroke="#F26725" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.22168 32.2764L3.90351 27.4459" stroke="#F26725" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="29" viewBox="0 0 22 29" fill="none">
<g clipPath="url(#clip0_394_43518)">
<path d="M2.11312 1.64777C2.11312 1.64777 2.10178 1.64849 2.09045 1.6492C2.06211 1.65099 2.08478 1.64956 2.11312 1.64777ZM9.047 20.493C9.43106 19.9965 8.97268 19.2232 8.35639 19.2848C7.72208 19.4215 6.27243 20.3435 5.13995 20.8814C4.2724 21.3798 3.245 21.6892 2.54015 22.4221C1.87751 23.2831 2.70599 23.9706 3.47833 24.3088C4.73679 24.9578 6.00624 25.6004 7.25975 26.2611C8.4424 26.8807 9.57833 27.5715 10.7355 28.2383C10.9236 28.3345 11.1464 28.3489 11.3469 28.2794C11.9886 28.0796 12.0586 27.1137 11.4432 26.8282C9.83391 25.8485 8.17365 24.9631 6.50314 24.0955C8.93023 24.2384 11.3968 24.1058 13.5161 22.7945C16.6626 20.8097 19.0246 17.5714 20.2615 14.0854C22.0267 8.96164 18.9313 4.08153 13.9897 2.40722C10.5285 1.20289 6.76599 0.996166 3.14837 1.46306C2.50624 1.56611 2.68616 1.53201 2.10178 1.64849C2.12445 1.64706 2.14712 1.64563 2.16979 1.6442C2.01182 1.66553 1.86203 1.72618 1.75582 1.84666C1.48961 2.13654 1.58903 2.63096 1.9412 2.80222C2.19381 2.92854 2.4835 2.83063 2.74986 2.81385C3.7267 2.69541 4.70711 2.63364 5.69109 2.62853C8.30015 2.58932 10.5052 2.82021 13.2684 3.693C21.4149 6.65607 20.7135 14.2162 14.6733 20.0304C12.4961 22.2272 9.31209 22.8944 6.11128 22.4816C5.92391 22.4877 5.72342 22.4662 5.52257 22.439C6.35474 22.011 7.20002 21.6107 8.01305 21.1498C8.35227 20.935 8.81233 20.8321 9.05266 20.4926L9.047 20.493Z" fill="url(#paint0_linear_394_43518)" />
</g>
<defs>
<linearGradient id="paint0_linear_394_43518" x1="11" y1="-48.5001" x2="12.2401" y2="28.2518" gradientUnits="userSpaceOnUse">
<stop stopColor="#FDB022" />
<stop offset="1" stopColor="#F79009" />
</linearGradient>
<clipPath id="clip0_394_43518">
<rect width="19.1928" height="27.3696" fill="white" transform="translate(21.8271 27.6475) rotate(176.395)" />
</clipPath>
</defs>
</svg>
)
@@ -39,15 +37,16 @@ const SelectPlanRange: FC<Props> = ({
const { t } = useTranslation()
return (
<div>
<div className='mb-4 leading-[18px] text-sm font-medium text-[#F26725]'>{t('billing.plansCommon.yearlyTip')}</div>
<div className='inline-flex relative p-1 rounded-full bg-[#F5F8FF] border border-black/5'>
<ITem isActive={value === PlanRange.monthly} value={PlanRange.monthly} text={t('billing.plansCommon.planRange.monthly') as string} onClick={onChange} />
<ITem isActive={value === PlanRange.yearly} value={PlanRange.yearly} text={t('billing.plansCommon.planRange.yearly') as string} onClick={onChange} />
<div className='absolute right-0 top-[-16px] '>
{ArrowIcon}
</div>
<div className='relative flex flex-col items-end pr-6'>
<div className='text-sm italic bg-clip-text bg-premium-yearly-tip-text-background text-transparent'>{t('billing.plansCommon.yearlyTip')}</div>
<div className='flex items-center py-1'>
<span className='mr-2 text-[13px]'>{t('billing.plansCommon.annualBilling')}</span>
<Switch size='l' defaultValue={value === PlanRange.yearly} onChange={(v) => {
onChange(v ? PlanRange.yearly : PlanRange.monthly)
}} />
</div>
<div className='absolute right-0 top-2'>
{ArrowIcon}
</div>
</div>
)

View File

@@ -0,0 +1,176 @@
'use client'
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine, RiBrain2Line, RiCheckLine, RiQuestionLine } from '@remixicon/react'
import { SelfHostedPlan } from '../type'
import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../config'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
import { Asterisk, AwsMarketplace, Azure, Buildings, Diamond, GoogleCloud } from '../../base/icons/src/public/billing'
import type { PlanRange } from './select-plan-range'
import cn from '@/utils/classnames'
import { useAppContext } from '@/context/app-context'
type Props = {
plan: SelfHostedPlan
planRange: PlanRange
canPay: boolean
}
const KeyValue = ({ label, tooltip, textColor, tooltipIconColor }: { icon: ReactNode; label: string; tooltip?: string; textColor: string; tooltipIconColor: string }) => {
return (
<div className={cn('flex', textColor)}>
<div className='size-4 flex items-center justify-center'>
<RiCheckLine />
</div>
<div className={cn('ml-2 mr-0.5 system-sm-regular', textColor)}>{label}</div>
{tooltip && (
<Tooltip
asChild
popupContent={tooltip}
popupClassName='w-[200px]'
>
<div className='size-4 flex items-center justify-center'>
<RiQuestionLine className={cn(tooltipIconColor)} />
</div>
</Tooltip>
)}
</div>
)
}
const style = {
[SelfHostedPlan.community]: {
icon: <Asterisk className='text-text-primary size-7' />,
title: 'text-text-primary',
price: 'text-text-primary',
priceTip: 'text-text-tertiary',
description: 'text-util-colors-gray-gray-600',
bg: 'border-effects-highlight-lightmode-off bg-background-section-burn',
btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary',
values: 'text-text-secondary',
tooltipIconColor: 'text-text-tertiary',
},
[SelfHostedPlan.premium]: {
icon: <Diamond className='text-text-warning size-7' />,
title: 'text-text-primary',
price: 'text-text-primary',
priceTip: 'text-text-tertiary',
description: 'text-text-warning',
bg: 'border-effects-highlight bg-background-section-burn',
btnStyle: 'bg-third-party-aws hover:bg-third-party-aws-hover border border-components-button-primary-border text-text-primary-on-surface shadow-xs',
values: 'text-text-secondary',
tooltipIconColor: 'text-text-tertiary',
},
[SelfHostedPlan.enterprise]: {
icon: <Buildings className='text-text-primary-on-surface size-7' />,
title: 'text-text-primary-on-surface',
price: 'text-text-primary-on-surface',
priceTip: 'text-text-primary-on-surface',
description: 'text-text-primary-on-surface',
bg: 'border-effects-highlight bg-[#155AEF] text-text-primary-on-surface',
btnStyle: 'bg-white bg-opacity-96 hover:opacity-85 border-[0.5px] border-components-button-secondary-border text-[#155AEF] shadow-xs',
values: 'text-text-primary-on-surface',
tooltipIconColor: 'text-text-primary-on-surface',
},
}
const SelfHostedPlanItem: FC<Props> = ({
plan,
}) => {
const { t } = useTranslation()
const isFreePlan = plan === SelfHostedPlan.community
const isPremiumPlan = plan === SelfHostedPlan.premium
const i18nPrefix = `billing.plans.${plan}`
const isEnterprisePlan = plan === SelfHostedPlan.enterprise
const { isCurrentWorkspaceManager } = useAppContext()
const features = t(`${i18nPrefix}.features`, { returnObjects: true }) as string[]
const handleGetPayUrl = () => {
// Only workspace manager can buy plan
if (!isCurrentWorkspaceManager) {
Toast.notify({
type: 'error',
message: t('billing.buyPermissionDeniedTip'),
className: 'z-[1001]',
})
return
}
if (isFreePlan) {
window.location.href = getStartedWithCommunityUrl
return
}
if (isPremiumPlan) {
window.location.href = getWithPremiumUrl
return
}
if (isEnterprisePlan)
window.location.href = contactSalesUrl
}
return (
<div className={cn(`relative flex flex-col w-[374px] border-[0.5px] rounded-2xl
hover:shadow-lg hover:backdrop-blur-[5px] hover:border-effects-highlight overflow-hidden`, style[plan].bg)}>
<div>
<div className={cn(isEnterprisePlan ? 'bg-price-enterprise-background absolute left-0 top-0 right-0 bottom-0 z-1' : '')} >
</div>
{isEnterprisePlan && <div className='bg-[#09328c] opacity-15 mix-blend-plus-darker blur-[80px] size-[341px] rounded-full absolute -top-[104px] -left-[90px] z-15'></div>}
{isEnterprisePlan && <div className='bg-[#e2eafb] opacity-15 mix-blend-plus-darker blur-[80px] size-[341px] rounded-full absolute -right-[40px] -bottom-[72px] z-15'></div>}
</div>
<div className='relative w-full p-6 z-10 min-h-[559px]'>
<div className=' flex flex-col gap-y-1 min-h-[108px]'>
{style[plan].icon}
<div className='flex items-center'>
<div className={cn('leading-[125%] system-md-semibold uppercase', style[plan].title)}>{t(`${i18nPrefix}.name`)}</div>
</div>
<div className={cn(style[plan].description, 'system-sm-regular')}>{t(`${i18nPrefix}.description`)}</div>
</div>
<div className='my-3'>
<div className='flex items-end'>
<div className={cn('leading-[125%] text-[28px] font-bold shrink-0', style[plan].price)}>{t(`${i18nPrefix}.price`)}</div>
{!isFreePlan
&& <span className={cn('ml-2 py-1 leading-normal text-[14px] font-normal', style[plan].priceTip)}>
{t(`${i18nPrefix}.priceTip`)}
</span>}
</div>
</div>
<div
className={cn('flex py-3 px-5 rounded-full justify-center items-center h-[44px] system-md-semibold cursor-pointer',
style[plan].btnStyle)}
onClick={handleGetPayUrl}
>
{t(`${i18nPrefix}.btnText`)}
{isPremiumPlan
&& <>
<div className='pt-[6px] mx-1'>
<AwsMarketplace className='h-6' />
</div>
<RiArrowRightUpLine className='size-4' />
</>}
</div>
<div className={cn('mt-6 system-sm-semibold mb-2', style[plan].values)}>{t(`${i18nPrefix}.includesTitle`)}</div>
<div className='flex flex-col gap-y-3'>
{features.map(v =>
<KeyValue key={`${plan}-${v}`}
textColor={style[plan].values}
tooltipIconColor={style[plan].tooltipIconColor}
icon={<RiBrain2Line />}
label={v}
/>)}
</div>
{isPremiumPlan && <div className='mt-[68px]'>
<div className='flex items-center gap-x-1'>
<div className='size-8 flex items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default shadow-xs'>
<Azure />
</div>
<div className='size-8 flex items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default shadow-xs'>
<GoogleCloud />
</div>
</div>
<span className={cn('mt-2 system-xs-regular', style[plan].tooltipIconColor)}>{t('billing.plans.premium.comingSoon')}</span>
</div>}
</div>
</div>
)
}
export default React.memo(SelfHostedPlanItem)

View File

@@ -2,9 +2,7 @@ export enum Plan {
sandbox = 'sandbox',
professional = 'professional',
team = 'team',
enterprise = 'enterprise',
}
export enum Priority {
standard = 'standard',
priority = 'priority',
@@ -14,21 +12,42 @@ export type PlanInfo = {
level: number
price: number
modelProviders: string
teamWorkspace: number
teamMembers: number
buildApps: number
vectorSpace: number
documents: number
vectorSpace: string
documentsUploadQuota: number
documentsRequestQuota: number
documentProcessingPriority: Priority
logHistory: number
customTools: string | number
messageRequest: {
en: string | number
zh: string | number
}
messageRequest: number
annotatedResponse: number
}
export type UsagePlanInfo = Pick<PlanInfo, 'vectorSpace' | 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota'>
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<PlanInfo, 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota'> & { vectorSpace: number }
export enum DocumentProcessingPriority {
standard = 'standard',

View File

@@ -23,7 +23,7 @@ const AppsInfo: FC<Props> = ({
<UsageInfo
className={className}
Icon={ChatBot}
name={t('billing.plansCommon.buildApps')}
name={t('billing.usagePage.buildApps')}
usage={usage.buildApps}
total={total.buildApps}
/>

View File

@@ -23,8 +23,8 @@ const VectorSpaceInfo: FC<Props> = ({
<UsageInfo
className={className}
Icon={ArtificialBrain}
name={t('billing.plansCommon.vectorSpace')}
tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
name={t('billing.usagePage.vectorSpace')}
tooltip={t('billing.usagePage.vectorSpaceTooltip') as string}
usage={usage.vectorSpace}
total={total.vectorSpace}
unit='MB'