From 39a003bdd451039a9980d49f4aa8ea5426671f2a Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sun, 21 Dec 2025 16:27:28 +0800 Subject: [PATCH] refactor(dashboard): simplify stats and add available models card --- src/i18n/locales/en.json | 4 +- src/i18n/locales/zh-CN.json | 4 +- src/pages/DashboardPage.module.scss | 10 +- src/pages/DashboardPage.tsx | 192 +++++++++++----------------- 4 files changed, 88 insertions(+), 122 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 12041b5..835e9dc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -109,7 +109,9 @@ "models_used": "Models Used", "no_usage_data": "No usage data available", "view_detailed_usage": "View Detailed Stats", - "edit_settings": "Edit Settings" + "edit_settings": "Edit Settings", + "available_models": "Available Models", + "available_models_desc": "Total models from all providers" }, "basic_settings": { "title": "Basic Settings", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 90647d5..50e9569 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -109,7 +109,9 @@ "models_used": "使用模型数", "no_usage_data": "暂无使用数据", "view_detailed_usage": "查看详细统计", - "edit_settings": "编辑设置" + "edit_settings": "编辑设置", + "available_models": "可用模型", + "available_models_desc": "所有提供商的模型总数" }, "basic_settings": { "title": "基础设置", diff --git a/src/pages/DashboardPage.module.scss b/src/pages/DashboardPage.module.scss index 305de0f..9a640d8 100644 --- a/src/pages/DashboardPage.module.scss +++ b/src/pages/DashboardPage.module.scss @@ -113,8 +113,16 @@ .statsGrid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-template-columns: repeat(4, 1fr); gap: $spacing-md; + + @media (max-width: 900px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 500px) { + grid-template-columns: 1fr; + } } .statCard { diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 534b624..91aaf3b 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -1,20 +1,14 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Button } from '@/components/ui/Button'; import { IconKey, IconBot, IconFileText, - IconChartLine, - IconSettings, - IconShield, - IconScrollText, - IconInfo + IconSatellite } from '@/components/ui/icons'; -import { useAuthStore, useConfigStore } from '@/stores'; -import { apiKeysApi, providersApi, authFilesApi, usageApi } from '@/services/api'; -import { collectUsageDetails, extractTotalTokens, calculateRecentPerMinuteRates, formatCompactNumber } from '@/utils/usage'; +import { useAuthStore, useConfigStore, useModelsStore } from '@/stores'; +import { apiKeysApi, providersApi, authFilesApi } from '@/services/api'; import styles from './DashboardPage.module.scss'; interface QuickStat { @@ -33,14 +27,6 @@ interface ProviderStats { openai: number | null; } -interface UsageStats { - totalRequests: number; - totalTokens: number; - rpm: number; - tpm: number; - modelsUsed: number; -} - export function DashboardPage() { const { t } = useTranslation(); const connectionStatus = useAuthStore((state) => state.connectionStatus); @@ -49,6 +35,10 @@ export function DashboardPage() { const apiBase = useAuthStore((state) => state.apiBase); const config = useConfigStore((state) => state.config); + const models = useModelsStore((state) => state.models); + const modelsLoading = useModelsStore((state) => state.loading); + const fetchModelsFromStore = useModelsStore((state) => state.fetchModels); + const [stats, setStats] = useState<{ apiKeys: number | null; authFiles: number | null; @@ -64,9 +54,62 @@ export function DashboardPage() { openai: null }); - const [usageStats, setUsageStats] = useState(null); const [loading, setLoading] = useState(true); - const [usageLoading, setUsageLoading] = useState(true); + + const apiKeysCache = useRef([]); + + const normalizeApiKeyList = (input: any): string[] => { + if (!Array.isArray(input)) return []; + const seen = new Set(); + const keys: string[] = []; + + input.forEach((item) => { + const value = typeof item === 'string' ? item : item?.['api-key'] ?? item?.apiKey ?? ''; + const trimmed = String(value || '').trim(); + if (!trimmed || seen.has(trimmed)) return; + seen.add(trimmed); + keys.push(trimmed); + }); + + return keys; + }; + + const resolveApiKeysForModels = useCallback(async () => { + if (apiKeysCache.current.length) { + return apiKeysCache.current; + } + + const configKeys = normalizeApiKeyList(config?.apiKeys); + if (configKeys.length) { + apiKeysCache.current = configKeys; + return configKeys; + } + + try { + const list = await apiKeysApi.list(); + const normalized = normalizeApiKeyList(list); + if (normalized.length) { + apiKeysCache.current = normalized; + } + return normalized; + } catch { + return []; + } + }, [config?.apiKeys]); + + const fetchModels = useCallback(async () => { + if (connectionStatus !== 'connected' || !apiBase) { + return; + } + + try { + const apiKeys = await resolveApiKeysForModels(); + const primaryKey = apiKeys[0]; + await fetchModelsFromStore(apiBase, primaryKey); + } catch { + // Ignore model fetch errors on dashboard + } + }, [connectionStatus, apiBase, resolveApiKeysForModels, fetchModelsFromStore]); useEffect(() => { const fetchStats = async () => { @@ -97,48 +140,11 @@ export function DashboardPage() { } }; - const fetchUsage = async () => { - if (!config?.usageStatisticsEnabled) { - setUsageLoading(false); - return; - } - setUsageLoading(true); - try { - const response = await usageApi.getUsage(); - const usageData = response?.usage ?? response; - - if (usageData) { - const details = collectUsageDetails(usageData); - const totalRequests = details.length; - const totalTokens = details.reduce((sum, d) => sum + extractTotalTokens(d), 0); - const rateStats = calculateRecentPerMinuteRates(30, usageData); - - // Count unique models - const modelSet = new Set(); - details.forEach(d => { - if (d.__modelName) modelSet.add(d.__modelName); - }); - - setUsageStats({ - totalRequests, - totalTokens, - rpm: rateStats.rpm, - tpm: rateStats.tpm, - modelsUsed: modelSet.size - }); - } - } catch { - // Ignore usage fetch errors - } finally { - setUsageLoading(false); - } - }; - if (connectionStatus === 'connected') { fetchStats(); - fetchUsage(); + fetchModels(); } - }, [connectionStatus, config?.usageStatisticsEnabled]); + }, [connectionStatus, fetchModels]); // Calculate total provider keys const totalProviderKeys = @@ -176,18 +182,17 @@ export function DashboardPage() { path: '/auth-files', loading: loading && stats.authFiles === null, sublabel: t('dashboard.oauth_credentials') + }, + { + label: t('dashboard.available_models'), + value: modelsLoading ? '-' : models.length, + icon: , + path: '/system', + loading: modelsLoading, + sublabel: t('dashboard.available_models_desc') } ]; - const quickActions = [ - { label: t('nav.basic_settings'), icon: , path: '/settings' }, - { label: t('nav.ai_providers'), icon: , path: '/ai-providers' }, - { label: t('nav.oauth'), icon: , path: '/oauth' }, - { label: t('nav.usage_stats'), icon: , path: '/usage' }, - ...(config?.loggingToFile ? [{ label: t('nav.logs'), icon: , path: '/logs' }] : []), - { label: t('nav.system_info'), icon: , path: '/system' } - ]; - return (
@@ -242,57 +247,6 @@ export function DashboardPage() { ))}
- {config?.usageStatisticsEnabled && ( -
-

{t('dashboard.usage_overview')}

- {usageLoading ? ( -
{t('common.loading')}
- ) : usageStats ? ( -
-
- {formatCompactNumber(usageStats.totalRequests)} - {t('dashboard.total_requests')} -
-
- {formatCompactNumber(usageStats.totalTokens)} - {t('dashboard.total_tokens')} -
-
- {usageStats.rpm.toFixed(1)} - {t('dashboard.rpm_30min')} -
-
- {formatCompactNumber(usageStats.tpm)} - {t('dashboard.tpm_30min')} -
-
- {usageStats.modelsUsed} - {t('dashboard.models_used')} -
-
- ) : ( -
{t('dashboard.no_usage_data')}
- )} - - {t('dashboard.view_detailed_usage')} → - -
- )} - -
-

{t('dashboard.quick_actions')}

-
- {quickActions.map((action) => ( - - - - ))} -
-
- {config && (

{t('dashboard.current_config')}