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

+86
View File
@@ -0,0 +1,86 @@
import { useState, useMemo, useCallback } from 'react';
import { useAppConfig } from '@/hooks/useAppConfig';
import { CloudAccount } from '@/types/cloudAccount';
import { groupModelsByProvider, type AccountStats } from '@/utils/provider-grouping';
export interface UseProviderGroupingResult {
enabled: boolean;
getAccountStats: (account: CloudAccount) => AccountStats;
isProviderCollapsed: (accountId: string, providerKey: string) => boolean;
toggleProviderCollapse: (accountId: string, providerKey: string) => void;
isAccountCollapsed: (accountId: string) => boolean;
toggleAccountCollapse: (accountId: string) => void;
}
export function useProviderGrouping(): UseProviderGroupingResult {
const { config } = useAppConfig();
// Collapse state is managed locally per session for responsive toggling.
// Provider groups default to expanded on each page load.
const [collapsedProviders, setCollapsedProviders] = useState<Record<string, string[]>>({});
const [collapsedAccounts, setCollapsedAccounts] = useState<string[]>([]);
const enabled = config?.provider_groupings_enabled ?? false;
const visibilitySettings = config?.model_visibility ?? {};
const getAccountStats = useCallback(
(account: CloudAccount): AccountStats => {
const models = account.quota?.models ?? {};
return groupModelsByProvider(models, visibilitySettings);
},
[visibilitySettings],
);
const isProviderCollapsed = useCallback(
(accountId: string, providerKey: string): boolean => {
return collapsedProviders[accountId]?.includes(providerKey) ?? false;
},
[collapsedProviders],
);
const toggleProviderCollapse = useCallback((accountId: string, providerKey: string): void => {
setCollapsedProviders((prev) => {
const accountCollapsed = prev[accountId] ?? [];
const isCollapsed = accountCollapsed.includes(providerKey);
return {
...prev,
[accountId]: isCollapsed
? accountCollapsed.filter((k) => k !== providerKey)
: [...accountCollapsed, providerKey],
};
});
}, []);
const isAccountCollapsed = useCallback(
(accountId: string): boolean => {
return collapsedAccounts.includes(accountId);
},
[collapsedAccounts],
);
const toggleAccountCollapse = useCallback((accountId: string): void => {
setCollapsedAccounts((prev) =>
prev.includes(accountId) ? prev.filter((id) => id !== accountId) : [...prev, accountId],
);
}, []);
return useMemo(
() => ({
enabled,
getAccountStats,
isProviderCollapsed,
toggleProviderCollapse,
isAccountCollapsed,
toggleAccountCollapse,
}),
[
enabled,
getAccountStats,
isProviderCollapsed,
toggleProviderCollapse,
isAccountCollapsed,
toggleAccountCollapse,
],
);
}