diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 483913a62e..f79fb83ace 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/web/app/components/base/date-and-time-picker/types.ts @@ -1,4 +1,5 @@ import type { Dayjs } from 'dayjs' +import type { Placement } from '@floating-ui/react' export enum ViewType { date = 'date', @@ -65,6 +66,10 @@ export type TimePickerProps = { title?: string minuteFilter?: (minutes: string[]) => string[] popupClassName?: string + notClearable?: boolean + triggerFullWidth?: boolean + showTimezone?: boolean + placement?: Placement } export type TimePickerFooterProps = { diff --git a/web/app/components/billing/config.ts b/web/app/components/billing/config.ts index 1d5fbc7491..c0a21c1ebf 100644 --- a/web/app/components/billing/config.ts +++ b/web/app/components/billing/config.ts @@ -26,6 +26,7 @@ export const ALL_PLANS: Record = { apiRateLimit: 5000, documentProcessingPriority: Priority.standard, messageRequest: 200, + triggerEvents: 3000, annotatedResponse: 10, logHistory: 30, }, @@ -43,6 +44,7 @@ export const ALL_PLANS: Record = { apiRateLimit: NUM_INFINITE, documentProcessingPriority: Priority.priority, messageRequest: 5000, + triggerEvents: 20000, annotatedResponse: 2000, logHistory: NUM_INFINITE, }, @@ -60,6 +62,7 @@ export const ALL_PLANS: Record = { apiRateLimit: NUM_INFINITE, documentProcessingPriority: Priority.topPriority, messageRequest: 10000, + triggerEvents: NUM_INFINITE, annotatedResponse: 5000, logHistory: NUM_INFINITE, }, @@ -74,6 +77,8 @@ export const defaultPlan = { teamMembers: 1, annotatedResponse: 1, documentsUploadQuota: 0, + apiRateLimit: 0, + triggerEvents: 0, }, total: { documents: 50, @@ -82,5 +87,7 @@ export const defaultPlan = { teamMembers: 1, annotatedResponse: 10, documentsUploadQuota: 0, + apiRateLimit: ALL_PLANS.sandbox.apiRateLimit, + triggerEvents: ALL_PLANS.sandbox.triggerEvents, }, } diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index dd3908635b..4b68fcfb15 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -6,8 +6,10 @@ import { useRouter } from 'next/navigation' import { RiBook2Line, RiFileEditLine, + RiFlashlightLine, RiGraduationCapLine, RiGroupLine, + RiSpeedLine, } from '@remixicon/react' import { Plan, SelfHostedPlan } from '../type' import VectorSpaceInfo from '../usage-info/vector-space-info' @@ -43,6 +45,8 @@ const PlanComp: FC = ({ usage, total, } = plan + const perMonthUnit = ` ${t('billing.usagePage.perMonth')}` + const triggerEventUnit = plan.type === Plan.sandbox ? undefined : perMonthUnit const [showModal, setShowModal] = React.useState(false) const { mutateAsync } = useEducationVerify() @@ -119,6 +123,20 @@ const PlanComp: FC = ({ usage={usage.annotatedResponse} total={total.annotatedResponse} /> + + + + + & { vectorSpace: number } +export type UsagePlanInfo = Pick & { vectorSpace: number } export enum DocumentProcessingPriority { standard = 'standard', @@ -87,6 +88,14 @@ export type CurrentPlanInfoBackend = { size: number limit: number // total. 0 means unlimited } + api_rate_limit?: { + size: number + limit: number // total. 0 means unlimited + } + trigger_events?: { + size: number + limit: number // total. 0 means unlimited + } docs_processing: DocumentProcessingPriority can_replace_logo: boolean model_load_balancing_enabled: boolean diff --git a/web/app/components/billing/usage-info/index.tsx b/web/app/components/billing/usage-info/index.tsx index 30b4bca776..0ed8775772 100644 --- a/web/app/components/billing/usage-info/index.tsx +++ b/web/app/components/billing/usage-info/index.tsx @@ -15,6 +15,7 @@ type Props = { usage: number total: number unit?: string + unitPosition?: 'inline' | 'suffix' } const LOW = 50 @@ -27,7 +28,8 @@ const UsageInfo: FC = ({ tooltip, usage, total, - unit = '', + unit, + unitPosition = 'suffix', }) => { const { t } = useTranslation() @@ -41,6 +43,12 @@ const UsageInfo: FC = ({ return 'bg-components-progress-error-progress' })() + const isUnlimited = total === NUM_INFINITE + let totalDisplay: string | number = isUnlimited ? t('billing.plansCommon.unlimited') : total + if (!isUnlimited && unit && unitPosition === 'inline') + totalDisplay = `${total}${unit}` + const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix' + return (
@@ -56,10 +64,17 @@ const UsageInfo: FC = ({ /> )}
-
- {usage} -
/
-
{total === NUM_INFINITE ? t('billing.plansCommon.unlimited') : `${total}${unit}`}
+
+
+ {usage} +
/
+
{totalDisplay}
+
+ {showUnit && ( +
+ {unit} +
+ )}
= ({ usage={usage.vectorSpace} total={total.vectorSpace} unit='MB' + unitPosition='inline' /> ) } diff --git a/web/app/components/billing/utils/index.ts b/web/app/components/billing/utils/index.ts index 111f02e3cf..00ab7913b5 100644 --- a/web/app/components/billing/utils/index.ts +++ b/web/app/components/billing/utils/index.ts @@ -1,5 +1,5 @@ import type { CurrentPlanInfoBackend } from '../type' -import { NUM_INFINITE } from '@/app/components/billing/config' +import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config' const parseLimit = (limit: number) => { if (limit === 0) @@ -9,14 +9,23 @@ const parseLimit = (limit: number) => { } export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => { + const planType = data.billing.subscription.plan + const planPreset = ALL_PLANS[planType] + const resolveLimit = (limit?: number, fallback?: number) => { + const value = limit ?? fallback ?? 0 + return parseLimit(value) + } + return { - type: data.billing.subscription.plan, + type: planType, 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, + apiRateLimit: data.api_rate_limit?.size ?? 0, + triggerEvents: data.trigger_events?.size ?? 0, }, total: { vectorSpace: parseLimit(data.vector_space.limit), @@ -24,6 +33,8 @@ export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => { teamMembers: parseLimit(data.members.limit), annotatedResponse: parseLimit(data.annotation_quota_limit.limit), documentsUploadQuota: parseLimit(data.documents_upload_quota.limit), + apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE), + triggerEvents: resolveLimit(data.trigger_events?.limit, planPreset?.triggerEvents), }, } } diff --git a/web/app/education-apply/constants.ts b/web/app/education-apply/constants.ts index 2c42d2b263..75b6da67e5 100644 --- a/web/app/education-apply/constants.ts +++ b/web/app/education-apply/constants.ts @@ -2,3 +2,4 @@ export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify' export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying' export const EDUCATION_PRICING_SHOW_ACTION = 'educationPricing' export const EDUCATION_RE_VERIFY_ACTION = 'educationReVerify' +export const SHOW_PRICING_MODAL_ACTION = 'showPricing' diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index 5baadc934b..708a951dde 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -3,7 +3,7 @@ import type { Dispatch, SetStateAction } from 'react' import { useCallback, useState } from 'react' import { createContext, useContext, useContextSelector } from 'use-context-selector' -import { useRouter, useSearchParams } from 'next/navigation' +import { useSearchParams } from 'next/navigation' import type { ConfigurationMethodEnum, Credential, @@ -13,6 +13,7 @@ import type { } from '@/app/components/header/account-setting/model-provider-page/declarations' import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, + SHOW_PRICING_MODAL_ACTION, } from '@/app/education-apply/constants' import type { ModerationConfig, PromptVariable } from '@/models/debug' import type { @@ -151,8 +152,9 @@ export const ModalContextProvider = ({ const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState | null>(null) const searchParams = useSearchParams() - const router = useRouter() - const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1') + const [showPricingModal, setShowPricingModal] = useState( + searchParams.get('action') === SHOW_PRICING_MODAL_ACTION, + ) const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) const handleCancelAccountSettingModal = () => { const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) @@ -308,8 +310,6 @@ export const ModalContextProvider = ({ { !!showPricingModal && ( { - if (searchParams.get('show-pricing') === '1') - router.push(location.pathname, { forceOptimisticNavigation: true } as any) removeSpecificQueryParam('action') setShowPricingModal(false) }} /> diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 755131c859..90233fbc21 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -17,7 +17,8 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { RETRIEVE_METHOD } from '@/types/app' -import { Plan, type UsagePlanInfo } from '@/app/components/billing/type' +import type { Plan } from '@/app/components/billing/type' +import type { UsagePlanInfo } from '@/app/components/billing/type' import { fetchCurrentPlanInfo } from '@/service/billing' import { parseCurrentPlan } from '@/app/components/billing/utils' import { defaultPlan } from '@/app/components/billing/config' @@ -70,23 +71,7 @@ const ProviderContext = createContext({ textGenerationModelList: [], supportRetrievalMethods: [], isAPIKeySet: true, - plan: { - type: Plan.sandbox, - usage: { - vectorSpace: 32, - buildApps: 12, - teamMembers: 1, - annotatedResponse: 1, - documentsUploadQuota: 50, - }, - total: { - vectorSpace: 200, - buildApps: 50, - teamMembers: 1, - annotatedResponse: 10, - documentsUploadQuota: 500, - }, - }, + plan: defaultPlan, isFetchedPlan: false, enableBilling: false, onPlanInfoChanged: noop, diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts index 927f41a21d..0bd26c1075 100644 --- a/web/i18n/en-US/billing.ts +++ b/web/i18n/en-US/billing.ts @@ -7,6 +7,8 @@ const translation = { documentsUploadQuota: 'Documents Upload Quota', vectorSpace: 'Knowledge Data Storage', vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.', + triggerEvents: 'Trigger Events', + perMonth: 'per month', }, teamMembers: 'Team Members', upgradeBtn: { @@ -72,6 +74,20 @@ const translation = { 'priority': 'Priority', 'top-priority': 'Top Priority', }, + triggerEvents: { + sandbox: '{{count,number}} Trigger Events', + professional: '{{count,number}} Trigger Events/month', + unlimited: 'Unlimited Trigger Events', + }, + workflowExecution: { + standard: 'Standard Workflow Execution', + faster: 'Faster Workflow Execution', + priority: 'Priority Workflow Execution', + }, + startNodes: { + limited: 'Up to {{count}} Start Nodes per Workflow', + unlimited: 'Unlimited Start Nodes per Workflow', + }, logsHistory: '{{days}} Log history', customTools: 'Custom Tools', unavailable: 'Unavailable', diff --git a/web/i18n/ja-JP/billing.ts b/web/i18n/ja-JP/billing.ts index e13cd3ade8..b679ae571a 100644 --- a/web/i18n/ja-JP/billing.ts +++ b/web/i18n/ja-JP/billing.ts @@ -7,6 +7,8 @@ const translation = { documentsUploadQuota: 'ドキュメント・アップロード・クォータ', vectorSpace: 'ナレッジベースのデータストレージ', vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。', + triggerEvents: 'トリガーイベント', + perMonth: '月あたり', }, upgradeBtn: { plain: 'プランをアップグレード', diff --git a/web/i18n/zh-Hans/billing.ts b/web/i18n/zh-Hans/billing.ts index e4e6e30f90..3c50abd01f 100644 --- a/web/i18n/zh-Hans/billing.ts +++ b/web/i18n/zh-Hans/billing.ts @@ -7,6 +7,8 @@ const translation = { documentsUploadQuota: '文档上传配额', vectorSpace: '知识库数据存储空间', vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。', + triggerEvents: '触发事件', + perMonth: '每月', }, upgradeBtn: { plain: '查看套餐', @@ -71,6 +73,20 @@ const translation = { 'priority': '优先', 'top-priority': '最高优先级', }, + triggerEvents: { + sandbox: '{{count,number}} 触发事件', + professional: '{{count,number}} 触发事件/月', + unlimited: '无限制触发事件', + }, + workflowExecution: { + standard: '标准工作流执行', + faster: '更快的工作流执行', + priority: '优先工作流执行', + }, + startNodes: { + limited: '每个工作流最多 {{count}} 个起始节点', + unlimited: '每个工作流无限制起始节点', + }, logsHistory: '{{days}}日志历史', customTools: '自定义工具', unavailable: '不可用',