From 648d9ef1f980e774da840e4764f74960c8b28d4e Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 4 Mar 2026 23:30:25 +0800 Subject: [PATCH] refactor(web): extract SystemQuotaCard compound component and shared useTrialCredits hook Extract trial credits calculation into a shared useTrialCredits hook to prevent logic drift between QuotaPanel and CredentialPanel. Add SystemQuotaCard compound component with explicit default/destructive variants for the system quota UI state in provider cards, replacing inline conditional styling with composable Label and Actions slots. Remove unnecessary useMemo for simple derived values. --- .../provider-added-card/credential-panel.tsx | 55 ++++++++++----- .../provider-added-card/quota-panel.tsx | 12 ++-- .../provider-added-card/system-quota-card.tsx | 67 +++++++++++++++++++ .../provider-added-card/use-trial-credits.ts | 13 ++++ web/eslint-suppressions.json | 3 - web/i18n/en-US/common.json | 1 + web/i18n/zh-Hans/common.json | 1 + 7 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/system-quota-card.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index ba7079ef88..fee8e43423 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -2,7 +2,6 @@ import type { ModelProvider, } from '../declarations' import { useQueryClient } from '@tanstack/react-query' -import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useToastContext } from '@/app/components/base/toast' import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth' @@ -25,6 +24,8 @@ import { import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index' import PrioritySelector from './priority-selector' import PriorityUseTip from './priority-use-tip' +import SystemQuotaCard from './system-quota-card' +import { useTrialCredits } from './use-trial-credits' type CredentialPanelProps = { provider: ModelProvider @@ -53,6 +54,8 @@ const CredentialPanel = ({ } = useCredentialStatus(provider) const showPrioritySelector = systemConfig.enabled && isCustomConfigured && IS_CLOUD_EDITION + const isUsingSystemQuota = systemConfig.enabled && priorityUseType === PreferredProviderTypeEnum.system && IS_CLOUD_EDITION + const { isExhausted } = useTrialCredits() const handleChangePriority = async (key: PreferredProviderTypeEnum) => { const res = await changeModelProviderPriority({ @@ -80,24 +83,40 @@ const CredentialPanel = ({ } as any) } } - const credentialLabel = useMemo(() => { - if (!hasCredential) - return t('modelProvider.auth.unAuthorized', { ns: 'common' }) - if (authorized) - return current_credential_name - if (authRemoved) - return t('modelProvider.auth.authRemoved', { ns: 'common' }) + const credentialLabel = !hasCredential + ? t('modelProvider.auth.unAuthorized', { ns: 'common' }) + : authorized + ? current_credential_name + : authRemoved + ? t('modelProvider.auth.authRemoved', { ns: 'common' }) + : '' - return '' - }, [authorized, authRemoved, current_credential_name, hasCredential, t]) + const color = (authRemoved || !hasCredential) + ? 'red' + : notAllowedToUse + ? 'gray' + : 'green' - const color = useMemo(() => { - if (authRemoved || !hasCredential) - return 'red' - if (notAllowedToUse) - return 'gray' - return 'green' - }, [authRemoved, notAllowedToUse, hasCredential]) + if (isUsingSystemQuota) { + return ( + + + {isExhausted + ? t('modelProvider.card.quotaExhausted', { ns: 'common' }) + : t('modelProvider.card.aiCreditsInUse', { ns: 'common' })} + + + + {showPrioritySelector && ( + + )} + + + ) + } return ( <> @@ -108,7 +127,7 @@ const CredentialPanel = ({ authRemoved && 'border-state-destructive-border bg-state-destructive-hover', )} > -
+
> = { [ModelProviderQuotaGetPaid.OPENAI]: OpenaiSmall, @@ -50,11 +50,9 @@ const QuotaPanel: FC = ({ providers, }) => { const { t } = useTranslation() - const { data: currentWorkspace, isPending: isPendingWorkspace } = useCurrentWorkspace() + const { credits, isExhausted, isLoading, nextCreditResetDate } = useTrialCredits() const { data: systemFeatures } = useSystemFeaturesQuery() const trialModels = systemFeatures?.trial_models ?? [] - const credits = Math.max(((currentWorkspace?.trial_credits ?? 0) - (currentWorkspace?.trial_credits_used ?? 0)) || 0, 0) - const isLoading = isPendingWorkspace && !currentWorkspace const providerMap = useMemo(() => new Map( providers.map(p => [p.provider, p.preferred_provider_type]), ), [providers]) @@ -111,7 +109,7 @@ const QuotaPanel: FC = ({ return (
= ({ {credits > 0 ? {formatNumber(credits)} : {t('modelProvider.card.quotaExhausted', { ns: 'common' })}} - {currentWorkspace?.next_credit_reset_date + {nextCreditResetDate ? ( <> · {t('modelProvider.resetDate', { ns: 'common', - date: formatTime(currentWorkspace.next_credit_reset_date, t('dateFormat', { ns: 'appLog' })), + date: formatTime(nextCreditResetDate!, t('dateFormat', { ns: 'appLog' })), interpolation: { escapeValue: false }, })} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/system-quota-card.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/system-quota-card.tsx new file mode 100644 index 0000000000..3bdc005ec9 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/system-quota-card.tsx @@ -0,0 +1,67 @@ +import type { ReactNode } from 'react' +import { createContext, useContext } from 'react' +import { cn } from '@/utils/classnames' +import styles from './quota-panel.module.css' + +type Variant = 'default' | 'destructive' + +const VariantContext = createContext('default') + +const containerVariants: Record = { + default: 'border-components-panel-border bg-white/[0.18]', + destructive: 'border-state-destructive-border bg-state-destructive-hover', +} + +const labelVariants: Record = { + default: 'text-text-secondary', + destructive: 'text-text-destructive', +} + +type SystemQuotaCardProps = { + variant?: Variant + children: ReactNode +} + +const SystemQuotaCard = ({ + variant = 'default', + children, +}: SystemQuotaCardProps) => { + return ( + +
+
+ {children} +
+ + ) +} + +const Label = ({ children }: { children: ReactNode }) => { + const variant = useContext(VariantContext) + return ( +
+ {children} +
+ ) +} + +const Actions = ({ children }: { children: ReactNode }) => { + return ( +
+ {children} +
+ ) +} + +SystemQuotaCard.Label = Label +SystemQuotaCard.Actions = Actions + +export default SystemQuotaCard diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts new file mode 100644 index 0000000000..e92bcd4b21 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts @@ -0,0 +1,13 @@ +import { useCurrentWorkspace } from '@/service/use-common' + +export const useTrialCredits = () => { + const { data: currentWorkspace, isPending } = useCurrentWorkspace() + const credits = Math.max(((currentWorkspace?.trial_credits ?? 0) - (currentWorkspace?.trial_credits_used ?? 0)) || 0, 0) + + return { + credits, + isExhausted: credits <= 0, + isLoading: isPending && !currentWorkspace, + nextCreditResetDate: currentWorkspace?.next_credit_reset_date, + } +} diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 98d6246028..da264df2e5 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -4858,9 +4858,6 @@ } }, "app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index a38416539a..9062cc77df 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -340,6 +340,7 @@ "modelProvider.auth.unAuthorized": "Unauthorized", "modelProvider.buyQuota": "Buy Quota", "modelProvider.callTimes": "Call times", + "modelProvider.card.aiCreditsInUse": "AI credits in use", "modelProvider.card.buyQuota": "Buy Quota", "modelProvider.card.callTimes": "Call times", "modelProvider.card.modelAPI": "{{modelName}} models are using the API Key.", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 0168669d34..adf54c1529 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -340,6 +340,7 @@ "modelProvider.auth.unAuthorized": "未授权", "modelProvider.buyQuota": "购买额度", "modelProvider.callTimes": "调用次数", + "modelProvider.card.aiCreditsInUse": "AI 额度使用中", "modelProvider.card.buyQuota": "购买额度", "modelProvider.card.callTimes": "调用次数", "modelProvider.card.modelAPI": "{{modelName}} 模型正在使用 API Key。",