fix(web): align UsagePrioritySection with Figma design and fix i18n key ordering

- Single-row layout for icon, label, and option cards
- Icon: arrow-up-double-line matching design spec
- Buttons: flexible width with whitespace-nowrap instead of fixed w-[72px]
- Add min-w-0 + truncate for text overflow, focus-visible ring for a11y
- Sort modelProvider.card.* i18n keys alphabetically
This commit is contained in:
yyh
2026-03-05 09:14:55 +08:00
parent 970493fa85
commit dd119eb44f
8 changed files with 99 additions and 54 deletions

View File

@ -0,0 +1,26 @@
import { useTranslation } from 'react-i18next'
type CreditsFallbackAlertProps = {
hasCredentials: boolean
}
export default function CreditsFallbackAlert({ hasCredentials }: CreditsFallbackAlertProps) {
const { t } = useTranslation()
const titleKey = hasCredentials
? 'modelProvider.card.apiKeyUnavailableFallback'
: 'modelProvider.card.noApiKeysFallback'
return (
<div className="mx-3 mb-1 mt-0.5 rounded-lg bg-background-section-burn p-3">
<div className="text-text-primary system-xs-medium">
{t(titleKey, { ns: 'common' })}
</div>
{hasCredentials && (
<div className="mt-0.5 text-text-tertiary system-2xs-regular">
{t('modelProvider.card.apiKeyUnavailableFallbackDescription', { ns: 'common' })}
</div>
)}
</div>
)
}

View File

@ -15,6 +15,7 @@ import { ConfigurationMethodEnum } from '../../declarations'
import { useAuth } from '../../model-auth/hooks'
import ApiKeySection from './api-key-section'
import CreditsExhaustedAlert from './credits-exhausted-alert'
import CreditsFallbackAlert from './credits-fallback-alert'
import UsagePrioritySection from './usage-priority-section'
type DropdownContentProps = {
@ -66,9 +67,13 @@ function DropdownContent({
onClose()
}, [handleOpenModal, onClose])
const showCreditsAlert = state.isCreditsExhausted && state.supportsCredits
const showCreditsExhaustedAlert = state.isCreditsExhausted && state.supportsCredits
const hasApiKeyFallback = state.variant === 'api-fallback'
|| (state.variant === 'api-active' && state.priority === 'apiKey')
const showCreditsFallbackAlert = state.priority === 'apiKey'
&& state.supportsCredits
&& !state.isCreditsExhausted
&& state.variant !== 'api-active'
return (
<>
@ -79,7 +84,10 @@ function DropdownContent({
onSelect={onChangePriority}
/>
)}
{showCreditsAlert && (
{showCreditsFallbackAlert && (
<CreditsFallbackAlert hasCredentials={state.hasCredentials} />
)}
{showCreditsExhaustedAlert && (
<CreditsExhaustedAlert hasApiKeyFallback={hasApiKeyFallback} />
)}
<ApiKeySection

View File

@ -1,5 +1,5 @@
import type { CredentialPanelState } from '../use-credential-panel-state'
import type { ModelProvider } from '../../declarations'
import type { CredentialPanelState } from '../use-credential-panel-state'
import { render, screen } from '@testing-library/react'
import { CustomConfigurationStatusEnum, PreferredProviderTypeEnum } from '../../declarations'
import ModelAuthDropdown from './index'

View File

@ -20,28 +20,34 @@ export default function UsagePrioritySection({ value, onSelect }: UsagePriorityS
: PreferredProviderTypeEnum.custom
return (
<div className="border-b border-b-divider-subtle px-3 pb-2 pt-2.5">
<div className="mb-1.5 flex items-center gap-1 text-text-tertiary system-xs-medium">
<span className="i-ri-arrow-up-down-line h-4 w-4 shrink-0" />
<span>{t('modelProvider.card.usagePriority', { ns: 'common' })}</span>
<span className="i-ri-question-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
</div>
<div className="flex gap-1.5">
{options.map(option => (
<button
key={option.key}
type="button"
className={cn(
'flex-1 rounded-lg px-2 py-1 text-center transition-colors system-xs-medium',
selectedKey === option.key
? 'border-[1.5px] border-components-option-card-option-selected-border bg-components-panel-bg text-text-primary shadow-xs'
: 'border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary hover:bg-components-option-card-option-bg-hover',
)}
onClick={() => onSelect(option.key)}
>
{t(option.labelKey, { ns: 'common' })}
</button>
))}
<div className="border-b border-b-divider-subtle p-1">
<div className="flex items-center gap-1 rounded-lg p-1">
<div className="shrink-0 px-0.5 py-1">
<span className="i-ri-arrow-up-double-line block h-4 w-4 text-text-tertiary" />
</div>
<div className="flex min-w-0 flex-1 items-center gap-0.5 py-0.5">
<span className="truncate text-text-secondary system-sm-medium">
{t('modelProvider.card.usagePriority', { ns: 'common' })}
</span>
<span className="i-ri-question-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
</div>
<div className="flex shrink-0 items-center gap-1">
{options.map(option => (
<button
key={option.key}
type="button"
className={cn(
'shrink-0 whitespace-nowrap rounded-md px-2 py-1 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-button-primary-border',
selectedKey === option.key
? 'border-[1.5px] border-components-option-card-option-selected-border bg-components-panel-bg text-text-primary shadow-xs system-xs-medium'
: 'border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary system-xs-regular hover:bg-components-option-card-option-bg-hover',
)}
onClick={() => onSelect(option.key)}
>
{t(option.labelKey, { ns: 'common' })}
</button>
))}
</div>
</div>
</div>
)

View File

@ -2,6 +2,7 @@ import type { ModelProvider } from '../declarations'
import { renderHook } from '@testing-library/react'
import {
ConfigurationMethodEnum,
CurrentSystemQuotaTypeEnum,
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
@ -138,7 +139,7 @@ describe('useCredentialPanelState', () => {
describe('apiKeyOnly priority (non-cloud / system disabled)', () => {
it('should return apiKeyOnly when system config disabled', () => {
const provider = createProvider({
system_configuration: { enabled: false, current_quota_type: 'trial', quota_configurations: [] },
system_configuration: { enabled: false, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
})
const { result } = renderHook(() => useCredentialPanelState(provider))

View File

@ -1,7 +1,6 @@
import type { ModelProvider } from '../declarations'
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
import {
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
import { useTrialCredits } from './use-trial-credits'
@ -43,6 +42,7 @@ function deriveVariant(
isExhausted: boolean,
hasCredential: boolean,
authorized: boolean | undefined,
credentialName: string | undefined,
): CardVariant {
if (priority === 'credits') {
if (!isExhausted)
@ -57,7 +57,7 @@ function deriveVariant(
if (hasCredential && authorized)
return 'api-active'
if (hasCredential && !authorized)
return 'api-unavailable'
return credentialName ? 'api-unavailable' : 'api-required-configure'
return 'api-required-add'
}
@ -70,11 +70,9 @@ export function useCredentialPanelState(provider: ModelProvider): CredentialPane
} = useCredentialStatus(provider)
const systemConfig = provider.system_configuration
const customConfig = provider.custom_configuration
const preferredType = provider.preferred_provider_type
const supportsCredits = systemConfig.enabled
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
const priority: UsagePriority = !supportsCredits
? 'apiKeyOnly'
@ -82,9 +80,9 @@ export function useCredentialPanelState(provider: ModelProvider): CredentialPane
? 'credits'
: 'apiKey'
const showPrioritySwitcher = supportsCredits && isCustomConfigured
const showPrioritySwitcher = supportsCredits
const variant = deriveVariant(priority, isExhausted, hasCredential, !!authorized)
const variant = deriveVariant(priority, isExhausted, hasCredential, !!authorized, current_credential_name)
return {
variant,

View File

@ -341,11 +341,24 @@
"modelProvider.buyQuota": "Buy Quota",
"modelProvider.callTimes": "Call times",
"modelProvider.card.aiCreditsInUse": "AI credits in use",
"modelProvider.card.aiCreditsOption": "AI credits",
"modelProvider.card.apiKeyOption": "API Key",
"modelProvider.card.apiKeyRequired": "API key required",
"modelProvider.card.apiKeyUnavailableFallback": "API Key unavailable, now using AI credits",
"modelProvider.card.apiKeyUnavailableFallbackDescription": "Check your API key configuration to switch back",
"modelProvider.card.buyQuota": "Buy Quota",
"modelProvider.card.callTimes": "Call times",
"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.creditsExhaustedMessage": "AI credits have been exhausted",
"modelProvider.card.modelAPI": "{{modelName}} models are using the API Key.",
"modelProvider.card.modelNotSupported": "{{modelName}} not installed",
"modelProvider.card.modelSupported": "{{modelName}} models are using these credits.",
"modelProvider.card.noApiKeysDescription": "Add an API key to start using your own model credentials.",
"modelProvider.card.noApiKeysFallback": "No API keys, using AI credits instead",
"modelProvider.card.noApiKeysTitle": "No API keys configured yet",
"modelProvider.card.noAvailableUsage": "No available usage",
"modelProvider.card.onTrial": "On Trial",
"modelProvider.card.paid": "Paid",
"modelProvider.card.priorityUse": "Priority use",
@ -354,20 +367,10 @@
"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.card.usagePriority": "Usage Priority",
"modelProvider.collapse": "Collapse",
"modelProvider.config": "Config",
"modelProvider.configLoadBalancing": "Config Load Balancing",

View File

@ -341,11 +341,24 @@
"modelProvider.buyQuota": "购买额度",
"modelProvider.callTimes": "调用次数",
"modelProvider.card.aiCreditsInUse": "AI 额度使用中",
"modelProvider.card.aiCreditsOption": "AI 额度",
"modelProvider.card.apiKeyOption": "API Key",
"modelProvider.card.apiKeyRequired": "需要配置 API Key",
"modelProvider.card.apiKeyUnavailableFallback": "API Key 不可用,正在使用 AI 额度",
"modelProvider.card.apiKeyUnavailableFallbackDescription": "检查你的 API Key 配置以切换回来",
"modelProvider.card.buyQuota": "购买额度",
"modelProvider.card.callTimes": "调用次数",
"modelProvider.card.creditsExhaustedDescription": "请{{upgradeLink}}或配置 API Key",
"modelProvider.card.creditsExhaustedFallback": "AI 额度已用尽,正在使用 API Key",
"modelProvider.card.creditsExhaustedFallbackDescription": "{{upgradeLink}}以恢复 AI 额度优先使用。",
"modelProvider.card.creditsExhaustedMessage": "AI 额度已用尽",
"modelProvider.card.modelAPI": "{{modelName}} 模型正在使用 API Key。",
"modelProvider.card.modelNotSupported": "{{modelName}} 未安装",
"modelProvider.card.modelSupported": "{{modelName}} 模型正在使用此额度。",
"modelProvider.card.noApiKeysDescription": "添加 API Key 以使用自有模型凭证。",
"modelProvider.card.noApiKeysFallback": "未配置 API Key正在使用 AI 额度",
"modelProvider.card.noApiKeysTitle": "尚未配置 API Key",
"modelProvider.card.noAvailableUsage": "无可用额度",
"modelProvider.card.onTrial": "试用中",
"modelProvider.card.paid": "已购买",
"modelProvider.card.priorityUse": "优先使用",
@ -354,20 +367,10 @@
"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.card.usagePriority": "使用优先级",
"modelProvider.collapse": "收起",
"modelProvider.config": "配置",
"modelProvider.configLoadBalancing": "设置负载均衡",