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 flex cursor-pointer flex-col rounded-lg border-2 border-solid border-transparent px-3.5 py-4 shadow-md transition-all duration-200 ease-in-out'>
<div className='flex items-center justify-between'>
<div className={cn(s.textGradient, 'text-base font-semibold leading-[24px]')}>
<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 flex cursor-pointer flex-col rounded-lg border-2 border-solid border-transparent px-7 py-6 shadow-md transition-all duration-200 ease-in-out'>
<div className='flex items-center justify-between'>
<div className={cn(s.textGradient, 'text-[18px] font-semibold leading-[27px]')}>
<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,84 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '../upgrade-btn'
import ProgressBar from '@/app/components/billing/progress-bar'
import Button from '@/app/components/base/button'
import { mailToSupport } from '@/app/components/header/utils/util'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import { Plan } from '@/app/components/billing/type'
import s from './style.module.css'
import cn from '@/utils/classnames'
const LOW = 50
const MIDDLE = 80
const AppsFull: FC<{ loc: string; className?: string; }> = ({
loc,
className,
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { userProfile, langeniusVersionInfo } = useAppContext()
const isTeam = plan.type === Plan.team
const usage = plan.usage.buildApps
const total = plan.total.buildApps
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(
'flex flex-col gap-3 rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg p-4 shadow-xs backdrop-blur-sm',
className,
)}>
<div className='flex justify-between'>
{!isTeam && (
<div>
<div className={cn('title-xl-semi-bold mb-1', s.textGradient)}>
{t('billing.apps.fullTip1')}
</div>
<div className='system-xs-regular text-text-tertiary'>{t('billing.apps.fullTip1des')}</div>
</div>
)}
{isTeam && (
<div>
<div className={cn('title-xl-semi-bold mb-1', s.textGradient)}>
{t('billing.apps.fullTip2')}
</div>
<div className='system-xs-regular text-text-tertiary'>{t('billing.apps.fullTip2des')}</div>
</div>
)}
{(plan.type === Plan.sandbox || plan.type === Plan.professional) && (
<UpgradeBtn isShort loc={loc} />
)}
{plan.type !== Plan.sandbox && plan.type !== Plan.professional && (
<Button variant='secondary-accent'>
<a target='_blank' rel='noopener noreferrer' href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}>
{t('billing.apps.contactUs')}
</a>
</Button>
)}
</div>
<div className='flex flex-col gap-2'>
<div className='system-xs-medium flex items-center justify-between text-text-secondary'>
<div>{t('billing.usagePage.buildApps')}</div>
<div>{usage}/{total}</div>
</div>
<ProgressBar
percent={percent}
color={color}
/>
</div>
</div>
)
}
export default React.memo(AppsFull)

View File

@@ -0,0 +1,7 @@
.textGradient {
background: linear-gradient(92deg, #0EBCF3 -29.55%, #2250F2 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='system-xs-medium flex cursor-pointer items-center text-text-accent-light-mode-only' href={billingUrl} target='_blank' rel='noopener noreferrer'>
<span className='pr-0.5'>{t('billing.viewBilling')}</span>
<RiArrowRightUpLine className='h-4 w-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 h-[22px] items-center rounded-md border px-2 text-xs font-semibold uppercase',
isDisplayOnly ? 'cursor-default' : 'cursor-pointer',
)}
>
{name}
</div>
)
}
export default React.memo(HeaderBillingBtn)

View File

@@ -0,0 +1,137 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import {
RiBook2Line,
RiBox3Line,
RiFileEditLine,
RiGraduationCapLine,
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 { useAppContext } from '@/context/app-context'
import Button from '@/app/components/base/button'
import UsageInfo from '@/app/components/billing/usage-info'
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants'
import { useEducationVerify } from '@/service/use-education'
import { useModalContextSelector } from '@/context/modal-context'
type Props = {
loc: string
}
const PlanComp: FC<Props> = ({
loc,
}) => {
const { t } = useTranslation()
const router = useRouter()
const { userProfile } = useAppContext()
const { plan, enableEducationPlan, isEducationAccount } = useProviderContext()
const {
type,
} = plan
const {
usage,
total,
} = plan
const [showModal, setShowModal] = React.useState(false)
const { mutateAsync } = useEducationVerify()
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
const handleVerify = () => {
mutateAsync().then((res) => {
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
router.push(`/education-apply?token=${res.token}`)
setShowAccountSettingModal(null)
}).catch(() => {
setShowModal(true)
})
}
return (
<div className='rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn'>
<div className='p-6 pb-2'>
{plan.type === Plan.sandbox && (
<RiBox3Line className='h-7 w-7 text-text-primary'/>
)}
{plan.type === Plan.professional && (
<RiSquareLine className='h-7 w-7 rotate-90 text-util-colors-blue-brand-blue-brand-600'/>
)}
{plan.type === Plan.team && (
<RiGroup3Line className='h-7 w-7 text-util-colors-indigo-indigo-600'/>
)}
{(plan.type as any) === SelfHostedPlan.enterprise && (
<RiGroup3Line className='h-7 w-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='system-md-semibold-uppercase text-text-primary'>{t(`billing.plans.${type}.name`)}</div>
<div className='system-2xs-medium-uppercase rounded-[5px] border border-divider-deep px-1 py-0.5 text-text-tertiary'>{t('billing.currentPlan')}</div>
</div>
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
<div className='flex shrink-0 items-center gap-1'>
{enableEducationPlan && !isEducationAccount && (
<Button variant='ghost' onClick={handleVerify}>
<RiGraduationCapLine className='mr-1 h-4 w-4'/>
{t('education.toVerified')}
</Button>
)}
{(plan.type as any) !== SelfHostedPlan.enterprise && (
<UpgradeBtn
className='shrink-0'
isPlain={type === Plan.team}
isShort
loc={loc}
/>
)}
</div>
</div>
</div>
{/* Plan detail */}
<div className='grid grid-cols-3 content-start gap-1 p-2'>
<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>
<VerifyStateModal
showLink
email={userProfile.email}
isShow={showModal}
title={t('education.rejectTitle')}
content={t('education.rejectContent')}
onConfirm={() => setShowModal(false)}
onCancel={() => setShowModal(false)}
/>
</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 bottom-0 left-0 right-0 top-0 z-[1000] bg-background-overlay-backdrop p-4 backdrop-blur-[6px]'
onClick={e => e.stopPropagation()}
>
<div className='relative h-full w-full overflow-auto rounded-2xl border border-effects-highlight bg-saas-background'>
<div
className='fixed right-7 top-7 z-[1001] flex h-9 w-9 cursor-pointer items-center justify-center rounded-[10px] bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover'
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='flex flex-col items-center px-8 pb-7 pt-12'>
<div className='title-5xl-bold mb-2 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='mx-auto w-[1152px]'>
<div className='flex h-[64px] items-center justify-between py-2'>
<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='mr-2 size-4' />{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='mr-2 size-4' />{t('billing.plansCommon.self')}</div>,
}]}
onChange={v => setCurrentPlan(v)} />
{currentPlan === 'cloud' && <SelectPlanRange
value={planRange}
onChange={setPlanRange}
/>}
</div>
<div className='pb-8 pt-3'>
<div className='flex flex-nowrap justify-center 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='flex items-center justify-center py-4'>
<div className='flex items-center justify-center gap-x-0.5 rounded-lg px-3 py-2 text-components-button-secondary-accent-text hover:cursor-pointer hover:bg-state-accent-hover'>
<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='flex size-4 items-center justify-center'>
{icon}
</div>
<div className='system-sm-regular ml-2 mr-0.5 text-text-primary'>{label}</div>
{tooltip && (
<Tooltip
asChild
popupContent={tooltip}
popupClassName='w-[200px]'
>
<div className='flex size-4 items-center justify-center'>
<RiQuestionLine className='text-text-quaternary' />
</div>
</Tooltip>
)}
</div>
)
}
const priceClassName = 'leading-[125%] text-[28px] font-bold text-text-primary'
const style = {
[Plan.sandbox]: {
icon: <ArCube1 className='size-7 text-text-primary' />,
description: 'text-util-colors-gray-gray-600',
btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary',
btnDisabledStyle: 'bg-components-button-secondary-bg-disabled hover:bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled',
},
[Plan.professional]: {
icon: <Keyframe className='size-7 text-util-colors-blue-brand-blue-brand-600' />,
description: 'text-util-colors-blue-brand-blue-brand-600',
btnStyle: 'bg-components-button-primary-bg hover:bg-components-button-primary-bg-hover border border-components-button-primary-border text-components-button-primary-text',
btnDisabledStyle: 'bg-components-button-primary-bg-disabled hover:bg-components-button-primary-bg-disabled border-components-button-primary-border-disabled text-components-button-primary-text-disabled',
},
[Plan.team]: {
icon: <Group2 className='size-7 text-util-colors-indigo-indigo-600' />,
description: 'text-util-colors-indigo-indigo-600',
btnStyle: 'bg-components-button-indigo-bg hover:bg-components-button-indigo-bg-hover border border-components-button-primary-border text-components-button-primary-text',
btnDisabledStyle: 'bg-components-button-indigo-bg-disabled hover:bg-components-button-indigo-bg-disabled border-components-button-indigo-border-disabled text-components-button-primary-text-disabled',
},
}
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 w-[373px] flex-col rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn p-6',
isMostPopularPlan ? 'border-effects-highlight shadow-lg backdrop-blur-[5px]' : 'hover:border-effects-highlight hover:shadow-lg hover:backdrop-blur-[5px]',
)}>
<div className='flex flex-col gap-y-1'>
{style[plan].icon}
<div className='flex items-center'>
<div className='text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 flex items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'>
<div className='pl-0.5'>
<SparklesSoft className='size-3' />
</div>
<span className='system-2xs-semibold-uppercase bg-price-premium-text-background bg-clip-text px-0.5 text-transparent'>{t('billing.plansCommon.mostPopular')}</span>
</div>}
</div>
<div className={cn(style[plan].description, 'system-sm-regular')}>{t(`${i18nPrefix}.description`)}</div>
</div>
<div className='my-5'>
{/* 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='text-[14px] font-normal italic leading-[14px] text-text-warning'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>}
<div className='text-[14px] font-normal leading-normal text-text-tertiary'>
{t('billing.plansCommon.priceTip')}
{t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</div>
</div>
</div>
)}
</div>
<div
className={cn('flex h-[42px] items-center justify-center rounded-full px-5 py-3',
style[plan].btnStyle,
isPlanDisabled && style[plan].btnDisabledStyle,
isPlanDisabled ? 'cursor-not-allowed' : 'cursor-pointer')}
onClick={handleGetPayUrl}
>
{btnText}
</div>
<div className='mt-6 flex flex-col gap-y-3'>
<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='bg-premium-yearly-tip-text-background bg-clip-text text-sm italic 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='flex size-4 items-center justify-center'>
<RiCheckLine />
</div>
<div className={cn('system-sm-regular ml-2 mr-0.5', textColor)}>{label}</div>
{tooltip && (
<Tooltip
asChild
popupContent={tooltip}
popupClassName='w-[200px]'
>
<div className='flex size-4 items-center justify-center'>
<RiQuestionLine className={cn(tooltipIconColor)} />
</div>
</Tooltip>
)}
</div>
)
}
const style = {
[SelfHostedPlan.community]: {
icon: <Asterisk className='size-7 text-text-primary' />,
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='size-7 text-text-warning' />,
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='size-7 text-text-primary-on-surface' />,
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 w-[374px] flex-col overflow-hidden rounded-2xl
border-[0.5px] hover:border-effects-highlight hover:shadow-lg hover:backdrop-blur-[5px]`, style[plan].bg)}>
<div>
<div className={cn(isEnterprisePlan ? 'z-1 absolute bottom-0 left-0 right-0 top-0 bg-price-enterprise-background' : '')} >
</div>
{isEnterprisePlan && <div className='z-15 absolute -left-[90px] -top-[104px] size-[341px] rounded-full bg-[#09328c] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
{isEnterprisePlan && <div className='z-15 absolute -bottom-[72px] -right-[40px] size-[341px] rounded-full bg-[#e2eafb] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
</div>
<div className='relative z-10 min-h-[559px] w-full p-6'>
<div className=' flex min-h-[108px] flex-col gap-y-1'>
{style[plan].icon}
<div className='flex items-center'>
<div className={cn('system-md-semibold uppercase leading-[125%]', 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('shrink-0 text-[28px] font-bold leading-[125%]', style[plan].price)}>{t(`${i18nPrefix}.price`)}</div>
{!isFreePlan
&& <span className={cn('ml-2 py-1 text-[14px] font-normal leading-normal', style[plan].priceTip)}>
{t(`${i18nPrefix}.priceTip`)}
</span>}
</div>
</div>
<div
className={cn('system-md-semibold flex h-[44px] cursor-pointer items-center justify-center rounded-full px-5 py-3',
style[plan].btnStyle)}
onClick={handleGetPayUrl}
>
{t(`${i18nPrefix}.btnText`)}
{isPremiumPlan
&& <>
<div className='mx-1 pt-[6px]'>
<AwsMarketplace className='h-6' />
</div>
<RiArrowRightUpLine className='size-4' />
</>}
</div>
<div className={cn('system-sm-semibold mb-2 mt-6', 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='flex size-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default shadow-xs'>
<Azure />
</div>
<div className='flex size-8 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('system-xs-regular mt-2', 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(`
ml-1 flex h-[18px] shrink-0 items-center rounded-[5px] border border-text-accent-secondary px-1
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='overflow-hidden rounded-[6px] bg-components-progress-bar-bg'>
<div
className={cn('h-1 rounded-[6px]', color)}
style={{
width: `${Math.min(percent, 100)}%`,
}}
/>
</div>
)
}
export default ProgressBar

View File

@@ -0,0 +1,103 @@
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
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
education: {
enabled: boolean
activated: 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 h-3.5 w-3.5 items-center py-[1px] pl-[3px] 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('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
<Icon className='h-4 w-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='system-md-semibold flex items-center gap-1 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='px-6 py-5'>
<div className='flex items-center justify-between'>
<div className={cn(s.textGradient, 'text-base font-semibold leading-[24px]')}>
<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;
}