From b8b70da9ad8ed3610cc222a81401ee7d29f70f26 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 5 Mar 2026 08:33:04 +0800 Subject: [PATCH] refactor(web): rewrite CredentialPanel with declarative variant-driven state and new ModelAuthDropdown - Extract useCredentialPanelState hook with discriminated union CardVariant type replacing scattered boolean conditions - Create ModelAuthDropdown compound component (Popover-based) with UsagePrioritySection, CreditsExhaustedAlert, and ApiKeySection - Enhance SystemQuotaCard.Label to accept className override for flexible styling - Add i18n keys for new card states and dropdown content (en-US, zh-Hans) --- .../provider-added-card/credential-panel.tsx | 196 +++++++----------- .../model-auth-dropdown/api-key-section.tsx | 88 ++++++++ .../credits-exhausted-alert.tsx | 51 +++++ .../model-auth-dropdown/dropdown-content.tsx | 123 +++++++++++ .../model-auth-dropdown/index.tsx | 78 +++++++ .../usage-priority-section.tsx | 48 +++++ .../provider-added-card/system-quota-card.tsx | 6 +- .../use-credential-panel-state.ts | 100 +++++++++ web/eslint-suppressions.json | 20 -- web/i18n/en-US/common.json | 14 ++ web/i18n/zh-Hans/common.json | 14 ++ 11 files changed, 599 insertions(+), 139 deletions(-) create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/index.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/usage-priority-section.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.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 fee8e43423..c6655b976b 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 @@ -1,36 +1,40 @@ import type { ModelProvider, + PreferredProviderTypeEnum, } from '../declarations' +import type { CardVariant } from './use-credential-panel-state' import { useQueryClient } from '@tanstack/react-query' +import { useCallback } 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' -import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks' import Indicator from '@/app/components/header/indicator' -import { IS_CLOUD_EDITION } from '@/config' import { useEventEmitterContextContext } from '@/context/event-emitter' import { consoleQuery } from '@/service/client' import { changeModelProviderPriority } from '@/service/common' -import { cn } from '@/utils/classnames' import { ConfigurationMethodEnum, - CustomConfigurationStatusEnum, - PreferredProviderTypeEnum, } from '../declarations' import { useUpdateModelList, useUpdateModelProviders, } from '../hooks' import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index' -import PrioritySelector from './priority-selector' -import PriorityUseTip from './priority-use-tip' +import ModelAuthDropdown from './model-auth-dropdown' import SystemQuotaCard from './system-quota-card' -import { useTrialCredits } from './use-trial-credits' +import { isDestructiveVariant, useCredentialPanelState } from './use-credential-panel-state' type CredentialPanelProps = { provider: ModelProvider } +const TEXT_LABEL_VARIANTS = new Set([ + 'credits-active', + 'credits-exhausted', + 'no-usage', + 'api-required-add', + 'api-required-configure', +]) + const CredentialPanel = ({ provider, }: CredentialPanelProps) => { @@ -40,29 +44,12 @@ const CredentialPanel = ({ const queryClient = useQueryClient() const updateModelList = useUpdateModelList() const updateModelProviders = useUpdateModelProviders() - const customConfig = provider.custom_configuration - const systemConfig = provider.system_configuration - const priorityUseType = provider.preferred_provider_type - const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active - const configurateMethods = provider.configurate_methods - const { - hasCredential, - authorized, - authRemoved, - current_credential_name, - notAllowedToUse, - } = useCredentialStatus(provider) + const state = useCredentialPanelState(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 handleChangePriority = useCallback(async (key: PreferredProviderTypeEnum) => { const res = await changeModelProviderPriority({ url: `/workspaces/current/model-providers/${provider.provider}/preferred-provider-type`, - body: { - preferred_provider_type: key, - }, + body: { preferred_provider_type: key }, }) if (res.result === 'success') { notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) @@ -71,105 +58,82 @@ const CredentialPanel = ({ refetchType: 'none', }) updateModelProviders() - - configurateMethods.forEach((method) => { + provider.configurate_methods.forEach((method) => { if (method === ConfigurationMethodEnum.predefinedModel) provider.supported_model_types.forEach(modelType => updateModelList(modelType)) }) - eventEmitter?.emit({ type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST, payload: provider.provider, - } as any) + } as { type: string, payload: string }) } - } - const credentialLabel = !hasCredential - ? t('modelProvider.auth.unAuthorized', { ns: 'common' }) - : authorized - ? current_credential_name - : authRemoved - ? t('modelProvider.auth.authRemoved', { ns: 'common' }) - : '' + }, [provider, notify, t, queryClient, updateModelProviders, updateModelList, eventEmitter]) - const color = (authRemoved || !hasCredential) - ? 'red' - : notAllowedToUse - ? 'gray' - : 'green' + const { variant, credentialName } = state + const isDestructive = isDestructiveVariant(variant) + const isTextLabel = TEXT_LABEL_VARIANTS.has(variant) - if (isUsingSystemQuota) { - return ( - - - {isExhausted - ? t('modelProvider.card.quotaExhausted', { ns: 'common' }) - : t('modelProvider.card.aiCreditsInUse', { ns: 'common' })} - - - - {showPrioritySelector && ( - - )} - - - ) - } + return ( + + + {isTextLabel + ? + : } + + + + + + ) +} + +function TextLabel({ variant }: { variant: CardVariant }) { + const { t } = useTranslation() + const isDestructive = isDestructiveVariant(variant) + const labelKey = variant === 'credits-active' + ? 'modelProvider.card.aiCreditsInUse' + : variant === 'credits-exhausted' + ? 'modelProvider.card.quotaExhausted' + : variant === 'no-usage' + ? 'modelProvider.card.noAvailableUsage' + : 'modelProvider.card.apiKeyRequired' + + return ( + + {t(labelKey, { ns: 'common' })} + + ) +} + +function StatusLabel({ variant, credentialName }: { + variant: CardVariant + credentialName: string | undefined +}) { + const { t } = useTranslation() + const dotColor = variant === 'api-unavailable' ? 'red' : 'green' + const showWarning = variant === 'api-fallback' return ( <> - { - provider.provider_credential_schema && ( -
-
-
- {credentialLabel} -
- -
-
- - { - showPrioritySelector && ( - - ) - } -
- { - priorityUseType === PreferredProviderTypeEnum.custom && systemConfig.enabled && ( - - ) - } -
- ) - } - { - showPrioritySelector && !provider.provider_credential_schema && ( -
- -
- ) - } + + + {credentialName} + + {showWarning && ( + + )} + {variant === 'api-unavailable' && ( + + {t('modelProvider.card.unavailable', { ns: 'common' })} + + )} ) } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx new file mode 100644 index 0000000000..74a9229704 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx @@ -0,0 +1,88 @@ +import type { Credential, CustomModel, ModelProvider } from '../../declarations' +import { memo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import CredentialItem from '../../model-auth/authorized/credential-item' + +type ApiKeySectionProps = { + provider: ModelProvider + credentials: Credential[] + selectedCredentialId: string | undefined + onItemClick: (credential: Credential, model?: CustomModel) => void + onEdit: (credential?: Credential) => void + onDelete: (credential?: Credential) => void + onAdd: () => void +} + +function ApiKeySection({ + provider, + credentials, + selectedCredentialId, + onItemClick, + onEdit, + onDelete, + onAdd, +}: ApiKeySectionProps) { + const { t } = useTranslation() + const notAllowCustomCredential = provider.allow_custom_token === false + + const handleItemClick = useCallback((credential: Credential) => { + onItemClick(credential) + }, [onItemClick]) + + if (!credentials.length) { + return ( +
+
+
+ {t('modelProvider.card.noApiKeysTitle', { ns: 'common' })} +
+
+ {t('modelProvider.card.noApiKeysDescription', { ns: 'common' })} +
+
+ {!notAllowCustomCredential && ( + + )} +
+ ) + } + + return ( +
+
+ {t('modelProvider.auth.apiKeys', { ns: 'common' })} +
+
+ {credentials.map(credential => ( + + ))} +
+ {!notAllowCustomCredential && ( +
+ +
+ )} +
+ ) +} + +export default memo(ApiKeySection) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.tsx new file mode 100644 index 0000000000..99a3c68a97 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.tsx @@ -0,0 +1,51 @@ +import { useTranslation } from 'react-i18next' +import { formatNumber } from '@/utils/format' +import { useTrialCredits } from '../use-trial-credits' + +type CreditsExhaustedAlertProps = { + hasApiKeyFallback: boolean +} + +export default function CreditsExhaustedAlert({ hasApiKeyFallback }: CreditsExhaustedAlertProps) { + const { t } = useTranslation() + const { credits } = useTrialCredits() + const totalCredits = 10_000 + + const titleKey = hasApiKeyFallback + ? 'modelProvider.card.creditsExhaustedFallback' + : 'modelProvider.card.creditsExhaustedMessage' + const descriptionKey = hasApiKeyFallback + ? 'modelProvider.card.creditsExhaustedFallbackDescription' + : 'modelProvider.card.creditsExhaustedDescription' + + return ( +
+
+ {t(titleKey, { ns: 'common' })} +
+
+ {t(descriptionKey, { + ns: 'common', + upgradeLink: `${t('modelProvider.card.upgradePlan', { ns: 'common' })}`, + interpolation: { escapeValue: false }, + })} +
+
+ + {t('modelProvider.card.usageLabel', { ns: 'common' })} + +
+ + + {formatNumber(totalCredits - credits)} + / + {formatNumber(totalCredits)} + +
+
+
+
+
+
+ ) +} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx new file mode 100644 index 0000000000..f0ffa5242e --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/dropdown-content.tsx @@ -0,0 +1,123 @@ +import type { Credential, ModelProvider, PreferredProviderTypeEnum } from '../../declarations' +import type { CredentialPanelState } from '../use-credential-panel-state' +import { memo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' +import { ConfigurationMethodEnum } from '../../declarations' +import { useAuth } from '../../model-auth/hooks' +import ApiKeySection from './api-key-section' +import CreditsExhaustedAlert from './credits-exhausted-alert' +import UsagePrioritySection from './usage-priority-section' + +type DropdownContentProps = { + provider: ModelProvider + state: CredentialPanelState + onChangePriority: (key: PreferredProviderTypeEnum) => void + onClose: () => void +} + +function DropdownContent({ + provider, + state, + onChangePriority, + onClose, +}: DropdownContentProps) { + const { t } = useTranslation() + const { + current_credential_id, + available_credentials, + } = provider.custom_configuration + + const { + openConfirmDelete, + closeConfirmDelete, + doingAction, + handleActiveCredential, + handleConfirmDelete, + deleteCredentialId, + handleOpenModal, + } = useAuth(provider, ConfigurationMethodEnum.predefinedModel) + + const handleItemClick = useCallback((credential: Credential) => { + handleActiveCredential(credential) + onClose() + }, [handleActiveCredential, onClose]) + + const handleEdit = useCallback((credential?: Credential) => { + handleOpenModal(credential) + onClose() + }, [handleOpenModal, onClose]) + + const handleDelete = useCallback((credential?: Credential) => { + if (credential) + openConfirmDelete(credential) + }, [openConfirmDelete]) + + const handleAdd = useCallback(() => { + handleOpenModal() + onClose() + }, [handleOpenModal, onClose]) + + const showCreditsAlert = state.isCreditsExhausted && state.supportsCredits + const hasApiKeyFallback = state.variant === 'api-fallback' + || (state.variant === 'api-active' && state.priority === 'apiKey') + + return ( + <> +
+ {state.showPrioritySwitcher && ( + + )} + {showCreditsAlert && ( + + )} + +
+ { + if (!open) + closeConfirmDelete() + }} + > + +
+ + {t('modelProvider.confirmDelete', { ns: 'common' })} + + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.delete', { ns: 'common' })} + + +
+
+ + ) +} + +export default memo(DropdownContent) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/index.tsx new file mode 100644 index 0000000000..da0fe3909f --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/index.tsx @@ -0,0 +1,78 @@ +import type { ModelProvider, PreferredProviderTypeEnum } from '../../declarations' +import type { CardVariant, CredentialPanelState } from '../use-credential-panel-state' +import { memo, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/app/components/base/ui/popover' +import DropdownContent from './dropdown-content' + +type ModelAuthDropdownProps = { + provider: ModelProvider + state: CredentialPanelState + onChangePriority: (key: PreferredProviderTypeEnum) => void +} + +const ACCENT_VARIANTS = new Set([ + 'api-required-add', + 'api-required-configure', +]) + +function getButtonConfig(variant: CardVariant, hasCredentials: boolean, t: (key: string, opts?: Record) => string) { + if (ACCENT_VARIANTS.has(variant)) { + return { + text: variant === 'api-required-add' + ? t('modelProvider.auth.addApiKey', { ns: 'common' }) + : t('operation.config', { ns: 'common' }), + variant: 'secondary-accent' as const, + } + } + + const text = hasCredentials + ? t('operation.config', { ns: 'common' }) + : t('modelProvider.auth.addApiKey', { ns: 'common' }) + + return { text, variant: 'secondary' as const } +} + +function ModelAuthDropdown({ provider, state, onChangePriority }: ModelAuthDropdownProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const handleClose = useCallback(() => setOpen(false), []) + + const buttonConfig = getButtonConfig(state.variant, state.hasCredentials, t) + + return ( + + + + + {buttonConfig.text} + + + )} + /> + + + + + ) +} + +export default memo(ModelAuthDropdown) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/usage-priority-section.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/usage-priority-section.tsx new file mode 100644 index 0000000000..a415db8492 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/usage-priority-section.tsx @@ -0,0 +1,48 @@ +import type { UsagePriority } from '../use-credential-panel-state' +import { useTranslation } from 'react-i18next' +import { cn } from '@/utils/classnames' +import { PreferredProviderTypeEnum } from '../../declarations' + +type UsagePrioritySectionProps = { + value: UsagePriority + onSelect: (key: PreferredProviderTypeEnum) => void +} + +const options = [ + { key: PreferredProviderTypeEnum.system, labelKey: 'modelProvider.card.aiCreditsOption' }, + { key: PreferredProviderTypeEnum.custom, labelKey: 'modelProvider.card.apiKeyOption' }, +] as const + +export default function UsagePrioritySection({ value, onSelect }: UsagePrioritySectionProps) { + const { t } = useTranslation() + const selectedKey = value === 'credits' + ? PreferredProviderTypeEnum.system + : PreferredProviderTypeEnum.custom + + return ( +
+
+ + {t('modelProvider.card.usagePriority', { ns: 'common' })} + +
+
+ {options.map(option => ( + + ))} +
+
+ ) +} 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 index 3bdc005ec9..9a11f38116 100644 --- 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 @@ -40,12 +40,12 @@ const SystemQuotaCard = ({ ) } -const Label = ({ children }: { children: ReactNode }) => { +const Label = ({ children, className }: { children: ReactNode, className?: string }) => { const variant = useContext(VariantContext) return (
{children} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts new file mode 100644 index 0000000000..334d666ed0 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts @@ -0,0 +1,100 @@ +import type { ModelProvider } from '../declarations' +import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks' +import { IS_CLOUD_EDITION } from '@/config' +import { + CustomConfigurationStatusEnum, + PreferredProviderTypeEnum, +} from '../declarations' +import { useTrialCredits } from './use-trial-credits' + +export type UsagePriority = 'credits' | 'apiKey' | 'apiKeyOnly' + +export type CardVariant + = | 'credits-active' + | 'credits-exhausted' + | 'no-usage' + | 'api-fallback' + | 'api-active' + | 'api-required-add' + | 'api-required-configure' + | 'api-unavailable' + +export type CredentialPanelState = { + variant: CardVariant + priority: UsagePriority + supportsCredits: boolean + showPrioritySwitcher: boolean + hasCredentials: boolean + isCreditsExhausted: boolean + credentialName: string | undefined + credits: number +} + +const DESTRUCTIVE_VARIANTS = new Set([ + 'credits-exhausted', + 'no-usage', + 'api-unavailable', +]) + +export const isDestructiveVariant = (variant: CardVariant) => + DESTRUCTIVE_VARIANTS.has(variant) + +function deriveVariant( + priority: UsagePriority, + isExhausted: boolean, + hasCredential: boolean, + authorized: boolean | undefined, +): CardVariant { + if (priority === 'credits') { + if (!isExhausted) + return 'credits-active' + if (hasCredential && authorized) + return 'api-fallback' + if (hasCredential && !authorized) + return 'no-usage' + return 'credits-exhausted' + } + + if (hasCredential && authorized) + return 'api-active' + if (hasCredential && !authorized) + return 'api-unavailable' + return 'api-required-add' +} + +export function useCredentialPanelState(provider: ModelProvider): CredentialPanelState { + const { isExhausted, credits } = useTrialCredits() + const { + hasCredential, + authorized, + current_credential_name, + } = useCredentialStatus(provider) + + const systemConfig = provider.system_configuration + const customConfig = provider.custom_configuration + const preferredType = provider.preferred_provider_type + + const supportsCredits = systemConfig.enabled && IS_CLOUD_EDITION + const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active + + const priority: UsagePriority = !supportsCredits + ? 'apiKeyOnly' + : preferredType === PreferredProviderTypeEnum.system + ? 'credits' + : 'apiKey' + + const showPrioritySwitcher = supportsCredits && isCustomConfigured + + const variant = deriveVariant(priority, isExhausted, hasCredential, !!authorized) + + return { + variant, + priority, + supportsCredits, + showPrioritySwitcher, + hasCredentials: hasCredential, + isCreditsExhausted: isExhausted, + credentialName: current_credential_name, + credits, + } +} diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index da264df2e5..fe2bf66c1a 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -4857,11 +4857,6 @@ "count": 2 } }, - "app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx": { "no-restricted-imports": { "count": 1 @@ -6344,11 +6339,6 @@ "count": 2 } }, - "app/components/workflow/__tests__/trigger-status-sync.test.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "app/components/workflow/block-selector/all-start-blocks.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -8360,11 +8350,6 @@ "count": 5 } }, - "app/components/workflow/nodes/tool/__tests__/output-schema-utils.test.ts": { - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/workflow/nodes/tool/components/copy-id.tsx": { "no-restricted-imports": { "count": 1 @@ -8489,11 +8474,6 @@ "count": 1 } }, - "app/components/workflow/nodes/trigger-plugin/utils/__tests__/form-helpers.test.ts": { - "ts/no-explicit-any": { - "count": 2 - } - }, "app/components/workflow/nodes/trigger-plugin/utils/form-helpers.ts": { "ts/no-explicit-any": { "count": 7 diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 21e0aa9260..250c72c9b6 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -354,6 +354,20 @@ "modelProvider.card.removeKey": "Remove API Key", "modelProvider.card.tip": "AI Credits supports models from {{modelNames}}. Priority will be given to the paid quota. The Trial quota will be used after the paid quota is exhausted.", "modelProvider.card.tokens": "Tokens", + "modelProvider.card.noAvailableUsage": "No available usage", + "modelProvider.card.apiKeyRequired": "API key required", + "modelProvider.card.unavailable": "Unavailable", + "modelProvider.card.usagePriority": "Usage Priority", + "modelProvider.card.aiCreditsOption": "AI credits", + "modelProvider.card.apiKeyOption": "API Key", + "modelProvider.card.creditsExhaustedMessage": "AI credits have been exhausted", + "modelProvider.card.creditsExhaustedDescription": "Please {{upgradeLink}} or configure an API key", + "modelProvider.card.creditsExhaustedFallback": "AI credits exhausted, now using API key", + "modelProvider.card.creditsExhaustedFallbackDescription": "{{upgradeLink}} to resume AI credit priority.", + "modelProvider.card.upgradePlan": "upgrade your plan", + "modelProvider.card.noApiKeysTitle": "No API keys configured yet", + "modelProvider.card.noApiKeysDescription": "Add an API key to start using your own model credentials.", + "modelProvider.card.usageLabel": "Usage", "modelProvider.collapse": "Collapse", "modelProvider.config": "Config", "modelProvider.configLoadBalancing": "Config Load Balancing", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index adf54c1529..2f0ce96872 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -354,6 +354,20 @@ "modelProvider.card.removeKey": "删除 API 密钥", "modelProvider.card.tip": "AI Credits 支持使用 {{modelNames}} 的模型;试用额度会在付费额度用尽后才会消耗。", "modelProvider.card.tokens": "Tokens", + "modelProvider.card.noAvailableUsage": "无可用额度", + "modelProvider.card.apiKeyRequired": "需要配置 API Key", + "modelProvider.card.unavailable": "不可用", + "modelProvider.card.usagePriority": "使用优先级", + "modelProvider.card.aiCreditsOption": "AI 额度", + "modelProvider.card.apiKeyOption": "API Key", + "modelProvider.card.creditsExhaustedMessage": "AI 额度已用尽", + "modelProvider.card.creditsExhaustedDescription": "请{{upgradeLink}}或配置 API Key", + "modelProvider.card.creditsExhaustedFallback": "AI 额度已用尽,正在使用 API Key", + "modelProvider.card.creditsExhaustedFallbackDescription": "{{upgradeLink}}以恢复 AI 额度优先使用。", + "modelProvider.card.upgradePlan": "升级套餐", + "modelProvider.card.noApiKeysTitle": "尚未配置 API Key", + "modelProvider.card.noApiKeysDescription": "添加 API Key 以使用自有模型凭证。", + "modelProvider.card.usageLabel": "用量", "modelProvider.collapse": "收起", "modelProvider.config": "配置", "modelProvider.configLoadBalancing": "设置负载均衡",