refactor(dashboard): simplify stats and add available models card

This commit is contained in:
Supra4E8C
2025-12-21 16:27:28 +08:00
parent b1426ccefc
commit 39a003bdd4
4 changed files with 88 additions and 122 deletions

View File

@@ -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 {

View File

@@ -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<UsageStats | null>(null);
const [loading, setLoading] = useState(true);
const [usageLoading, setUsageLoading] = useState(true);
const apiKeysCache = useRef<string[]>([]);
const normalizeApiKeyList = (input: any): string[] => {
if (!Array.isArray(input)) return [];
const seen = new Set<string>();
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<string>();
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: <IconSatellite size={24} />,
path: '/system',
loading: modelsLoading,
sublabel: t('dashboard.available_models_desc')
}
];
const quickActions = [
{ label: t('nav.basic_settings'), icon: <IconSettings size={18} />, path: '/settings' },
{ label: t('nav.ai_providers'), icon: <IconBot size={18} />, path: '/ai-providers' },
{ label: t('nav.oauth'), icon: <IconShield size={18} />, path: '/oauth' },
{ label: t('nav.usage_stats'), icon: <IconChartLine size={18} />, path: '/usage' },
...(config?.loggingToFile ? [{ label: t('nav.logs'), icon: <IconScrollText size={18} />, path: '/logs' }] : []),
{ label: t('nav.system_info'), icon: <IconInfo size={18} />, path: '/system' }
];
return (
<div className={styles.dashboard}>
<div className={styles.header}>
@@ -242,57 +247,6 @@ export function DashboardPage() {
))}
</div>
{config?.usageStatisticsEnabled && (
<div className={styles.section}>
<h2 className={styles.sectionTitle}>{t('dashboard.usage_overview')}</h2>
{usageLoading ? (
<div className={styles.usageLoading}>{t('common.loading')}</div>
) : usageStats ? (
<div className={styles.usageGrid}>
<div className={styles.usageCard}>
<span className={styles.usageValue}>{formatCompactNumber(usageStats.totalRequests)}</span>
<span className={styles.usageLabel}>{t('dashboard.total_requests')}</span>
</div>
<div className={styles.usageCard}>
<span className={styles.usageValue}>{formatCompactNumber(usageStats.totalTokens)}</span>
<span className={styles.usageLabel}>{t('dashboard.total_tokens')}</span>
</div>
<div className={styles.usageCard}>
<span className={styles.usageValue}>{usageStats.rpm.toFixed(1)}</span>
<span className={styles.usageLabel}>{t('dashboard.rpm_30min')}</span>
</div>
<div className={styles.usageCard}>
<span className={styles.usageValue}>{formatCompactNumber(usageStats.tpm)}</span>
<span className={styles.usageLabel}>{t('dashboard.tpm_30min')}</span>
</div>
<div className={styles.usageCard}>
<span className={styles.usageValue}>{usageStats.modelsUsed}</span>
<span className={styles.usageLabel}>{t('dashboard.models_used')}</span>
</div>
</div>
) : (
<div className={styles.usageEmpty}>{t('dashboard.no_usage_data')}</div>
)}
<Link to="/usage" className={styles.viewMoreLink}>
{t('dashboard.view_detailed_usage')}
</Link>
</div>
)}
<div className={styles.section}>
<h2 className={styles.sectionTitle}>{t('dashboard.quick_actions')}</h2>
<div className={styles.actionsGrid}>
{quickActions.map((action) => (
<Link key={action.path} to={action.path}>
<Button variant="secondary" className={styles.actionButton}>
{action.icon}
{action.label}
</Button>
</Link>
))}
</div>
</div>
{config && (
<div className={styles.section}>
<h2 className={styles.sectionTitle}>{t('dashboard.current_config')}</h2>