feat: SaaS price plan frontend (#1683)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
31
web/app/components/billing/apps-full-in-dialog/index.tsx
Normal file
31
web/app/components/billing/apps-full-in-dialog/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import AppsInfo from '../usage-info/apps-info'
|
||||
import s from './style.module.css'
|
||||
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='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.apps.fullTipLine1')}</div>
|
||||
<div>{t('billing.apps.fullTipLine2')}</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<UpgradeBtn />
|
||||
</div>
|
||||
</div>
|
||||
<AppsInfo className='mt-4' />
|
||||
</div>
|
||||
</GridMask>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppsFull)
|
@@ -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;
|
||||
}
|
27
web/app/components/billing/apps-full/index.tsx
Normal file
27
web/app/components/billing/apps-full/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import s from './style.module.css'
|
||||
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 />
|
||||
</div>
|
||||
</div>
|
||||
</GridMask>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppsFull)
|
7
web/app/components/billing/apps-full/style.module.css
Normal file
7
web/app/components/billing/apps-full/style.module.css
Normal 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;
|
||||
}
|
42
web/app/components/billing/billing-page/index.tsx
Normal file
42
web/app/components/billing/billing-page/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PlanComp from '../plan'
|
||||
import { ReceiptList } from '../../base/icons/src/vender/line/financeAndECommerce'
|
||||
import { LinkExternal01 } from '../../base/icons/src/vender/line/general'
|
||||
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 [billingUrl, setBillingUrl] = React.useState('')
|
||||
const { enableBilling } = useProviderContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableBilling && !isCurrentWorkspaceManager)
|
||||
return
|
||||
(async () => {
|
||||
const { url } = await fetchBillingUrl()
|
||||
setBillingUrl(url)
|
||||
})()
|
||||
}, [isCurrentWorkspaceManager])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PlanComp />
|
||||
{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'>
|
||||
<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>
|
||||
<LinkExternal01 className='w-3 h-3' />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Billing)
|
64
web/app/components/billing/config.ts
Normal file
64
web/app/components/billing/config.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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 contactSalesUrl = 'mailto:business@dify.ai'
|
||||
|
||||
export const ALL_PLANS: Record<Plan, PlanInfo> = {
|
||||
sandbox: {
|
||||
level: 1,
|
||||
price: 0,
|
||||
modelProviders: supportModelProviders,
|
||||
teamMembers: 1,
|
||||
buildApps: 10,
|
||||
vectorSpace: 10,
|
||||
documentProcessingPriority: Priority.standard,
|
||||
logHistory: 30,
|
||||
},
|
||||
professional: {
|
||||
level: 2,
|
||||
price: 59,
|
||||
modelProviders: supportModelProviders,
|
||||
teamMembers: 3,
|
||||
buildApps: 50,
|
||||
vectorSpace: 200,
|
||||
documentProcessingPriority: Priority.priority,
|
||||
logHistory: NUM_INFINITE,
|
||||
},
|
||||
team: {
|
||||
level: 3,
|
||||
price: 159,
|
||||
modelProviders: supportModelProviders,
|
||||
teamMembers: NUM_INFINITE,
|
||||
buildApps: NUM_INFINITE,
|
||||
vectorSpace: 1000,
|
||||
documentProcessingPriority: Priority.topPriority,
|
||||
logHistory: NUM_INFINITE,
|
||||
},
|
||||
enterprise: {
|
||||
level: 4,
|
||||
price: 0,
|
||||
modelProviders: supportModelProviders,
|
||||
teamMembers: NUM_INFINITE,
|
||||
buildApps: NUM_INFINITE,
|
||||
vectorSpace: NUM_INFINITE,
|
||||
documentProcessingPriority: Priority.topPriority,
|
||||
logHistory: NUM_INFINITE,
|
||||
},
|
||||
}
|
||||
|
||||
export const defaultPlan = {
|
||||
type: Plan.sandbox,
|
||||
usage: {
|
||||
vectorSpace: 1,
|
||||
buildApps: 1,
|
||||
teamMembers: 1,
|
||||
},
|
||||
total: {
|
||||
vectorSpace: 10,
|
||||
buildApps: 10,
|
||||
teamMembers: 1,
|
||||
},
|
||||
}
|
46
web/app/components/billing/header-billing-btn/index.tsx
Normal file
46
web/app/components/billing/header-billing-btn/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import { Plan } from '../type'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type Props = {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const HeaderBillingBtn: FC<Props> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
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] hover:border-[#B9E6FE] bg-[#E0F2FE] text-[#026AA2]'
|
||||
if (type === Plan.team)
|
||||
return 'border-[#E0EAFF] hover:border-[#C7D7FE] bg-[#E0EAFF] text-[#3538CD]'
|
||||
return ''
|
||||
})()
|
||||
|
||||
if (!enableBilling || !isFetchedPlan)
|
||||
return null
|
||||
|
||||
if (type === Plan.sandbox)
|
||||
return <UpgradeBtn onClick={onClick} isShort />
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className={cn(classNames, 'flex items-center h-[22px] px-2 rounded-md border text-xs font-semibold uppercase cursor-pointer')}>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(HeaderBillingBtn)
|
92
web/app/components/billing/plan/index.tsx
Normal file
92
web/app/components/billing/plan/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
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 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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Plan detail */}
|
||||
<div className='rounded-xl bg-white px-6 py-3'>
|
||||
<VectorSpaceInfo className='py-3' />
|
||||
<AppsInfo className='py-3' />
|
||||
{isInHeader && type === Plan.sandbox && (
|
||||
<UpgradeBtn
|
||||
className='flex-shrink-0 my-3'
|
||||
isFull
|
||||
size='lg'
|
||||
isPlain={type !== Plan.sandbox}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PlanComp)
|
74
web/app/components/billing/pricing/index.tsx
Normal file
74
web/app/components/billing/pricing/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plan } from '../type'
|
||||
import SelectPlanRange, { PlanRange } from './select-plan-range'
|
||||
import PlanItem from './plan-item'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import GridMask from '@/app/components/base/grid-mask'
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const Pricing: FC<Props> = ({
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
|
||||
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}
|
||||
/>
|
||||
<PlanItem
|
||||
currentPlan={plan.type}
|
||||
plan={Plan.professional}
|
||||
planRange={planRange}
|
||||
/>
|
||||
<PlanItem
|
||||
currentPlan={plan.type}
|
||||
plan={Plan.team}
|
||||
planRange={planRange}
|
||||
/>
|
||||
<PlanItem
|
||||
currentPlan={plan.type}
|
||||
plan={Plan.enterprise}
|
||||
planRange={planRange}
|
||||
/>
|
||||
</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}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-900' />
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
export default React.memo(Pricing)
|
221
web/app/components/billing/pricing/plan-item.tsx
Normal file
221
web/app/components/billing/pricing/plan-item.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { Plan } from '../type'
|
||||
import { ALL_PLANS, NUM_INFINITE, contactSalesUrl } from '../config'
|
||||
import Toast from '../../base/toast'
|
||||
import { PlanRange } from './select-plan-range'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchSubscriptionUrls } from '@/service/billing'
|
||||
|
||||
type Props = {
|
||||
currentPlan: Plan
|
||||
plan: Plan
|
||||
planRange: PlanRange
|
||||
}
|
||||
|
||||
const KeyValue = ({ label, value }: { label: string; value: string | number | JSX.Element }) => {
|
||||
return (
|
||||
<div className='mt-3.5 leading-[125%] text-[13px] font-medium'>
|
||||
<div className='text-gray-500'>{label}</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,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
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 { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const btnText = (() => {
|
||||
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]
|
||||
})()
|
||||
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 t('billing.plansCommon.supportItems.communityForums')
|
||||
case Plan.professional:
|
||||
return t('billing.plansCommon.supportItems.emailSupport')
|
||||
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.logoChange')}</div>
|
||||
<div>{comingSoon}</div>
|
||||
</div>
|
||||
<div className='mt-3.5 flex items-center space-x-1'>
|
||||
<div>+ {t('billing.plansCommon.supportItems.personalizedSupport')}</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')
|
||||
|
||||
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] h-[712px] 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 pt-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 ? 'text-[#FB6514]' : 'text-gray-500', 'mb-4 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.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`)}
|
||||
/>
|
||||
<KeyValue
|
||||
label={t('billing.plansCommon.documentProcessingPriority')}
|
||||
value={t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`) 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.support')}
|
||||
value={supportContent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PlanItem)
|
55
web/app/components/billing/pricing/select-plan-range.tsx
Normal file
55
web/app/components/billing/pricing/select-plan-range.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '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)
|
60
web/app/components/billing/priority-label/index.tsx
Normal file
60
web/app/components/billing/priority-label/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
DocumentProcessingPriority,
|
||||
Plan,
|
||||
} from '../type'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
ZapFast,
|
||||
ZapNarrow,
|
||||
} from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
const PriorityLabel = () => {
|
||||
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 (
|
||||
<TooltipPlus 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={`
|
||||
flex items-center ml-1 px-[5px] h-[18px] rounded border border-[#C7D7FE]
|
||||
text-[10px] font-medium text-[#3538CD]
|
||||
`}>
|
||||
{
|
||||
plan.type === Plan.professional && (
|
||||
<ZapNarrow className='mr-0.5 w-3 h-3' />
|
||||
)
|
||||
}
|
||||
{
|
||||
(plan.type === Plan.team || plan.type === Plan.enterprise) && (
|
||||
<ZapFast className='mr-0.5 w-3 h-3' />
|
||||
)
|
||||
}
|
||||
{t(`billing.plansCommon.priority.${priority}`)}
|
||||
</span>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriorityLabel
|
22
web/app/components/billing/progress-bar/index.tsx
Normal file
22
web/app/components/billing/progress-bar/index.tsx
Normal 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]'>
|
||||
<div
|
||||
className='h-2 rounded-[4px]'
|
||||
style={{
|
||||
width: `${Math.min(percent, 100)}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgressBar
|
59
web/app/components/billing/type.ts
Normal file
59
web/app/components/billing/type.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
||||
documentProcessingPriority: Priority
|
||||
logHistory: number
|
||||
}
|
||||
|
||||
export type UsagePlanInfo = Pick<PlanInfo, 'vectorSpace' | 'buildApps' | 'teamMembers'>
|
||||
|
||||
export enum DocumentProcessingPriority {
|
||||
standard = 'standard',
|
||||
priority = 'priority',
|
||||
topPriority = 'top-priority',
|
||||
}
|
||||
|
||||
export type CurrentPlanInfoBackend = {
|
||||
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
|
||||
}
|
||||
docs_processing: DocumentProcessingPriority
|
||||
}
|
||||
|
||||
export type SubscriptionItem = {
|
||||
plan: Plan
|
||||
url: string
|
||||
}
|
||||
|
||||
export type SubscriptionUrlsBackend = {
|
||||
url: string
|
||||
}
|
68
web/app/components/billing/upgrade-btn/index.tsx
Normal file
68
web/app/components/billing/upgrade-btn/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { GoldCoin } from '../../base/icons/src/vender/solid/FinanceAndECommerce'
|
||||
import { Sparkles } from '../../base/icons/src/public/billing'
|
||||
import s from './style.module.css'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
isFull?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
isPlain?: boolean
|
||||
isShort?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const PlainBtn = ({ className, onClick }: { className?: string; onClick: () => {} }) => {
|
||||
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,
|
||||
isFull = false,
|
||||
isShort = false,
|
||||
size = 'md',
|
||||
onClick,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
|
||||
if (isPlain)
|
||||
return <PlainBtn onClick={onClick || setShowPricingModal as any} className={className} />
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
s.upgradeBtn,
|
||||
className,
|
||||
isFull ? 'justify-center' : 'px-3',
|
||||
size === 'lg' ? 'h-10' : 'h-9',
|
||||
'relative flex items-center cursor-pointer border rounded-[20px] border-[#0096EA] text-white',
|
||||
)}
|
||||
onClick={onClick || setShowPricingModal}
|
||||
>
|
||||
<GoldCoin className='mr-1 w-3.5 h-3.5' />
|
||||
<div className='text-xs font-normal'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
|
||||
<Sparkles
|
||||
className='absolute -right-1 -top-2 w-4 h-5 bg-cover'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(UpgradeBtn)
|
9
web/app/components/billing/upgrade-btn/style.module.css
Normal file
9
web/app/components/billing/upgrade-btn/style.module.css
Normal 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);
|
||||
}
|
32
web/app/components/billing/usage-info/apps-info.tsx
Normal file
32
web/app/components/billing/usage-info/apps-info.tsx
Normal 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)
|
75
web/app/components/billing/usage-info/index.tsx
Normal file
75
web/app/components/billing/usage-info/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InfoCircle } from '../../base/icons/src/vender/line/general'
|
||||
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 htmlContent={<div className='w-[180px]'>
|
||||
{tooltip}
|
||||
</div>} selector='config-var-tooltip'>
|
||||
<InfoCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</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)
|
34
web/app/components/billing/usage-info/vector-space-info.tsx
Normal file
34
web/app/components/billing/usage-info/vector-space-info.tsx
Normal 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)
|
25
web/app/components/billing/utils/index.ts
Normal file
25
web/app/components/billing/utils/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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.subscription.plan,
|
||||
usage: {
|
||||
vectorSpace: data.vector_space.size,
|
||||
buildApps: data.apps?.size || 0,
|
||||
teamMembers: data.members.size,
|
||||
},
|
||||
total: {
|
||||
vectorSpace: parseLimit(data.vector_space.limit),
|
||||
buildApps: parseLimit(data.apps?.limit) || 0,
|
||||
teamMembers: parseLimit(data.members.limit),
|
||||
},
|
||||
}
|
||||
}
|
32
web/app/components/billing/vector-space-full/index.tsx
Normal file
32
web/app/components/billing/vector-space-full/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import VectorSpaceInfo from '../usage-info/vector-space-info'
|
||||
import s from './style.module.css'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import GridMask from '@/app/components/base/grid-mask'
|
||||
|
||||
const VectorSpaceFull: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const { total } = plan
|
||||
|
||||
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 />
|
||||
</div>
|
||||
<VectorSpaceInfo className='pt-4' />
|
||||
</div>
|
||||
</GridMask>
|
||||
)
|
||||
}
|
||||
export default React.memo(VectorSpaceFull)
|
@@ -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;
|
||||
}
|
Reference in New Issue
Block a user