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 {
RiArrowRightUpLine,
} from '@remixicon/react'
import PlanComp from '../plan'
import Divider from '@/app/components/base/divider'
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 && (
<>
<Divider className='my-4' />
<a className='flex items-center text-text-accent-light-mode-only system-xs-medium cursor-pointer' href={billingUrl} target='_blank' rel='noopener noreferrer'>
<span className='pr-0.5'>{t('billing.viewBilling')}</span>
<RiArrowRightUpLine className='w-4 h-4' />
</a>
</>
)}
</div>
)
}
export default React.memo(Billing)

View File

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

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,98 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiBook2Line,
RiBox3Line,
RiFileEditLine,
RiGroup3Line,
RiGroupLine,
RiSquareLine,
} from '@remixicon/react'
import { Plan, SelfHostedPlan } from '../type'
import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn'
import { useProviderContext } from '@/context/provider-context'
import UsageInfo from '@/app/components/billing/usage-info'
type Props = {
loc: string
}
const PlanComp: FC<Props> = ({
loc,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const {
type,
} = plan
const {
usage,
total,
} = plan
return (
<div className='bg-background-section-burn rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off'>
<div className='p-6 pb-2'>
{plan.type === Plan.sandbox && (
<RiBox3Line className='w-7 h-7 text-text-primary'/>
)}
{plan.type === Plan.professional && (
<RiSquareLine className='w-7 h-7 rotate-90 text-util-colors-blue-brand-blue-brand-600'/>
)}
{plan.type === Plan.team && (
<RiGroup3Line className='w-7 h-7 text-util-colors-indigo-indigo-600'/>
)}
{(plan.type as any) === SelfHostedPlan.enterprise && (
<RiGroup3Line className='w-7 h-7 text-util-colors-indigo-indigo-600'/>
)}
<div className='mt-1 flex items-center'>
<div className='grow'>
<div className='mb-1 flex items-center gap-1'>
<div className='text-text-primary system-md-semibold-uppercase'>{t(`billing.plans.${type}.name`)}</div>
<div className='px-1 py-0.5 border border-divider-deep rounded-[5px] text-text-tertiary system-2xs-medium-uppercase'>{t('billing.currentPlan')}</div>
</div>
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
{(plan.type as any) !== SelfHostedPlan.enterprise && (
<UpgradeBtn
className='shrink-0'
isPlain={type === Plan.team}
isShort
loc={loc}
/>
)}
</div>
</div>
{/* Plan detail */}
<div className='p-2 grid content-start grid-cols-3 gap-1'>
<AppsInfo />
<UsageInfo
Icon={RiGroupLine}
name={t('billing.usagePage.teamMembers')}
usage={usage.teamMembers}
total={total.teamMembers}
/>
<UsageInfo
Icon={RiBook2Line}
name={t('billing.usagePage.documentsUploadQuota')}
usage={usage.documentsUploadQuota}
total={total.documentsUploadQuota}
/>
<VectorSpaceInfo />
<UsageInfo
Icon={RiFileEditLine}
name={t('billing.usagePage.annotationQuota')}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
/>
</div>
</div>
)
}
export default React.memo(PlanComp)

View File

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

View File

@@ -0,0 +1,226 @@
'use client'
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine } from '@remixicon/react'
import { Plan } from '../type'
import { ALL_PLANS, NUM_INFINITE } 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'
type Props = {
currentPlan: Plan
plan: Plan
planRange: PlanRange
canPay: boolean
}
const KeyValue = ({ icon, label, tooltip }: { icon: ReactNode; label: string; tooltip?: ReactNode }) => {
return (
<div className='flex text-text-tertiary'>
<div className='size-4 flex items-center justify-center'>
{icon}
</div>
<div className='ml-2 mr-0.5 text-text-primary system-sm-regular'>{label}</div>
{tooltip && (
<Tooltip
asChild
popupContent={tooltip}
popupClassName='w-[200px]'
>
<div className='size-4 flex items-center justify-center'>
<RiQuestionLine className='text-text-quaternary' />
</div>
</Tooltip>
)}
</div>
)
}
const priceClassName = 'leading-[125%] text-[28px] font-bold text-text-primary'
const style = {
[Plan.sandbox]: {
icon: <ArCube1 className='text-text-primary size-7' />,
description: 'text-util-colors-gray-gray-600',
btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary',
btnDisabledStyle: 'bg-components-button-secondary-bg-disabled hover:bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled',
},
[Plan.professional]: {
icon: <Keyframe className='text-util-colors-blue-brand-blue-brand-600 size-7' />,
description: 'text-util-colors-blue-brand-blue-brand-600',
btnStyle: 'bg-components-button-primary-bg hover:bg-components-button-primary-bg-hover border border-components-button-primary-border text-components-button-primary-text',
btnDisabledStyle: 'bg-components-button-primary-bg-disabled hover:bg-components-button-primary-bg-disabled border-components-button-primary-border-disabled text-components-button-primary-text-disabled',
},
[Plan.team]: {
icon: <Group2 className='text-util-colors-indigo-indigo-600 size-7' />,
description: 'text-util-colors-indigo-indigo-600',
btnStyle: 'bg-components-button-indigo-bg hover:bg-components-button-indigo-bg-hover border border-components-button-primary-border text-components-button-primary-text',
btnDisabledStyle: 'bg-components-button-indigo-bg-disabled hover:bg-components-button-indigo-bg-disabled border-components-button-indigo-border-disabled text-components-button-primary-text-disabled',
},
}
const PlanItem: FC<Props> = ({
plan,
currentPlan,
planRange,
}) => {
const { t } = useTranslation()
const [loading, setLoading] = React.useState(false)
const i18nPrefix = `billing.plans.${plan}`
const isFreePlan = plan === Plan.sandbox
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.getStarted'),
[Plan.team]: t('billing.plansCommon.getStarted'),
})[plan]
})()
const handleGetPayUrl = async () => {
if (loading)
return
if (isPlanDisabled)
return
if (isFreePlan)
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('flex flex-col w-[373px] p-6 border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn rounded-2xl',
isMostPopularPlan ? 'shadow-lg backdrop-blur-[5px] border-effects-highlight' : 'hover:shadow-lg hover:backdrop-blur-[5px] hover:border-effects-highlight',
)}>
<div className='flex flex-col gap-y-1'>
{style[plan].icon}
<div className='flex items-center'>
<div className='leading-[125%] text-lg font-semibold uppercase text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 px-1 py-[3px] flex items-center justify-center rounded-full border-[0.5px] shadow-xs bg-price-premium-badge-background text-components-premium-badge-grey-text-stop-0'>
<div className='pl-0.5'>
<SparklesSoft className='size-3' />
</div>
<span className='px-0.5 system-2xs-semibold-uppercase bg-clip-text bg-price-premium-text-background text-transparent'>{t('billing.plansCommon.mostPopular')}</span>
</div>}
</div>
<div className={cn(style[plan].description, 'system-sm-regular')}>{t(`${i18nPrefix}.description`)}</div>
</div>
<div className='my-5'>
{/* Price */}
{isFreePlan && (
<div className={priceClassName}>{t('billing.plansCommon.free')}</div>
)}
{!isFreePlan && (
<div className='flex items-end'>
<div className={priceClassName}>${isYear ? planInfo.price * 10 : planInfo.price}</div>
<div className='ml-1 flex flex-col'>
{isYear && <div className='leading-[14px] text-[14px] font-normal italic text-text-warning'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>}
<div className='leading-normal text-[14px] font-normal text-text-tertiary'>
{t('billing.plansCommon.priceTip')}
{t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</div>
</div>
</div>
)}
</div>
<div
className={cn('flex py-3 px-5 rounded-full justify-center items-center h-[42px]',
style[plan].btnStyle,
isPlanDisabled && style[plan].btnDisabledStyle,
isPlanDisabled ? 'cursor-not-allowed' : 'cursor-pointer')}
onClick={handleGetPayUrl}
>
{btnText}
</div>
<div className='flex flex-col gap-y-3 mt-6'>
<KeyValue
icon={<RiChatAiLine />}
label={isFreePlan
? t('billing.plansCommon.messageRequest.title', { count: planInfo.messageRequest })
: t('billing.plansCommon.messageRequest.titlePerMonth', { count: planInfo.messageRequest })}
tooltip={t('billing.plansCommon.messageRequest.tooltip') as string}
/>
<KeyValue
icon={<RiBrain2Line />}
label={t('billing.plansCommon.modelProviders')}
/>
<KeyValue
icon={<RiFolder6Line />}
label={t('billing.plansCommon.teamWorkspace', { count: planInfo.teamWorkspace })}
/>
<KeyValue
icon={<RiGroupLine />}
label={t('billing.plansCommon.teamMember', { count: planInfo.teamMembers })}
/>
<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}
/>
<KeyValue
icon={<RiHardDrive3Line />}
label={t('billing.plansCommon.vectorSpace', { size: planInfo.vectorSpace })}
tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
/>
<KeyValue
icon={<RiSeoLine />}
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
/>
<KeyValue
icon={<RiProgress3Line />}
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
/>
<Divider bgStyle='gradient' />
<KeyValue
icon={<RiFileEditLine />}
label={t('billing.plansCommon.annotatedResponse.title', { count: planInfo.annotatedResponse })}
tooltip={t('billing.plansCommon.annotatedResponse.tooltip') as string}
/>
<KeyValue
icon={<RiHistoryLine />}
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
/>
</div>
</div>
)
}
export default React.memo(PlanItem)

View File

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

View File

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

View File

@@ -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,24 @@
import cn from '@/utils/classnames'
type ProgressBarProps = {
percent: number
color: string
}
const ProgressBar = ({
percent = 0,
color = '#2970FF',
}: ProgressBarProps) => {
return (
<div className='bg-components-progress-bar-bg rounded-[6px] overflow-hidden'>
<div
className={cn('h-1 rounded-[6px]', color)}
style={{
width: `${Math.min(percent, 100)}%`,
}}
/>
</div>
)
}
export default ProgressBar

View File

@@ -0,0 +1,98 @@
export enum Plan {
sandbox = 'sandbox',
professional = 'professional',
team = 'team',
}
export enum Priority {
standard = 'standard',
priority = 'priority',
topPriority = 'top-priority',
}
export type PlanInfo = {
level: number
price: number
modelProviders: string
teamWorkspace: number
teamMembers: number
buildApps: number
documents: number
vectorSpace: string
documentsUploadQuota: number
documentsRequestQuota: number
documentProcessingPriority: Priority
logHistory: number
messageRequest: number
annotatedResponse: number
}
export enum SelfHostedPlan {
community = 'community',
premium = 'premium',
enterprise = 'enterprise',
}
export type SelfHostedPlanInfo = {
level: number
price: number
modelProviders: string
teamWorkspace: number
teamMembers: number
buildApps: number
documents: number
vectorSpace: string
documentsRequestQuota: number
documentProcessingPriority: Priority
logHistory: number
messageRequest: number
annotatedResponse: number
}
export type UsagePlanInfo = Pick<PlanInfo, 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota'> & { vectorSpace: number }
export enum DocumentProcessingPriority {
standard = 'standard',
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,67 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import PremiumBadge from '../../base/premium-badge'
import Button from '@/app/components/base/button'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import { useModalContext } from '@/context/modal-context'
type Props = {
className?: string
isFull?: boolean
size?: 'md' | 'lg'
isPlain?: boolean
isShort?: boolean
onClick?: () => void
loc?: string
}
const UpgradeBtn: FC<Props> = ({
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 (
<Button onClick={onClick}>
{t('billing.upgradeBtn.plain')}
</Button>
)
}
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,34 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiApps2Line,
} from '@remixicon/react'
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={RiApps2Line}
name={t('billing.usagePage.buildApps')}
usage={usage.buildApps}
total={total.buildApps}
/>
)
}
export default React.memo(AppsInfo)

View File

@@ -0,0 +1,71 @@
'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'
import cn from '@/utils/classnames'
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 'bg-components-progress-bar-progress-solid'
if (percent < MIDDLE)
return 'bg-components-progress-warning-progress'
return 'bg-components-progress-error-progress'
})()
return (
<div className={cn('p-4 flex flex-col gap-2 rounded-xl bg-components-panel-bg', className)}>
<Icon className='w-4 h-4 text-text-tertiary' />
<div className='flex items-center gap-1'>
<div className='system-xs-medium text-text-tertiary'>{name}</div>
{tooltip && (
<Tooltip
popupContent={
<div className='w-[180px]'>
{tooltip}
</div>
}
/>
)}
</div>
<div className='flex items-center gap-1 system-md-semibold text-text-primary'>
{usage}
<div className='system-md-regular text-text-quaternary'>/</div>
<div>{total === NUM_INFINITE ? t('billing.plansCommon.unlimited') : `${total}${unit}`}</div>
</div>
<ProgressBar
percent={percent}
color={color}
/>
</div>
)
}
export default React.memo(UsageInfo)

View File

@@ -0,0 +1,36 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import {
RiHardDrive3Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
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={RiHardDrive3Line}
name={t('billing.usagePage.vectorSpace')}
tooltip={t('billing.usagePage.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;
}