Refactor Gemini model display: filter old versions, group low/high pкo, and improve naming (#114)

* Refactor Gemini model display: filter old versions, group low/high pro, and improve naming

* Fix potential negative minutes and refine Gemini 3 Pro grouping logic based on PR feedback

* The account change process has been sped up

* Address PR review: enhance error message and parameterize polling interval in process handler

* Address PR review: fix potential race condition in database backup logic

* fix: prevent renderer white screen and harden sentry gating

* feat:  localize model labels

* chore: enable hoisted node-linker

* chore: ignore .agent/.claude/.codex in prettier

---------

Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
AngeloooooandDraculabo authored and GitHub committed 2026-02-19 19:37:30 +08:00
1 parent b9497648c6
commit 8ed4593f73
162 files changed
+2981 -2541

No files matched your search

+132 -80
View File
@@ -26,6 +26,10 @@ import { useAppConfig } from '@/hooks/useAppConfig';
import { useProviderGrouping } from '@/hooks/useProviderGrouping';
import { ProviderGroup } from '@/components/ProviderGroup';
interface ModelQuotaInfo {
percentage: number;
resetTime: string;
}
interface CloudAccountCardProps {
account: CloudAccount;
onRefresh: (id: string) => void;
@@ -62,22 +66,14 @@ export function CloudAccountCard({
// Helpers to get quota color
const getQuotaColor = (percentage: number) => {
if (percentage > 80) {
return 'text-green-500';
}
if (percentage > 20) {
return 'text-yellow-500';
}
if (percentage > 80) return 'text-green-500';
if (percentage > 20) return 'text-yellow-500';
return 'text-red-500';
};
const getQuotaBarColor = (percentage: number) => {
if (percentage > 80) {
return 'bg-emerald-500';
}
if (percentage > 20) {
return 'bg-amber-500';
}
if (percentage > 80) return 'bg-emerald-500';
if (percentage > 20) return 'bg-amber-500';
return 'bg-rose-500';
};
@@ -90,63 +86,144 @@ export function CloudAccountCard({
const formatTimeRemaining = (dateStr: string) => {
const targetDate = new Date(dateStr);
if (Number.isNaN(targetDate.getTime())) {
return null;
}
if (Number.isNaN(targetDate.getTime())) return null;
const now = new Date();
if (isBefore(targetDate, now)) {
return '0h 0m';
}
if (isBefore(targetDate, now)) return '0h 0m';
// Calculate difference in hours and minutes
const diffHrs = differenceInHours(targetDate, now);
const diffMins = differenceInMinutes(targetDate, now) - diffHrs * 60;
const diffHrs = Math.max(0, differenceInHours(targetDate, now));
const diffMins = Math.max(0, differenceInMinutes(targetDate, now) - diffHrs * 60);
if (diffHrs >= 24) {
const diffDays = Math.floor(diffHrs / 24);
const remainingHrs = diffHrs % 24;
return `${diffDays}d ${remainingHrs}h`;
}
return `${diffHrs}h ${diffMins}m`;
};
const getResetTimeLabel = (resetTime?: string) => {
if (!resetTime) {
return t('cloud.card.resetUnknown');
}
if (!resetTime) return t('cloud.card.resetUnknown');
const remaining = formatTimeRemaining(resetTime);
if (!remaining) {
return t('cloud.card.resetUnknown');
}
if (!remaining) return t('cloud.card.resetUnknown');
return `${t('cloud.card.resetPrefix')}: ${remaining}`;
};
const getResetTimeTitle = (resetTime?: string) => {
if (!resetTime) {
return undefined;
}
if (!resetTime) return undefined;
const resetDate = new Date(resetTime);
if (Number.isNaN(resetDate.getTime())) {
return undefined;
}
if (Number.isNaN(resetDate.getTime())) return undefined;
return `${t('cloud.card.resetTime')}: ${resetDate.toLocaleString()}`;
};
const modelQuotas = Object.entries(account.quota?.models || {}).filter(
// --- Logic for model groups ---
const rawModels = Object.entries(account.quota?.models || {}).filter(
([modelName]) => config?.model_visibility?.[modelName] !== false,
);
// Group Gemini 3 Pro Low/High if both exist
const processedModels: Record<string, ModelQuotaInfo> = {};
const hasLow = rawModels.some(([name]) => name.includes('gemini-3-pro-low'));
const hasHigh = rawModels.some(([name]) => name.includes('gemini-3-pro-high'));
for (const [name, info] of rawModels) {
if (name.includes('gemini-3-pro-low') && hasHigh) continue;
if (name.includes('gemini-3-pro-high') && hasLow) {
// Use the lower percentage if both exist, to be safe
const lowInfo = rawModels.find(([n]) => n.includes('gemini-3-pro-low'))?.[1];
const mergedPercentage = lowInfo
? Math.min(info.percentage, lowInfo.percentage)
: info.percentage;
processedModels['gemini-3-pro-low/high'] = { ...info, percentage: mergedPercentage };
continue;
}
processedModels[name] = info;
}
const geminiModels = Object.entries(processedModels)
.filter(([name]) => name.includes('gemini') && !/gemini-[12](\.|$|-)/i.test(name))
.sort((a, b) => b[1].percentage - a[1].percentage);
const claudeModels = Object.entries(processedModels)
.filter(([name]) => name.includes('claude'))
.sort((a, b) => b[1].percentage - a[1].percentage);
const hasHighTier = geminiModels.some(
([name, info]) => name.includes('gemini-3-pro') && info.percentage > 50,
);
const hasRenderableModels = geminiModels.length > 0 || claudeModels.length > 0;
const formatModelName = (name: string) => {
return name
.replace('models/', '')
.replace('gemini-3-pro-low/high', 'Gemini 3 Pro (Low/High)')
.replace('gemini-3-pro-preview', 'Gemini 3 Pro Preview')
.replace('gemini-3-pro-image', 'Gemini 3 Pro Image')
.replace('gemini-3-pro', 'Gemini 3 Pro')
.replace('gemini-3-flash', 'Gemini 3 Flash')
.replace('claude-sonnet-4-5-thinking', 'Claude 4.5 Sonnet (Thinking)')
.replace('claude-sonnet-4-5', 'Claude 4.5 Sonnet')
.replace('claude-opus-4-6-thinking', 'Claude 4.6 Opus (Thinking)')
.replace('claude-opus-4-5-thinking', 'Claude 4.5 Opus (Thinking)')
.replace('claude-3-5-sonnet', 'Claude 3.5 Sonnet')
.replace(/-/g, ' ')
.split(' ')
.map((word) => (word.length > 2 ? word.charAt(0).toUpperCase() + word.slice(1) : word))
.join(' ');
};
const renderModelGroup = (title: string, models: [string, ModelQuotaInfo][]) => {
if (models.length === 0) return null;
return (
<div key={title} className="space-y-1">
<div className="flex items-center gap-1.5 px-2 py-1">
<span className="text-muted-foreground/70 text-[10px] font-bold tracking-wider uppercase">
{title}
</span>
<div className="bg-border/50 h-[1px] flex-1" />
</div>
{models.map(([modelName, info]) => (
<div
key={modelName}
className="group/item hover:bg-muted/60 hover:border-border/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-1.5 text-sm transition-all"
>
<span
className="text-muted-foreground group-hover/item:text-foreground min-w-0 truncate font-semibold"
title={modelName}
>
{formatModelName(modelName)}
</span>
<div className="flex flex-col items-end gap-0.5">
<span
className="text-muted-foreground text-[9px] leading-none opacity-80"
title={getResetTimeTitle(info.resetTime)}
>
{getResetTimeLabel(info.resetTime)}
</span>
<div className="flex items-baseline gap-1">
<span
className={`font-mono text-xs leading-none font-bold ${getQuotaColor(info.percentage)}`}
>
{info.percentage}%
</span>
<div className="bg-muted h-1 w-16 overflow-hidden rounded-full">
<div
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColor(info.percentage)}`}
style={{ width: `${Math.max(0, Math.min(100, info.percentage))}%` }}
/>
</div>
</div>
</div>
</div>
))}
</div>
);
};
return (
<Card
className={`group bg-card hover:border-primary/40 flex h-full flex-col overflow-hidden border transition-all duration-200 hover:shadow-sm ${isSelected ? 'ring-primary border-primary/50 ring-2' : ''}`}
>
<CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2">
{/* Selection Checkbox - Visible on hover or selected */}
{onToggleSelection && (
<div
className={`absolute top-2 left-2 z-10 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} bg-background/90 rounded-full p-2 transition-opacity`}
@@ -233,6 +310,15 @@ export function CloudAccountCard({
)}
</div>
{hasHighTier && (
<Badge
variant="secondary"
className="animate-pulse border-blue-500/20 bg-blue-500/10 text-[10px] text-blue-500"
>
{t('cloud.card.gemini3Ready')}
</Badge>
)}
{account.is_active ? (
<Button variant="ghost" size="sm" disabled className="text-green-600 opacity-100">
<Power className="mr-1 h-3 w-3" />
@@ -258,7 +344,6 @@ export function CloudAccountCard({
<div className="space-y-2">
{providerGroupingsEnabled ? (
// Grouped view with provider sections
(() => {
const accountStats = getAccountStats(account);
if (accountStats.visibleModels === 0) {
@@ -271,7 +356,6 @@ export function CloudAccountCard({
}
return (
<>
{/* Account-level overall stats */}
<div className="bg-muted/40 flex items-center justify-between rounded-lg px-3 py-1.5 text-xs">
<span className="font-medium">{t('settings.providerGroupings.overall')}</span>
<div className="flex items-center gap-2">
@@ -290,7 +374,6 @@ export function CloudAccountCard({
</div>
</div>
</div>
{/* Provider groups */}
{accountStats.providers.map((providerStats) => (
<ProviderGroup
key={providerStats.providerKey}
@@ -310,43 +393,12 @@ export function CloudAccountCard({
</>
);
})()
) : modelQuotas.length > 0 ? (
modelQuotas.map(([modelName, info]) => (
<div
key={modelName}
className="hover:bg-muted/60 hover:border-border/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-2 text-sm transition-colors"
>
<span className="text-muted-foreground min-w-0 truncate" title={modelName}>
{modelName.replace('models/', '')}
</span>
<div className="flex flex-col items-end gap-1">
<span
className="text-muted-foreground text-[10px] leading-none"
title={getResetTimeTitle(info.resetTime)}
>
{getResetTimeLabel(info.resetTime)}
</span>
<div className="flex items-baseline gap-1.5">
<span
className={`font-mono leading-none font-bold ${getQuotaColor(info.percentage)}`}
>
{getQuotaLabel(info.percentage)}
</span>
{info.percentage > 0 && (
<span className="text-muted-foreground text-[10px]">
{t('cloud.card.left')}
</span>
)}
</div>
<div className="bg-muted h-1.5 w-24 overflow-hidden rounded-full">
<div
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColor(info.percentage)}`}
style={{ width: `${Math.max(0, Math.min(100, info.percentage))}%` }}
/>
</div>
</div>
</div>
))
) : hasRenderableModels ? (
<div className="space-y-3">
{renderModelGroup(t('cloud.card.groupGoogleGemini'), geminiModels)}
<div className="pt-1" />
{renderModelGroup(t('cloud.card.groupAnthropicClaude'), claudeModels)}
</div>
) : (
<div className="text-muted-foreground flex flex-col items-center justify-center py-4">
<Box className="mb-2 h-8 w-8 opacity-20" />