fix(frontend): align Antigravity quota summaries

This commit is contained in:
ZheFox
2026-09-01 15:38:00 +08:00
parent 57abb20778
commit 6c71f87589
7 changed files with 42 additions and 41 deletions
@@ -104,7 +104,7 @@ describe('pool key display panels', () => {
meterClass: 'text-emerald-600', meterClass: 'text-emerald-600',
}, },
{ {
label: 'Claude额度', label: 'Claude & ChatGPT',
remainingPercent: 100, remainingPercent: 100,
resetText: '1h 后重置', resetText: '1h 后重置',
meterText: '100%', meterText: '100%',
@@ -119,7 +119,7 @@ describe('pool key display panels', () => {
expect(root.querySelector('[data-testid="pool-quota-rows"]')?.className).toContain('space-y-2') expect(root.querySelector('[data-testid="pool-quota-rows"]')?.className).toContain('space-y-2')
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-period-label"]')).map(node => node.textContent)).toEqual([ expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-period-label"]')).map(node => node.textContent)).toEqual([
'Gemini额度', 'Gemini额度',
'Claude额度', 'Claude & ChatGPT',
]) ])
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-meter-text"]')).map(node => node.textContent)).toEqual(['90.6%100%', '100%']) expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-meter-text"]')).map(node => node.textContent)).toEqual(['90.6%100%', '100%'])
expect(root.querySelectorAll('[data-testid="pool-quota-progress-track"]')).toHaveLength(2) expect(root.querySelectorAll('[data-testid="pool-quota-progress-track"]')).toHaveLength(2)
@@ -8,7 +8,7 @@
@update:model-value="$emit('update:open', $event)" @update:model-value="$emit('update:open', $event)"
> >
<template <template
v-if="providerId && items.length > 0" v-if="providerId && rawItems.length > 0"
#header-actions #header-actions
> >
<DropdownMenu :modal="false"> <DropdownMenu :modal="false">
@@ -32,7 +32,7 @@
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
v-for="item in items" v-for="item in rawItems"
:key="item.model" :key="item.model"
:title="item.model" :title="item.model"
@select="handleTestModel(item.model)" @select="handleTestModel(item.model)"
@@ -56,13 +56,13 @@
<div class="min-w-0 flex-1 mr-2"> <div class="min-w-0 flex-1 mr-2">
<div <div
class="text-muted-foreground truncate" class="text-muted-foreground truncate"
:title="item.model" :title="item.label"
> >
{{ item.label }} {{ item.label }}
</div> </div>
</div> </div>
<span :class="getQuotaRemainingClass(item.usedPercent)"> <span :class="getQuotaRemainingClass(item.usedPercent)">
{{ item.remainingPercent.toFixed(1) }}% {{ item.detail || `${item.remainingPercent.toFixed(1)}%` }}
</span> </span>
</div> </div>
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden"> <div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
@@ -122,6 +122,7 @@ import {
compareAntigravityQuotaItems, compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel, dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel, resolveAntigravityQuotaLabel,
summarizeAntigravityQuotaItems,
} from '@/features/providers/utils/antigravityQuota' } from '@/features/providers/utils/antigravityQuota'
const props = defineProps<{ const props = defineProps<{
@@ -143,6 +144,7 @@ interface QuotaItem {
usedPercent: number usedPercent: number
remainingPercent: number remainingPercent: number
resetSeconds: number | null resetSeconds: number | null
detail?: string
} }
const { error: showError, success: showSuccess } = useToast() const { error: showError, success: showSuccess } = useToast()
@@ -236,7 +238,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
return dedupeAntigravityQuotaItemsByLabel(items) return dedupeAntigravityQuotaItemsByLabel(items)
} }
const items = computed<QuotaItem[]>(() => { const rawItems = computed<QuotaItem[]>(() => {
const snapshotItems = buildItemsFromQuotaSnapshot(props.quotaSnapshot) const snapshotItems = buildItemsFromQuotaSnapshot(props.quotaSnapshot)
if (snapshotItems.length > 0) return snapshotItems if (snapshotItems.length > 0) return snapshotItems
@@ -288,6 +290,8 @@ const items = computed<QuotaItem[]>(() => {
return dedupeAntigravityQuotaItemsByLabel(result) return dedupeAntigravityQuotaItemsByLabel(result)
}) })
const items = computed<QuotaItem[]>(() => summarizeAntigravityQuotaItems(rawItems.value))
async function handleTestModel(modelName: string) { async function handleTestModel(modelName: string) {
if (!props.providerId || testingModel.value) return if (!props.providerId || testingModel.value) return
@@ -376,12 +376,13 @@
/> />
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<ProviderQuotaProgressRow <ProviderQuotaProgressRow
v-for="item in getAntigravityQuotaPreviewForKey(key)" v-for="item in getAntigravityQuotaSummaryForKey(key)"
:key="item.model" :key="item.model"
:label="item.label" :label="item.label"
:title="item.model" :title="item.label"
:used-percent="item.usedPercent" :used-percent="item.usedPercent"
:remaining-percent="item.remainingPercent" :remaining-percent="item.remainingPercent"
:meter-text="item.detail"
:meter-class="getQuotaRemainingClass(item.usedPercent)" :meter-class="getQuotaRemainingClass(item.usedPercent)"
:bar-class="getQuotaRemainingBarColor(item.usedPercent)" :bar-class="getQuotaRemainingBarColor(item.usedPercent)"
> >
@@ -401,14 +402,6 @@
</div> </div>
</template> </template>
</ProviderQuotaProgressRow> </ProviderQuotaProgressRow>
<button
v-if="getAntigravityQuotaHiddenCountForKey(key) > 0"
type="button"
class="col-span-2 text-left text-[10px] text-muted-foreground hover:text-foreground transition-colors"
@click="openAntigravityQuotaDialog(key)"
>
{{ legacyT('另有') }} {{ getAntigravityQuotaHiddenCountForKey(key) }} {{ legacyT('个模型,查看全部') }}
</button>
</div> </div>
</template> </template>
</div> </div>
@@ -989,6 +982,7 @@ import {
compareAntigravityQuotaItems, compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel, dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel, resolveAntigravityQuotaLabel,
summarizeAntigravityQuotaItems,
} from '@/features/providers/utils/antigravityQuota' } from '@/features/providers/utils/antigravityQuota'
import { import {
deleteEndpointKey, deleteEndpointKey,
@@ -3374,6 +3368,7 @@ interface AntigravityQuotaItem {
usedPercent: number usedPercent: number
remainingPercent: number remainingPercent: number
resetSeconds: number | null resetSeconds: number | null
detail?: string
} }
interface GeminiCliQuotaItem { interface GeminiCliQuotaItem {
@@ -3579,20 +3574,14 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
return dedupeAntigravityQuotaItemsByLabel(items) return dedupeAntigravityQuotaItemsByLabel(items)
} }
const ANTIGRAVITY_QUOTA_PREVIEW_LIMIT = 6
function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] { function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key) const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key)
if (snapshotItems.length > 0) return snapshotItems if (snapshotItems.length > 0) return snapshotItems
return getAntigravityQuotaItems(key.upstream_metadata) return getAntigravityQuotaItems(key.upstream_metadata)
} }
function getAntigravityQuotaPreviewForKey(key: EndpointAPIKey): AntigravityQuotaItem[] { function getAntigravityQuotaSummaryForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
return getAntigravityQuotaItemsForKey(key).slice(0, ANTIGRAVITY_QUOTA_PREVIEW_LIMIT) return summarizeAntigravityQuotaItems(getAntigravityQuotaItemsForKey(key))
}
function getAntigravityQuotaHiddenCountForKey(key: EndpointAPIKey): number {
return Math.max(getAntigravityQuotaItemsForKey(key).length - ANTIGRAVITY_QUOTA_PREVIEW_LIMIT, 0)
} }
function getResetCountdownText( function getResetCountdownText(
@@ -11,7 +11,7 @@
:class="meterClass" :class="meterClass"
data-testid="provider-quota-progress-meter" data-testid="provider-quota-progress-meter"
> >
{{ normalizedRemainingPercent.toFixed(1) }}% {{ meterText || `${normalizedRemainingPercent.toFixed(1)}%` }}
</span> </span>
</div> </div>
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden"> <div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
@@ -47,6 +47,7 @@ const props = withDefaults(defineProps<{
barClass?: string barClass?: string
footerClass?: string footerClass?: string
resetText?: string | null resetText?: string | null
meterText?: string | null
}>(), { }>(), {
usedPercent: null, usedPercent: null,
remainingPercent: null, remainingPercent: null,
@@ -55,6 +56,7 @@ const props = withDefaults(defineProps<{
barClass: '', barClass: '',
footerClass: '', footerClass: '',
resetText: null, resetText: null,
meterText: null,
}) })
function normalizePercent(value: number | null | undefined): number | null { function normalizePercent(value: number | null | undefined): number | null {
@@ -102,7 +102,7 @@ function mount(metadata: UpstreamMetadata) {
} }
describe('AntigravityQuotaDialog', () => { describe('AntigravityQuotaDialog', () => {
it('renders opaque quota identifiers with concise visible labels', () => { it('hides quota buckets outside the shared pool summary families', () => {
const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb' const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb'
const { root, unmount } = mount({ const { root, unmount } = mount({
antigravity: { antigravity: {
@@ -116,14 +116,15 @@ describe('AntigravityQuotaDialog', () => {
}, },
}) })
expect(root.textContent).toContain('Key-1') expect(root.textContent).toContain('暂无配额数据')
expect(root.textContent).not.toContain('Key-1')
expect(root.textContent).not.toContain(rawIdentifier) expect(root.textContent).not.toContain(rawIdentifier)
expect(root.textContent).toContain('25.0%') expect(root.textContent).not.toContain('25.0%')
unmount() unmount()
}) })
it('orders important Gemini and Claude quota rows before low-priority rows', () => { it('renders only the shared Gemini and Claude ChatGPT family summaries', () => {
const { root, unmount } = mount({ const { root, unmount } = mount({
antigravity: { antigravity: {
quota_by_model: { quota_by_model: {
@@ -164,16 +165,20 @@ describe('AntigravityQuotaDialog', () => {
expect(text).not.toContain('gemini-pro-agent') expect(text).not.toContain('gemini-pro-agent')
expect(text).not.toContain('claude-opus-4-6-thinking') expect(text).not.toContain('claude-opus-4-6-thinking')
expect(text.indexOf('Claude Opus 4.6 (Thinking)')).toBeLessThan(text.indexOf('Gemini 3.1 Pro (High)')) expect(text).toContain('Gemini额度')
expect(text.indexOf('Gemini 3.1 Pro (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (High)')) expect(text).toContain('80%95%')
expect(text.indexOf('Gemini 3.5 Flash (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (Medium)')) expect(text).toContain('Claude & ChatGPT')
expect(text.indexOf('Gemini 3.5 Flash (Medium)')).toBeLessThan(text.indexOf('Tab Flash Lite Preview')) expect(text).toContain('100%')
expect(text.indexOf('Tab Flash Lite Preview')).toBeLessThan(text.indexOf('chat_20706')) expect(text).not.toContain('Gemini 3.1 Pro (High)')
expect(text).not.toContain('Gemini 3.5 Flash (High)')
expect(text).not.toContain('Gemini 3.5 Flash (Medium)')
expect(text).not.toContain('Tab Flash Lite Preview')
expect(text).not.toContain('chat_20706')
unmount() unmount()
}) })
it('renders one row for duplicate quota labels and keeps the preferred active bucket', () => { it('collapses duplicate model buckets into one family range', () => {
const { root, unmount } = mount({ const { root, unmount } = mount({
antigravity: { antigravity: {
quota_by_model: { quota_by_model: {
@@ -197,10 +202,11 @@ describe('AntigravityQuotaDialog', () => {
}) })
const text = root.textContent || '' const text = root.textContent || ''
expect(text.match(/Gemini 3\.1 Pro \(High\)/g)).toHaveLength(1) expect(text.match(/Gemini额度/g)).toHaveLength(1)
expect(text).toContain('40.0%') expect(text).toContain('40%90%')
expect(text).not.toContain('95.0%') expect(text).not.toContain('95.0%')
expect(text.indexOf('Gemini 3.1 Pro (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (High)')) expect(text).not.toContain('Gemini 3.1 Pro (High)')
expect(text).not.toContain('Gemini 3.5 Flash (High)')
unmount() unmount()
}) })
@@ -16,7 +16,7 @@ describe('summarizeAntigravityQuotaItems', () => {
expect(items.map(item => [item.label, item.remainingPercent, item.detail])).toEqual([ expect(items.map(item => [item.label, item.remainingPercent, item.detail])).toEqual([
['Gemini额度', 90.6, '90.6%95%'], ['Gemini额度', 90.6, '90.6%95%'],
['Claude额度', 82, '82%100%'], ['Claude & ChatGPT', 82, '82%100%'],
]) ])
expect(items.map(item => item.resetSeconds)).toEqual([180, 120]) expect(items.map(item => item.resetSeconds)).toEqual([180, 120])
expect(items[1]?.model).toBe('claude-sonnet-4-6') expect(items[1]?.model).toBe('claude-sonnet-4-6')
@@ -9,7 +9,7 @@ export interface AntigravityQuotaSortableItem {
const ANTIGRAVITY_QUOTA_GROUPS = [ const ANTIGRAVITY_QUOTA_GROUPS = [
{ label: 'Gemini额度', matches: (model: string) => model.startsWith('gemini-') }, { label: 'Gemini额度', matches: (model: string) => model.startsWith('gemini-') },
{ {
label: 'Claude额度', label: 'Claude & ChatGPT',
matches: (model: string) => model.startsWith('claude-') || model.startsWith('gpt-'), matches: (model: string) => model.startsWith('claude-') || model.startsWith('gpt-'),
}, },
] as const ] as const