mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
refactor(dashboard): simplify stats and add available models card
This commit is contained in:
@@ -109,7 +109,9 @@
|
|||||||
"models_used": "Models Used",
|
"models_used": "Models Used",
|
||||||
"no_usage_data": "No usage data available",
|
"no_usage_data": "No usage data available",
|
||||||
"view_detailed_usage": "View Detailed Stats",
|
"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": {
|
"basic_settings": {
|
||||||
"title": "Basic Settings",
|
"title": "Basic Settings",
|
||||||
|
|||||||
@@ -109,7 +109,9 @@
|
|||||||
"models_used": "使用模型数",
|
"models_used": "使用模型数",
|
||||||
"no_usage_data": "暂无使用数据",
|
"no_usage_data": "暂无使用数据",
|
||||||
"view_detailed_usage": "查看详细统计",
|
"view_detailed_usage": "查看详细统计",
|
||||||
"edit_settings": "编辑设置"
|
"edit_settings": "编辑设置",
|
||||||
|
"available_models": "可用模型",
|
||||||
|
"available_models_desc": "所有提供商的模型总数"
|
||||||
},
|
},
|
||||||
"basic_settings": {
|
"basic_settings": {
|
||||||
"title": "基础设置",
|
"title": "基础设置",
|
||||||
|
|||||||
@@ -113,8 +113,16 @@
|
|||||||
|
|
||||||
.statsGrid {
|
.statsGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: $spacing-md;
|
gap: $spacing-md;
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.statCard {
|
.statCard {
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import {
|
import {
|
||||||
IconKey,
|
IconKey,
|
||||||
IconBot,
|
IconBot,
|
||||||
IconFileText,
|
IconFileText,
|
||||||
IconChartLine,
|
IconSatellite
|
||||||
IconSettings,
|
|
||||||
IconShield,
|
|
||||||
IconScrollText,
|
|
||||||
IconInfo
|
|
||||||
} from '@/components/ui/icons';
|
} from '@/components/ui/icons';
|
||||||
import { useAuthStore, useConfigStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useModelsStore } from '@/stores';
|
||||||
import { apiKeysApi, providersApi, authFilesApi, usageApi } from '@/services/api';
|
import { apiKeysApi, providersApi, authFilesApi } from '@/services/api';
|
||||||
import { collectUsageDetails, extractTotalTokens, calculateRecentPerMinuteRates, formatCompactNumber } from '@/utils/usage';
|
|
||||||
import styles from './DashboardPage.module.scss';
|
import styles from './DashboardPage.module.scss';
|
||||||
|
|
||||||
interface QuickStat {
|
interface QuickStat {
|
||||||
@@ -33,14 +27,6 @@ interface ProviderStats {
|
|||||||
openai: number | null;
|
openai: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UsageStats {
|
|
||||||
totalRequests: number;
|
|
||||||
totalTokens: number;
|
|
||||||
rpm: number;
|
|
||||||
tpm: number;
|
|
||||||
modelsUsed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
@@ -49,6 +35,10 @@ export function DashboardPage() {
|
|||||||
const apiBase = useAuthStore((state) => state.apiBase);
|
const apiBase = useAuthStore((state) => state.apiBase);
|
||||||
const config = useConfigStore((state) => state.config);
|
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<{
|
const [stats, setStats] = useState<{
|
||||||
apiKeys: number | null;
|
apiKeys: number | null;
|
||||||
authFiles: number | null;
|
authFiles: number | null;
|
||||||
@@ -64,9 +54,62 @@ export function DashboardPage() {
|
|||||||
openai: null
|
openai: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const [usageStats, setUsageStats] = useState<UsageStats | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchStats = async () => {
|
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') {
|
if (connectionStatus === 'connected') {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
fetchUsage();
|
fetchModels();
|
||||||
}
|
}
|
||||||
}, [connectionStatus, config?.usageStatisticsEnabled]);
|
}, [connectionStatus, fetchModels]);
|
||||||
|
|
||||||
// Calculate total provider keys
|
// Calculate total provider keys
|
||||||
const totalProviderKeys =
|
const totalProviderKeys =
|
||||||
@@ -176,18 +182,17 @@ export function DashboardPage() {
|
|||||||
path: '/auth-files',
|
path: '/auth-files',
|
||||||
loading: loading && stats.authFiles === null,
|
loading: loading && stats.authFiles === null,
|
||||||
sublabel: t('dashboard.oauth_credentials')
|
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 (
|
return (
|
||||||
<div className={styles.dashboard}>
|
<div className={styles.dashboard}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -242,57 +247,6 @@ export function DashboardPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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 && (
|
{config && (
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h2 className={styles.sectionTitle}>{t('dashboard.current_config')}</h2>
|
<h2 className={styles.sectionTitle}>{t('dashboard.current_config')}</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user