From 9e763e80e821608cc1b4b077bd0e3b8f7c3950cc Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Sat, 15 Nov 2025 13:04:48 +0800 Subject: [PATCH] feat: add resets time in billing usage info --- web/app/components/billing/config.ts | 4 ++++ web/app/components/billing/plan/index.tsx | 10 +++++++++ web/app/components/billing/type.ts | 7 ++++++ .../components/billing/usage-info/index.tsx | 22 ++++++++++++++----- web/app/components/billing/utils/index.ts | 4 ++++ web/context/provider-context.tsx | 3 ++- web/i18n/en-US/billing.ts | 1 + web/i18n/ja-JP/billing.ts | 1 + web/i18n/zh-Hans/billing.ts | 1 + web/utils/time.ts | 7 ++++++ 10 files changed, 54 insertions(+), 6 deletions(-) diff --git a/web/app/components/billing/config.ts b/web/app/components/billing/config.ts index c0a21c1ebf..f343f4b487 100644 --- a/web/app/components/billing/config.ts +++ b/web/app/components/billing/config.ts @@ -90,4 +90,8 @@ export const defaultPlan = { apiRateLimit: ALL_PLANS.sandbox.apiRateLimit, triggerEvents: ALL_PLANS.sandbox.triggerEvents, }, + reset: { + apiRateLimit: null, + triggerEvents: null, + }, } diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index 42bd98334f..d66616bcd5 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -11,6 +11,7 @@ import { } from '@remixicon/react' import { Plan, SelfHostedPlan } from '../type' import { NUM_INFINITE } from '../config' +import { getDaysUntilEndOfMonth } from '@/utils/time' import VectorSpaceInfo from '../usage-info/vector-space-info' import AppsInfo from '../usage-info/apps-info' import UpgradeBtn from '../upgrade-btn' @@ -44,7 +45,14 @@ const PlanComp: FC = ({ const { usage, total, + reset, } = plan + const triggerEventsResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE + ? reset.triggerEvents ?? undefined + : undefined + const apiRateLimitResetInDays = type === Plan.sandbox && total.apiRateLimit !== NUM_INFINITE + ? getDaysUntilEndOfMonth() + : undefined const [showModal, setShowModal] = React.useState(false) const { mutateAsync } = useEducationVerify() @@ -126,6 +134,7 @@ const PlanComp: FC = ({ usage={usage.triggerEvents} total={total.triggerEvents} tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string} + resetInDays={triggerEventsResetInDays} /> = ({ usage={usage.apiRateLimit} total={total.apiRateLimit} tooltip={total.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string} + resetInDays={apiRateLimitResetInDays} /> diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 081cfb4edd..03877cdcbf 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -55,6 +55,11 @@ export type SelfHostedPlanInfo = { export type UsagePlanInfo = Pick & { vectorSpace: number } +export type UsageResetInfo = { + apiRateLimit?: number | null + triggerEvents?: number | null +} + export enum DocumentProcessingPriority { standard = 'standard', priority = 'priority', @@ -91,10 +96,12 @@ export type CurrentPlanInfoBackend = { api_rate_limit?: { size: number limit: number // total. 0 means unlimited + reset_in_days?: number } trigger_events?: { size: number limit: number // total. 0 means unlimited + reset_in_days?: number } docs_processing: DocumentProcessingPriority can_replace_logo: boolean diff --git a/web/app/components/billing/usage-info/index.tsx b/web/app/components/billing/usage-info/index.tsx index 1434c376ad..40fdde61ae 100644 --- a/web/app/components/billing/usage-info/index.tsx +++ b/web/app/components/billing/usage-info/index.tsx @@ -16,6 +16,8 @@ type Props = { total: number unit?: string unitPosition?: 'inline' | 'suffix' + resetHint?: string + resetInDays?: number } const WARNING_THRESHOLD = 80 @@ -29,6 +31,8 @@ const UsageInfo: FC = ({ total, unit, unitPosition = 'suffix', + resetHint, + resetInDays, }) => { const { t } = useTranslation() @@ -41,6 +45,18 @@ const UsageInfo: FC = ({ if (!isUnlimited && unit && unitPosition === 'inline') totalDisplay = `${total}${unit}` const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix' + const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('billing.usagePage.resetsIn', { count: resetInDays }) : undefined) + const rightInfo = resetText + ? ( +
+ {resetText} +
+ ) + : (showUnit && ( +
+ {unit} +
+ )) return (
@@ -63,11 +79,7 @@ const UsageInfo: FC = ({
/
{totalDisplay}
- {showUnit && ( -
- {unit} -
- )} + {rightInfo} { apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE), triggerEvents: resolveLimit(data.trigger_events?.limit, planPreset?.triggerEvents), }, + reset: { + apiRateLimit: data.api_rate_limit?.reset_in_days ?? null, + triggerEvents: data.trigger_events?.reset_in_days ?? null, + }, } } diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 90233fbc21..26617921f1 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -17,7 +17,7 @@ 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 type { Plan } from '@/app/components/billing/type' +import type { Plan, UsageResetInfo } 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' @@ -40,6 +40,7 @@ type ProviderContextState = { type: Plan usage: UsagePlanInfo total: UsagePlanInfo + reset: UsageResetInfo } isFetchedPlan: boolean enableBilling: boolean diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts index 25121f5b17..40f5cfcea8 100644 --- a/web/i18n/en-US/billing.ts +++ b/web/i18n/en-US/billing.ts @@ -9,6 +9,7 @@ const translation = { 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', + resetsIn: 'Resets in {{count,number}} days', }, teamMembers: 'Team Members', upgradeBtn: { diff --git a/web/i18n/ja-JP/billing.ts b/web/i18n/ja-JP/billing.ts index 1a28eac12d..9dd06f7db4 100644 --- a/web/i18n/ja-JP/billing.ts +++ b/web/i18n/ja-JP/billing.ts @@ -9,6 +9,7 @@ const translation = { vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。', triggerEvents: 'トリガーイベント数', perMonth: '月あたり', + resetsIn: '{{count,number}}日後にリセット', }, upgradeBtn: { plain: 'プランをアップグレード', diff --git a/web/i18n/zh-Hans/billing.ts b/web/i18n/zh-Hans/billing.ts index dfa4488985..d734ae2652 100644 --- a/web/i18n/zh-Hans/billing.ts +++ b/web/i18n/zh-Hans/billing.ts @@ -9,6 +9,7 @@ const translation = { vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。', triggerEvents: '触发器事件数', perMonth: '每月', + resetsIn: '{{count,number}} 天后重置', }, upgradeBtn: { plain: '查看套餐', diff --git a/web/utils/time.ts b/web/utils/time.ts index ff2e38321f..daa54a5bf3 100644 --- a/web/utils/time.ts +++ b/web/utils/time.ts @@ -10,3 +10,10 @@ export const isAfter = (date: ConfigType, compare: ConfigType) => { export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => { return dayjs(date).format(dateFormat) } + +export const getDaysUntilEndOfMonth = (date: ConfigType = dayjs()) => { + const current = dayjs(date).startOf('day') + const endOfMonth = dayjs(date).endOf('month').startOf('day') + const diff = endOfMonth.diff(current, 'day') + return Math.max(diff, 0) +}