Initial commit

This commit is contained in:
2025-10-14 14:17:21 +08:00
commit ac715a8b88
35011 changed files with 3834178 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '../upgrade-btn'
import Usage from './usage'
import s from './style.module.css'
import cn from '@/utils/classnames'
import GridMask from '@/app/components/base/grid-mask'
const AnnotationFull: FC = () => {
const { t } = useTranslation()
return (
<GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
<div className='mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
<div className='flex justify-between items-center'>
<div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
<div>{t('billing.annotatedResponse.fullTipLine1')}</div>
<div>{t('billing.annotatedResponse.fullTipLine2')}</div>
</div>
<div className='flex'>
<UpgradeBtn loc={'annotation-create'} />
</div>
</div>
<Usage className='mt-4' />
</div>
</GridMask>
)
}
export default React.memo(AnnotationFull)

View File

@@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '../upgrade-btn'
import Modal from '../../base/modal'
import Usage from './usage'
import s from './style.module.css'
import cn from '@/utils/classnames'
import GridMask from '@/app/components/base/grid-mask'
type Props = {
show: boolean
onHide: () => void
}
const AnnotationFullModal: FC<Props> = ({
show,
onHide,
}) => {
const { t } = useTranslation()
return (
<Modal
isShow={show}
onClose={onHide}
closable
className='!p-0'
>
<GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
<div className='mt-6 px-7 py-6 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
<div className='flex justify-between items-center'>
<div className={cn(s.textGradient, 'leading-[27px] text-[18px] font-semibold')}>
<div>{t('billing.annotatedResponse.fullTipLine1')}</div>
<div>{t('billing.annotatedResponse.fullTipLine2')}</div>
</div>
</div>
<Usage className='mt-4' />
<div className='mt-7 flex justify-end'>
<UpgradeBtn loc={'annotation-create'} />
</div>
</div>
</GridMask>
</Modal>
)
}
export default React.memo(AnnotationFullModal)

View File

@@ -0,0 +1,7 @@
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}

View File

@@ -0,0 +1,32 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { MessageFastPlus } from '../../base/icons/src/vender/line/communication'
import UsageInfo from '../usage-info'
import { useProviderContext } from '@/context/provider-context'
type Props = {
className?: string
}
const Usage: FC<Props> = ({
className,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const {
usage,
total,
} = plan
return (
<UsageInfo
className={className}
Icon={MessageFastPlus}
name={t('billing.annotatedResponse.quotaTitle')}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
/>
)
}
export default React.memo(Usage)

View File

@@ -0,0 +1,37 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '../upgrade-btn'
import AppsInfo from '../usage-info/apps-info'
import s from './style.module.css'
import cn from '@/utils/classnames'
import GridMask from '@/app/components/base/grid-mask'
const AppsFull: FC<{ loc: string; className?: string }> = ({
loc,
className,
}) => {
const { t } = useTranslation()
return (
<GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
<div className={cn(
'mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer',
className,
)}>
<div className='flex justify-between items-center'>
<div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
<div>{t('billing.apps.fullTipLine1')}</div>
<div>{t('billing.apps.fullTipLine2')}</div>
</div>
<div className='flex'>
<UpgradeBtn loc={loc} />
</div>
</div>
<AppsInfo className='mt-4' />
</div>
</GridMask>
)
}
export default React.memo(AppsFull)

View File

@@ -0,0 +1,7 @@
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}

View File

@@ -0,0 +1,27 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '../upgrade-btn'
import s from './style.module.css'
import cn from '@/utils/classnames'
import GridMask from '@/app/components/base/grid-mask'
const AppsFull: FC = () => {
const { t } = useTranslation()
return (
<GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
<div className='col-span-1 px-3.5 pt-3.5 border-2 border-solid border-transparent rounded-lg shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'>
<div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
<div>{t('billing.apps.fullTipLine1')}</div>
<div>{t('billing.apps.fullTipLine2')}</div>
</div>
<div className='flex mt-8'>
<UpgradeBtn loc='app-create' />
</div>
</div>
</GridMask>
)
}
export default React.memo(AppsFull)

View File

@@ -0,0 +1,7 @@
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}

View File

@@ -0,0 +1,40 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import {
RiExternalLinkLine,
} from '@remixicon/react'
import PlanComp from '../plan'
import { ReceiptList } from '../../base/icons/src/vender/line/financeAndECommerce'
import { fetchBillingUrl } from '@/service/billing'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
const Billing: FC = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const { enableBilling } = useProviderContext()
const { data: billingUrl } = useSWR(
(!enableBilling || !isCurrentWorkspaceManager) ? null : ['/billing/invoices'],
() => fetchBillingUrl().then(data => data.url),
)
return (
<div>
<PlanComp loc={'billing-page'} />
{enableBilling && isCurrentWorkspaceManager && billingUrl && (
<a className='mt-5 flex px-6 justify-between h-12 items-center bg-gray-50 rounded-xl cursor-pointer' href={billingUrl} target='_blank' rel='noopener noreferrer'>
<div className='flex items-center'>
<ReceiptList className='w-4 h-4 text-gray-700' />
<div className='ml-2 text-sm font-normal text-gray-700'>{t('billing.viewBilling')}</div>
</div>
<RiExternalLinkLine className='w-3 h-3' />
</a>
)}
</div>
)
}
export default React.memo(Billing)

View File

@@ -0,0 +1,98 @@
import { Plan, type PlanInfo, Priority } from '@/app/components/billing/type'
const supportModelProviders = 'OpenAI/Anthropic/Azure OpenAI/ Llama2/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 ALL_PLANS: Record<Plan, PlanInfo> = {
sandbox: {
level: 1,
price: 0,
modelProviders: supportModelProviders,
teamMembers: 1,
buildApps: 10,
vectorSpace: 5,
documentsUploadQuota: 50,
documentProcessingPriority: Priority.standard,
logHistory: 30,
customTools: unAvailable,
messageRequest: {
en: '200 messages',
zh: '200 条信息',
},
annotatedResponse: 10,
},
professional: {
level: 2,
price: 59,
modelProviders: supportModelProviders,
teamMembers: 3,
buildApps: 50,
vectorSpace: 200,
documentsUploadQuota: 500,
documentProcessingPriority: Priority.priority,
logHistory: NUM_INFINITE,
customTools: 10,
messageRequest: {
en: '5,000 messages/month',
zh: '5,000 条信息/月',
},
annotatedResponse: 2000,
},
team: {
level: 3,
price: 159,
modelProviders: supportModelProviders,
teamMembers: NUM_INFINITE,
buildApps: NUM_INFINITE,
vectorSpace: 1000,
documentsUploadQuota: 1000,
documentProcessingPriority: Priority.topPriority,
logHistory: NUM_INFINITE,
customTools: NUM_INFINITE,
messageRequest: {
en: '10,000 messages/month',
zh: '10,000 条信息/月',
},
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: {
vectorSpace: 1,
buildApps: 1,
teamMembers: 1,
annotatedResponse: 1,
documentsUploadQuota: 1,
},
total: {
vectorSpace: 10,
buildApps: 10,
teamMembers: 1,
annotatedResponse: 10,
documentsUploadQuota: 50,
},
}

View File

@@ -0,0 +1,60 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import UpgradeBtn from '../upgrade-btn'
import { Plan } from '../type'
import cn from '@/utils/classnames'
import { useProviderContext } from '@/context/provider-context'
type Props = {
onClick?: () => void
isDisplayOnly?: boolean
}
const HeaderBillingBtn: FC<Props> = ({
onClick,
isDisplayOnly = false,
}) => {
const { plan, enableBilling, isFetchedPlan } = useProviderContext()
const {
type,
} = plan
const name = (() => {
if (type === Plan.professional)
return 'pro'
return type
})()
const classNames = (() => {
if (type === Plan.professional)
return `border-[#E0F2FE] ${!isDisplayOnly ? 'hover:border-[#B9E6FE]' : ''} bg-[#E0F2FE] text-[#026AA2]`
if (type === Plan.team)
return `border-[#E0EAFF] ${!isDisplayOnly ? 'hover:border-[#C7D7FE]' : ''} bg-[#E0EAFF] text-[#3538CD]`
return ''
})()
if (!enableBilling || !isFetchedPlan)
return null
if (type === Plan.sandbox)
return <UpgradeBtn onClick={isDisplayOnly ? undefined : onClick} isShort />
const handleClick = () => {
if (!isDisplayOnly && onClick)
onClick()
}
return (
<div
onClick={handleClick}
className={cn(
classNames,
'flex items-center h-[22px] px-2 rounded-md border text-xs font-semibold uppercase',
isDisplayOnly ? 'cursor-default' : 'cursor-pointer',
)}
>
{name}
</div>
)
}
export default React.memo(HeaderBillingBtn)

View File

@@ -0,0 +1,125 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Plan } from '../type'
import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn'
import { User01 } from '../../base/icons/src/vender/line/users'
import { MessageFastPlus } from '../../base/icons/src/vender/line/communication'
import { FileUpload } from '../../base/icons/src/vender/line/files'
import cn from '@/utils/classnames'
import { useProviderContext } from '@/context/provider-context'
import UsageInfo from '@/app/components/billing/usage-info'
const typeStyle = {
[Plan.sandbox]: {
textClassNames: 'text-gray-900',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #EAECF0',
},
[Plan.professional]: {
textClassNames: 'text-[#026AA2]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #E0F2FE',
},
[Plan.team]: {
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]: {
textClassNames: 'text-[#DC6803]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #FFEED3',
},
}
type Props = {
loc: string
}
const PlanComp: FC<Props> = ({
loc,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const {
type,
} = plan
const {
usage,
total,
} = plan
const isInHeader = loc === 'header'
return (
<div
className='rounded-xl border border-white select-none'
style={{
background: typeStyle[type].bg,
boxShadow: '5px 7px 12px 0px rgba(0, 0, 0, 0.06)',
}}
>
<div className='flex justify-between px-6 py-5 items-center'>
<div>
<div
className='leading-[18px] text-xs font-normal opacity-70'
style={{
color: 'rgba(0, 0, 0, 0.64)',
}}
>
{t('billing.currentPlan')}
</div>
<div className={cn(typeStyle[type].textClassNames, 'leading-[125%] text-lg font-semibold uppercase')}>
{t(`billing.plans.${type}.name`)}
</div>
</div>
{(!isInHeader || (isInHeader && type !== Plan.sandbox)) && (
<UpgradeBtn
className='flex-shrink-0'
isPlain={type !== Plan.sandbox}
loc={loc}
/>
)}
</div>
{/* Plan detail */}
<div className='rounded-xl bg-white px-6 py-3'>
<UsageInfo
className='py-3'
Icon={User01}
name={t('billing.plansCommon.teamMembers')}
usage={usage.teamMembers}
total={total.teamMembers}
/>
<AppsInfo className='py-3' />
<VectorSpaceInfo className='py-3' />
<UsageInfo
className='py-3'
Icon={MessageFastPlus}
name={t('billing.plansCommon.annotationQuota')}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
/>
<UsageInfo
className='py-3'
Icon={FileUpload}
name={t('billing.plansCommon.documentsUploadQuota')}
usage={usage.documentsUploadQuota}
total={total.documentsUploadQuota}
/>
{isInHeader && type === Plan.sandbox && (
<UpgradeBtn
className='flex-shrink-0 my-3'
isFull
size='lg'
isPlain={type !== Plan.sandbox}
loc={loc}
/>
)}
</div>
</div>
)
}
export default React.memo(PlanComp)

View File

@@ -0,0 +1,80 @@
'use client'
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 SelectPlanRange, { PlanRange } from './select-plan-range'
import PlanItem from './plan-item'
import { useProviderContext } from '@/context/provider-context'
import GridMask from '@/app/components/base/grid-mask'
import { useAppContext } from '@/context/app-context'
type Props = {
onCancel: () => void
}
const Pricing: FC<Props> = ({
onCancel,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { isCurrentWorkspaceManager } = useAppContext()
const canPay = isCurrentWorkspaceManager
const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
return createPortal(
<div
className='fixed inset-0 flex bg-white z-[1000] overflow-auto'
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>
</GridMask>
<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>,
document.body,
)
}
export default React.memo(Pricing)

View File

@@ -0,0 +1,303 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { Plan } from '../type'
import { ALL_PLANS, NUM_INFINITE, contactSalesUrl, contractSales, unAvailable } from '../config'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
import { PlanRange } from './select-plan-range'
import cn from '@/utils/classnames'
import { useAppContext } from '@/context/app-context'
import { fetchSubscriptionUrls } from '@/service/billing'
import { LanguagesSupported } from '@/i18n/language'
import I18n from '@/context/i18n'
type Props = {
currentPlan: Plan
plan: Plan
planRange: PlanRange
canPay: boolean
}
const KeyValue = ({ label, value, tooltip }: { label: string; value: string | number | JSX.Element; tooltip?: string }) => {
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>
<div className='mt-0.5 text-gray-900'>{value}</div>
</div>
)
}
const priceClassName = 'leading-[32px] text-[28px] font-bold text-gray-900'
const style = {
[Plan.sandbox]: {
bg: 'bg-[#F2F4F7]',
title: 'text-gray-900',
hoverAndActive: '',
},
[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]',
},
[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]',
},
}
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 { 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]
})()
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
if (isPlanDisabled)
return
if (isFreePlan)
return
if (isEnterprisePlan) {
window.location.href = contactSalesUrl
return
}
// Only workspace manager can buy plan
if (!isCurrentWorkspaceManager) {
Toast.notify({
type: 'error',
message: t('billing.buyPermissionDeniedTip'),
className: 'z-[1001]',
})
return
}
setLoading(true)
try {
const res = await fetchSubscriptionUrls(plan, isYear ? 'year' : 'month')
// Adb Block additional tracking block the gtag, so we need to redirect directly
window.location.href = res.url
}
finally {
setLoading(false)
}
}
return (
<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>
)}
{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'>
{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
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>
<KeyValue
label={t('billing.plansCommon.messageRequest.title')}
value={messagesRequest}
tooltip={t('billing.plansCommon.messageRequest.tooltip') as string}
/>
<KeyValue
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}
/>
<KeyValue
label={t('billing.plansCommon.buildApps')}
value={planInfo.buildApps === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.buildApps}
/>
<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}
/>
<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}
/>
<KeyValue
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
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>
)
}
export default React.memo(PlanItem)

View File

@@ -0,0 +1,55 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
export enum PlanRange {
monthly = 'monthly',
yearly = 'yearly',
}
type Props = {
value: PlanRange
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>
)
const SelectPlanRange: FC<Props> = ({
value,
onChange,
}) => {
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>
</div>
)
}
export default React.memo(SelectPlanRange)

View File

@@ -0,0 +1,65 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
DocumentProcessingPriority,
Plan,
} from '../type'
import cn from '@/utils/classnames'
import { useProviderContext } from '@/context/provider-context'
import {
ZapFast,
ZapNarrow,
} from '@/app/components/base/icons/src/vender/solid/general'
import Tooltip from '@/app/components/base/tooltip'
type PriorityLabelProps = {
className?: string
}
const PriorityLabel = ({ className }: PriorityLabelProps) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const priority = useMemo(() => {
if (plan.type === Plan.sandbox)
return DocumentProcessingPriority.standard
if (plan.type === Plan.professional)
return DocumentProcessingPriority.priority
if (plan.type === Plan.team || plan.type === Plan.enterprise)
return DocumentProcessingPriority.topPriority
}, [plan])
return (
<Tooltip popupContent={
<div>
<div className='mb-1 text-xs font-semibold text-gray-700'>{`${t('billing.plansCommon.documentProcessingPriority')}: ${t(`billing.plansCommon.priority.${priority}`)}`}</div>
{
priority !== DocumentProcessingPriority.topPriority && (
<div className='text-xs text-gray-500'>{t('billing.plansCommon.documentProcessingPriorityTip')}</div>
)
}
</div>
}>
<span className={cn(`
shrink-0 flex items-center ml-1 px-1 h-[18px] rounded-[5px] border border-text-accent-secondary
text-2xs font-medium text-text-accent-secondary
`, className)}>
{
plan.type === Plan.professional && (
<ZapNarrow className='mr-0.5 size-3' />
)
}
{
(plan.type === Plan.team || plan.type === Plan.enterprise) && (
<ZapFast className='mr-0.5 size-3' />
)
}
{t(`billing.plansCommon.priority.${priority}`)}
</span>
</Tooltip>
)
}
export default PriorityLabel

View File

@@ -0,0 +1,22 @@
type ProgressBarProps = {
percent: number
color: string
}
const ProgressBar = ({
percent = 0,
color = '#2970FF',
}: ProgressBarProps) => {
return (
<div className='bg-[#F2F4F7] rounded-[4px] overflow-hidden'>
<div
className='h-2 rounded-[4px]'
style={{
width: `${Math.min(percent, 100)}%`,
backgroundColor: color,
}}
/>
</div>
)
}
export default ProgressBar

View File

@@ -0,0 +1,79 @@
export enum Plan {
sandbox = 'sandbox',
professional = 'professional',
team = 'team',
enterprise = 'enterprise',
}
export enum Priority {
standard = 'standard',
priority = 'priority',
topPriority = 'top-priority',
}
export type PlanInfo = {
level: number
price: number
modelProviders: string
teamMembers: number
buildApps: number
vectorSpace: number
documentsUploadQuota: number
documentProcessingPriority: Priority
logHistory: number
customTools: string | number
messageRequest: {
en: string | number
zh: string | number
}
annotatedResponse: number
}
export type UsagePlanInfo = Pick<PlanInfo, 'vectorSpace' | 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota'>
export enum DocumentProcessingPriority {
standard = 'standard',
priority = 'priority',
topPriority = 'top-priority',
}
export type CurrentPlanInfoBackend = {
billing: {
enabled: boolean
subscription: {
plan: Plan
}
}
members: {
size: number
limit: number // total. 0 means unlimited
}
apps: {
size: number
limit: number // total. 0 means unlimited
}
vector_space: {
size: number
limit: number // total. 0 means unlimited
}
annotation_quota_limit: {
size: number
limit: number // total. 0 means unlimited
}
documents_upload_quota: {
size: number
limit: number // total. 0 means unlimited
}
docs_processing: DocumentProcessingPriority
can_replace_logo: boolean
model_load_balancing_enabled: boolean
dataset_operator_enabled: boolean
}
export type SubscriptionItem = {
plan: Plan
url: string
}
export type SubscriptionUrlsBackend = {
url: string
}

View File

@@ -0,0 +1,78 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import PremiumBadge from '../../base/premium-badge'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import cn from '@/utils/classnames'
import { useModalContext } from '@/context/modal-context'
type Props = {
className?: string
isFull?: boolean
size?: 'md' | 'lg'
isPlain?: boolean
isShort?: boolean
onClick?: () => void
loc?: string
}
const PlainBtn = ({ className, onClick }: { className?: string; onClick: () => void }) => {
const { t } = useTranslation()
return (
<div
className={cn(className, 'flex items-center h-8 px-3 rounded-lg border border-gray-200 bg-white shadow-sm cursor-pointer')}
onClick={onClick}
>
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>
{t('billing.upgradeBtn.plain')}
</div>
</div>
)
}
const UpgradeBtn: FC<Props> = ({
className,
isPlain = false,
isShort = false,
onClick: _onClick,
loc,
}) => {
const { t } = useTranslation()
const { setShowPricingModal } = useModalContext()
const handleClick = () => {
if (_onClick)
_onClick()
else
(setShowPricingModal as any)()
}
const onClick = () => {
handleClick()
if (loc && (window as any).gtag) {
(window as any).gtag('event', 'click_upgrade_btn', {
loc,
})
}
}
if (isPlain)
return <PlainBtn onClick={onClick} className={className} />
return (
<PremiumBadge
size="m"
color="blue"
allowHover={true}
onClick={onClick}
>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
</span>
</div>
</PremiumBadge>
)
}
export default React.memo(UpgradeBtn)

View File

@@ -0,0 +1,9 @@
.upgradeBtn {
background: linear-gradient(99deg, rgba(255, 255, 255, 0.12) 7.16%, rgba(255, 255, 255, 0.00) 85.47%), linear-gradient(280deg, #00B2FF 12.96%, #132BFF 90.95%);
box-shadow: 0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(0, 162, 253, 0.12);
}
.upgradeBtn:hover {
background: linear-gradient(99deg, rgba(255, 255, 255, 0.12) 7.16%, rgba(255, 255, 255, 0.00) 85.47%), linear-gradient(280deg, #02C2FF 12.96%, #001AFF 90.95%);
box-shadow: 0px 4px 6px -2px rgba(16, 18, 40, 0.08), 0px 12px 16px -4px rgba(0, 209, 255, 0.08);
}

View File

@@ -0,0 +1,32 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ChatBot } from '../../base/icons/src/vender/line/communication'
import UsageInfo from '../usage-info'
import { useProviderContext } from '@/context/provider-context'
type Props = {
className?: string
}
const AppsInfo: FC<Props> = ({
className,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const {
usage,
total,
} = plan
return (
<UsageInfo
className={className}
Icon={ChatBot}
name={t('billing.plansCommon.buildApps')}
usage={usage.buildApps}
total={total.buildApps}
/>
)
}
export default React.memo(AppsInfo)

View File

@@ -0,0 +1,76 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import ProgressBar from '../progress-bar'
import { NUM_INFINITE } from '../config'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
Icon: any
name: string
tooltip?: string
usage: number
total: number
unit?: string
}
const LOW = 50
const MIDDLE = 80
const UsageInfo: FC<Props> = ({
className,
Icon,
name,
tooltip,
usage,
total,
unit = '',
}) => {
const { t } = useTranslation()
const percent = usage / total * 100
const color = (() => {
if (percent < LOW)
return '#155EEF'
if (percent < MIDDLE)
return '#F79009'
return '#F04438'
})()
return (
<div className={className}>
<div className='flex justify-between h-5 items-center'>
<div className='flex items-center'>
<Icon className='w-4 h-4 text-gray-700' />
<div className='mx-1 leading-5 text-sm font-medium text-gray-700'>{name}</div>
{tooltip && (
<Tooltip
popupContent={
<div className='w-[180px]'>
{tooltip}
</div>
}
/>
)}
</div>
<div className='flex items-center leading-[18px] text-[13px] font-normal'>
<div style={{
color: percent < LOW ? '#344054' : color,
}}>{usage}{unit}</div>
<div className='mx-1 text-gray-300'>/</div>
<div className='text-gray-500'>{total === NUM_INFINITE ? t('billing.plansCommon.unlimited') : `${total}${unit}`}</div>
</div>
</div>
<div className='mt-2'>
<ProgressBar
percent={percent}
color={color}
/>
</div>
</div>
)
}
export default React.memo(UsageInfo)

View File

@@ -0,0 +1,34 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ArtificialBrain } from '../../base/icons/src/vender/line/development'
import UsageInfo from '../usage-info'
import { useProviderContext } from '@/context/provider-context'
type Props = {
className?: string
}
const VectorSpaceInfo: FC<Props> = ({
className,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const {
usage,
total,
} = plan
return (
<UsageInfo
className={className}
Icon={ArtificialBrain}
name={t('billing.plansCommon.vectorSpace')}
tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
usage={usage.vectorSpace}
total={total.vectorSpace}
unit='MB'
/>
)
}
export default React.memo(VectorSpaceInfo)

View File

@@ -0,0 +1,29 @@
import type { CurrentPlanInfoBackend } from '../type'
import { NUM_INFINITE } from '@/app/components/billing/config'
const parseLimit = (limit: number) => {
if (limit === 0)
return NUM_INFINITE
return limit
}
export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
return {
type: data.billing.subscription.plan,
usage: {
vectorSpace: data.vector_space.size,
buildApps: data.apps?.size || 0,
teamMembers: data.members.size,
annotatedResponse: data.annotation_quota_limit.size,
documentsUploadQuota: data.documents_upload_quota.size,
},
total: {
vectorSpace: parseLimit(data.vector_space.limit),
buildApps: parseLimit(data.apps?.limit) || 0,
teamMembers: parseLimit(data.members.limit),
annotatedResponse: parseLimit(data.annotation_quota_limit.limit),
documentsUploadQuota: parseLimit(data.documents_upload_quota.limit),
},
}
}

View File

@@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '../upgrade-btn'
import VectorSpaceInfo from '../usage-info/vector-space-info'
import s from './style.module.css'
import cn from '@/utils/classnames'
import GridMask from '@/app/components/base/grid-mask'
const VectorSpaceFull: FC = () => {
const { t } = useTranslation()
return (
<GridMask wrapperClassName='border border-gray-200 rounded-xl' canvasClassName='rounded-xl' gradientClassName='rounded-xl'>
<div className='py-5 px-6'>
<div className='flex justify-between items-center'>
<div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
<div>{t('billing.vectorSpace.fullTip')}</div>
<div>{t('billing.vectorSpace.fullSolution')}</div>
</div>
<UpgradeBtn loc='knowledge-add-file' />
</div>
<VectorSpaceInfo className='pt-4' />
</div>
</GridMask>
)
}
export default React.memo(VectorSpaceFull)

View File

@@ -0,0 +1,7 @@
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}