| | |
| | | 'use client' |
| | | import type { FC, ReactNode } from 'react' |
| | | import type { FC } from 'react' |
| | | import React from 'react' |
| | | import { useTranslation } from 'react-i18next' |
| | | import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine, RiTerminalBoxLine } from '@remixicon/react' |
| | | import type { BasicPlan } from '../type' |
| | | import { useContext } from 'use-context-selector' |
| | | import { Plan } from '../type' |
| | | import { ALL_PLANS, NUM_INFINITE } from '../config' |
| | | import { ALL_PLANS, NUM_INFINITE, contactSalesUrl, contractSales, unAvailable } 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: BasicPlan |
| | | plan: BasicPlan |
| | | currentPlan: Plan |
| | | plan: Plan |
| | | planRange: PlanRange |
| | | canPay: boolean |
| | | } |
| | | |
| | | const KeyValue = ({ icon, label, tooltip }: { icon: ReactNode; label: string; tooltip?: ReactNode }) => { |
| | | const KeyValue = ({ label, value, tooltip }: { label: string; value: string | number | JSX.Element; tooltip?: string }) => { |
| | | return ( |
| | | <div className='flex text-text-tertiary'> |
| | | <div className='flex size-4 items-center justify-center'> |
| | | {icon} |
| | | </div> |
| | | <div className='system-sm-regular ml-2 mr-0.5 text-text-primary'>{label}</div> |
| | | <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 |
| | | asChild |
| | | popupContent={tooltip} |
| | | popupClassName='w-[200px]' |
| | | > |
| | | <div className='flex size-4 items-center justify-center'> |
| | | <RiQuestionLine className='text-text-quaternary' /> |
| | | </div> |
| | | </Tooltip> |
| | | popupContent={ |
| | | <div className='w-[200px]'>{tooltip}</div> |
| | | } |
| | | /> |
| | | )} |
| | | </div> |
| | | <div className='mt-0.5 text-gray-900'>{value}</div> |
| | | </div> |
| | | ) |
| | | } |
| | | |
| | | const priceClassName = 'leading-[125%] text-[28px] font-bold text-text-primary' |
| | | const priceClassName = 'leading-[32px] text-[28px] font-bold text-gray-900' |
| | | const style = { |
| | | [Plan.sandbox]: { |
| | | icon: <ArCube1 className='size-7 text-text-primary' />, |
| | | 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', |
| | | bg: 'bg-[#F2F4F7]', |
| | | title: 'text-gray-900', |
| | | hoverAndActive: '', |
| | | }, |
| | | [Plan.professional]: { |
| | | icon: <Keyframe className='size-7 text-util-colors-blue-brand-blue-brand-600' />, |
| | | 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', |
| | | 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]', |
| | | }, |
| | | [Plan.team]: { |
| | | icon: <Group2 className='size-7 text-util-colors-indigo-indigo-600' />, |
| | | 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', |
| | | 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]', |
| | | }, |
| | | } |
| | | 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 |
| | | const isPlanDisabled = planInfo.level <= ALL_PLANS[currentPlan].level || (!canPay && plan !== Plan.enterprise) |
| | | 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.getStarted'), |
| | | [Plan.team]: t('billing.plansCommon.getStarted'), |
| | | [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] |
| | | })() |
| | | |
| | | 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 |
| | |
| | | if (isFreePlan) |
| | | return |
| | | |
| | | if (isEnterprisePlan) { |
| | | window.location.href = contactSalesUrl |
| | | return |
| | | } |
| | | // Only workspace manager can buy plan |
| | | if (!isCurrentWorkspaceManager) { |
| | | Toast.notify({ |
| | |
| | | } |
| | | } |
| | | return ( |
| | | <div className={cn('flex w-[373px] flex-col rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn p-6', |
| | | isMostPopularPlan ? 'border-effects-highlight shadow-lg backdrop-blur-[5px]' : 'hover:border-effects-highlight hover:shadow-lg hover:backdrop-blur-[5px]', |
| | | )}> |
| | | <div className='flex flex-col gap-y-1'> |
| | | {style[plan].icon} |
| | | <div className='flex items-center'> |
| | | <div className='grow text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div> |
| | | {isMostPopularPlan && <div className='ml-1 flex shrink-0 items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'> |
| | | <div className='pl-0.5'> |
| | | <SparklesSoft className='size-3' /> |
| | | </div> |
| | | <span className='system-2xs-semibold-uppercase bg-price-premium-text-background bg-clip-text px-0.5 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'> |
| | | <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> |
| | | |
| | | {/* Price */} |
| | | {isFreePlan && ( |
| | | <div className={priceClassName}>{t('billing.plansCommon.free')}</div> |
| | | )} |
| | | {!isFreePlan && ( |
| | | <div className='flex items-end'> |
| | | {isEnterprisePlan && ( |
| | | <div className={priceClassName}>{t('billing.plansCommon.contactSales')}</div> |
| | | )} |
| | | {!isFreePlan && !isEnterprisePlan && ( |
| | | <div className='flex items-end h-9'> |
| | | <div className={priceClassName}>${isYear ? planInfo.price * 10 : planInfo.price}</div> |
| | | <div className='ml-1 flex flex-col'> |
| | | {isYear && <div className='text-[14px] font-normal italic leading-[14px] text-text-warning'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>} |
| | | <div className='text-[14px] font-normal leading-normal text-text-tertiary'> |
| | | {t('billing.plansCommon.priceTip')} |
| | | {t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</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> |
| | | </div> |
| | | )} |
| | | </div> |
| | | |
| | | <div |
| | | className={cn('flex h-[42px] items-center justify-center rounded-full px-5 py-3', |
| | | style[plan].btnStyle, |
| | | isPlanDisabled && style[plan].btnDisabledStyle, |
| | | isPlanDisabled ? 'cursor-not-allowed' : 'cursor-pointer')} |
| | | 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='mt-6 flex flex-col gap-y-3'> |
| | | |
| | | <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> |
| | | <KeyValue |
| | | icon={<RiChatAiLine />} |
| | | label={isFreePlan |
| | | ? t('billing.plansCommon.messageRequest.title', { count: planInfo.messageRequest }) |
| | | : t('billing.plansCommon.messageRequest.titlePerMonth', { count: planInfo.messageRequest })} |
| | | label={t('billing.plansCommon.messageRequest.title')} |
| | | value={messagesRequest} |
| | | tooltip={t('billing.plansCommon.messageRequest.tooltip') as string} |
| | | /> |
| | | <KeyValue |
| | | icon={<RiBrain2Line />} |
| | | label={t('billing.plansCommon.modelProviders')} |
| | | value={planInfo.modelProviders} |
| | | /> |
| | | <KeyValue |
| | | icon={<RiFolder6Line />} |
| | | label={t('billing.plansCommon.teamWorkspace', { count: planInfo.teamWorkspace })} |
| | | label={t('billing.plansCommon.teamMembers')} |
| | | value={planInfo.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.teamMembers} |
| | | /> |
| | | <KeyValue |
| | | icon={<RiGroupLine />} |
| | | label={t('billing.plansCommon.teamMember', { count: planInfo.teamMembers })} |
| | | label={t('billing.plansCommon.buildApps')} |
| | | value={planInfo.buildApps === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.buildApps} |
| | | /> |
| | | <KeyValue |
| | | 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} |
| | | 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} |
| | | /> |
| | | <KeyValue |
| | | icon={<RiHardDrive3Line />} |
| | | label={t('billing.plansCommon.vectorSpace', { size: planInfo.vectorSpace })} |
| | | tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string} |
| | | 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} |
| | | /> |
| | | |
| | | <KeyValue |
| | | icon={<RiSeoLine />} |
| | | label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })} |
| | | tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')} |
| | | /> |
| | | <KeyValue |
| | | icon={<RiTerminalBoxLine />} |
| | | label={ |
| | | planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}` |
| | | : `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}` |
| | | } |
| | | tooltip={planInfo.apiRateLimit === NUM_INFINITE ? null : t('billing.plansCommon.apiRateLimitTooltip') as string} |
| | | /> |
| | | <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 })} |
| | | label={t('billing.plansCommon.annotatedResponse.title')} |
| | | value={planInfo.annotatedResponse === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.annotatedResponse}`} |
| | | tooltip={t('billing.plansCommon.annotatedResponse.tooltip') as string} |
| | | /> |
| | | <KeyValue |
| | | icon={<RiHistoryLine />} |
| | | label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })} |
| | | 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} |
| | | /> |
| | | </div> |
| | | </div> |