feat: Implement provider groupings with account calculator and compre… (#113)

* feat: Implement provider groupings with account calculator and comprehensive fixes

- Add provider detection utilities (detectProvider, PROVIDER_REGISTRY) for Claude/Gemini/Others categorization
- Implement grouping logic (groupModelsByProvider, calculateProviderStats, calculateAccountStats) with health status calculation
- Create useProviderGrouping hook for collapsible state management and persistence
- Add ProviderGroup component with health indicators, progress bars, and model visibility integration
- Enhance CloudAccountList with account-level calculator showing overall percentage and earliest reset time
- Implement grid layout selector (auto/2-col/3-col/list) with config persistence and tooltips
- Add global quota aggregate bar across all accounts in top statistics
- Fix model visibility settings regression by accessing account.quota.models instead of account.quota
- Show 'Rate Limited' instead of '0%' for exhausted quotas with consistent getQuotaLabel function
- Add comprehensive i18n translations for layout selector and rate limited labels (en, zh-CN, ru)
- Disable batch operations when provider groupings are enabled to prevent conflicts
- Include unit tests for provider detection logic and grouping functionality

Addresses critical issues: model list regression, rate limited display, layout control, global quota visibility, and consistent quota labeling across all components.

* fix: resolve ESLint no-ex-assign violations in proxy service

- Fix exception parameter reassignment in catch blocks
- Rename catch parameters to avoid no-ex-assign rule violations
- Maintain same error handling logic while complying with linting rules

* fix: add missing Models tab to settings page

- Add Models tab between General and Proxy tabs
- Import and integrate ModelVisibilitySettings component
- Fix critical regression where model visibility settings were inaccessible

* fix: add missing React hook imports in settings route

* test: remove obsolete tests for removed controller APIs

---------

Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
ArtemisAIandDraculabo authored and GitHub committed 2026-02-18 16:38:15 +08:00
1 parent 118fad567e
commit 1e59a1c901
12 files changed
+1237 -97

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { ProviderStats } from '@/utils/provider-grouping';
interface ProviderGroupProps {
stats: ProviderStats;
isCollapsed: boolean;
onToggleCollapse: () => void;
getQuotaColor: (percentage: number) => string;
getQuotaBarColor: (percentage: number) => string;
getQuotaLabel: (percentage: number) => string;
getResetTimeLabel: (resetTime?: string) => string;
getResetTimeTitle: (resetTime?: string) => string | undefined;
leftLabel: string;
}
export const ProviderGroup: React.FC<ProviderGroupProps> = ({
stats,
isCollapsed,
onToggleCollapse,
getQuotaColor,
getQuotaBarColor,
getQuotaLabel,
getResetTimeLabel,
getResetTimeTitle,
leftLabel,
}) => {
const { t } = useTranslation();
const { providerInfo, visibleModels, avgPercentage, earliestReset } = stats;
if (visibleModels.length === 0) {
return null;
}
return (
<div className="border-border/60 overflow-hidden rounded-lg border">
{/* Provider Header */}
<button
type="button"
onClick={onToggleCollapse}
className="hover:bg-muted/60 flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm transition-colors"
>
{isCollapsed ? (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
) : (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
)}
{/* Provider color dot */}
<span
className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
style={{ backgroundColor: providerInfo.color }}
/>
<span className="font-medium">{providerInfo.name}</span>
<span className="text-muted-foreground text-xs">
{t('settings.providerGroupings.models', { count: visibleModels.length })}
</span>
{/* Spacer */}
<div className="flex-1" />
{/* Averaged metrics */}
<div className="flex items-center gap-2">
{earliestReset && (
<span
className="text-muted-foreground text-[10px]"
title={getResetTimeTitle(earliestReset)}
>
{getResetTimeLabel(earliestReset)}
</span>
)}
<div className="flex items-baseline gap-1">
<span className={`font-mono text-xs font-bold ${getQuotaColor(avgPercentage)}`}>
{getQuotaLabel(avgPercentage)}
</span>
{avgPercentage > 0 && (
<span className="text-muted-foreground text-[10px]">
{t('settings.providerGroupings.avgLabel')}
</span>
)}
</div>
<div className="bg-muted h-1.5 w-16 overflow-hidden rounded-full">
<div
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColor(avgPercentage)}`}
style={{ width: `${Math.max(0, Math.min(100, avgPercentage))}%` }}
/>
</div>
</div>
</button>
{/* Individual model rows (shown when expanded) */}
{!isCollapsed && (
<div className="border-border/40 border-t">
{visibleModels.map((model, index) => (
<div
key={model.id}
className={`hover:bg-muted/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 px-3 py-2 pl-9 text-sm transition-colors ${
index < visibleModels.length - 1 ? 'border-border/20 border-b' : ''
}`}
>
<span className="text-muted-foreground min-w-0 truncate" title={model.id}>
{model.id.replace('models/', '')}
</span>
<div className="flex flex-col items-end gap-1">
<span
className="text-muted-foreground text-[10px] leading-none"
title={getResetTimeTitle(model.resetTime)}
>
{getResetTimeLabel(model.resetTime)}
</span>
<div className="flex items-baseline gap-1.5">
<span
className={`font-mono leading-none font-bold ${getQuotaColor(model.percentage)}`}
>
{getQuotaLabel(model.percentage)}
</span>
{model.percentage > 0 && (
<span className="text-muted-foreground text-[10px]">{leftLabel}</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(model.percentage)}`}
style={{ width: `${Math.max(0, Math.min(100, model.percentage))}%` }}
/>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
};