Fix: new upgrade page (#12417)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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,
|
||||
)
|
||||
}
|
||||
|
@@ -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'> {plan}</span></>,
|
||||
[Plan.team]: <>{t('billing.plansCommon.getStartedWith')}<span className='capitalize'> {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'> {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'> {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>
|
||||
|
@@ -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>
|
||||
)
|
||||
|
176
web/app/components/billing/pricing/self-hosted-plan-item.tsx
Normal file
176
web/app/components/billing/pricing/self-hosted-plan-item.tsx
Normal 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)
|
@@ -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',
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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'
|
||||
|
Reference in New Issue
Block a user