mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
185 lines
6.2 KiB
TypeScript
185 lines
6.2 KiB
TypeScript
import type { CSSProperties, ReactNode } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Line } from 'react-chartjs-2';
|
|
import { IconDiamond, IconDollarSign, IconSatellite, IconTimer, IconTrendingUp } from '@/components/ui/icons';
|
|
import {
|
|
formatTokensInMillions,
|
|
formatPerMinuteValue,
|
|
formatUsd,
|
|
calculateTokenBreakdown,
|
|
calculateRecentPerMinuteRates,
|
|
calculateTotalCost,
|
|
type ModelPrice
|
|
} from '@/utils/usage';
|
|
import { sparklineOptions } from '@/utils/usage/chartConfig';
|
|
import type { UsagePayload } from './hooks/useUsageData';
|
|
import type { SparklineBundle } from './hooks/useSparklines';
|
|
import styles from '@/pages/UsagePage.module.scss';
|
|
|
|
interface StatCardData {
|
|
key: string;
|
|
label: string;
|
|
icon: ReactNode;
|
|
accent: string;
|
|
accentSoft: string;
|
|
accentBorder: string;
|
|
value: string;
|
|
meta?: ReactNode;
|
|
trend: SparklineBundle | null;
|
|
}
|
|
|
|
export interface StatCardsProps {
|
|
usage: UsagePayload | null;
|
|
loading: boolean;
|
|
modelPrices: Record<string, ModelPrice>;
|
|
sparklines: {
|
|
requests: SparklineBundle | null;
|
|
tokens: SparklineBundle | null;
|
|
rpm: SparklineBundle | null;
|
|
tpm: SparklineBundle | null;
|
|
cost: SparklineBundle | null;
|
|
};
|
|
}
|
|
|
|
export function StatCards({ usage, loading, modelPrices, sparklines }: StatCardsProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const tokenBreakdown = usage ? calculateTokenBreakdown(usage) : { cachedTokens: 0, reasoningTokens: 0 };
|
|
const rateStats = usage
|
|
? calculateRecentPerMinuteRates(30, usage)
|
|
: { rpm: 0, tpm: 0, windowMinutes: 30, requestCount: 0, tokenCount: 0 };
|
|
const totalCost = usage ? calculateTotalCost(usage, modelPrices) : 0;
|
|
const hasPrices = Object.keys(modelPrices).length > 0;
|
|
|
|
const statsCards: StatCardData[] = [
|
|
{
|
|
key: 'requests',
|
|
label: t('usage_stats.total_requests'),
|
|
icon: <IconSatellite size={16} />,
|
|
accent: '#3b82f6',
|
|
accentSoft: 'rgba(59, 130, 246, 0.18)',
|
|
accentBorder: 'rgba(59, 130, 246, 0.35)',
|
|
value: loading ? '-' : (usage?.total_requests ?? 0).toLocaleString(),
|
|
meta: (
|
|
<>
|
|
<span className={styles.statMetaItem}>
|
|
<span className={styles.statMetaDot} style={{ backgroundColor: '#10b981' }} />
|
|
{t('usage_stats.success_requests')}: {loading ? '-' : (usage?.success_count ?? 0)}
|
|
</span>
|
|
<span className={styles.statMetaItem}>
|
|
<span className={styles.statMetaDot} style={{ backgroundColor: '#ef4444' }} />
|
|
{t('usage_stats.failed_requests')}: {loading ? '-' : (usage?.failure_count ?? 0)}
|
|
</span>
|
|
</>
|
|
),
|
|
trend: sparklines.requests
|
|
},
|
|
{
|
|
key: 'tokens',
|
|
label: t('usage_stats.total_tokens'),
|
|
icon: <IconDiamond size={16} />,
|
|
accent: '#8b5cf6',
|
|
accentSoft: 'rgba(139, 92, 246, 0.18)',
|
|
accentBorder: 'rgba(139, 92, 246, 0.35)',
|
|
value: loading ? '-' : formatTokensInMillions(usage?.total_tokens ?? 0),
|
|
meta: (
|
|
<>
|
|
<span className={styles.statMetaItem}>
|
|
{t('usage_stats.cached_tokens')}: {loading ? '-' : formatTokensInMillions(tokenBreakdown.cachedTokens)}
|
|
</span>
|
|
<span className={styles.statMetaItem}>
|
|
{t('usage_stats.reasoning_tokens')}: {loading ? '-' : formatTokensInMillions(tokenBreakdown.reasoningTokens)}
|
|
</span>
|
|
</>
|
|
),
|
|
trend: sparklines.tokens
|
|
},
|
|
{
|
|
key: 'rpm',
|
|
label: t('usage_stats.rpm_30m'),
|
|
icon: <IconTimer size={16} />,
|
|
accent: '#22c55e',
|
|
accentSoft: 'rgba(34, 197, 94, 0.18)',
|
|
accentBorder: 'rgba(34, 197, 94, 0.32)',
|
|
value: loading ? '-' : formatPerMinuteValue(rateStats.rpm),
|
|
meta: (
|
|
<span className={styles.statMetaItem}>
|
|
{t('usage_stats.total_requests')}: {loading ? '-' : rateStats.requestCount.toLocaleString()}
|
|
</span>
|
|
),
|
|
trend: sparklines.rpm
|
|
},
|
|
{
|
|
key: 'tpm',
|
|
label: t('usage_stats.tpm_30m'),
|
|
icon: <IconTrendingUp size={16} />,
|
|
accent: '#f97316',
|
|
accentSoft: 'rgba(249, 115, 22, 0.18)',
|
|
accentBorder: 'rgba(249, 115, 22, 0.32)',
|
|
value: loading ? '-' : formatPerMinuteValue(rateStats.tpm),
|
|
meta: (
|
|
<span className={styles.statMetaItem}>
|
|
{t('usage_stats.total_tokens')}: {loading ? '-' : formatTokensInMillions(rateStats.tokenCount)}
|
|
</span>
|
|
),
|
|
trend: sparklines.tpm
|
|
},
|
|
{
|
|
key: 'cost',
|
|
label: t('usage_stats.total_cost'),
|
|
icon: <IconDollarSign size={16} />,
|
|
accent: '#f59e0b',
|
|
accentSoft: 'rgba(245, 158, 11, 0.18)',
|
|
accentBorder: 'rgba(245, 158, 11, 0.32)',
|
|
value: loading ? '-' : hasPrices ? formatUsd(totalCost) : '--',
|
|
meta: (
|
|
<>
|
|
<span className={styles.statMetaItem}>
|
|
{t('usage_stats.total_tokens')}: {loading ? '-' : formatTokensInMillions(usage?.total_tokens ?? 0)}
|
|
</span>
|
|
{!hasPrices && (
|
|
<span className={`${styles.statMetaItem} ${styles.statSubtle}`}>
|
|
{t('usage_stats.cost_need_price')}
|
|
</span>
|
|
)}
|
|
</>
|
|
),
|
|
trend: hasPrices ? sparklines.cost : null
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className={styles.statsGrid}>
|
|
{statsCards.map((card) => (
|
|
<div
|
|
key={card.key}
|
|
className={styles.statCard}
|
|
style={
|
|
{
|
|
'--accent': card.accent,
|
|
'--accent-soft': card.accentSoft,
|
|
'--accent-border': card.accentBorder
|
|
} as CSSProperties
|
|
}
|
|
>
|
|
<div className={styles.statCardHeader}>
|
|
<div className={styles.statLabelGroup}>
|
|
<span className={styles.statLabel}>{card.label}</span>
|
|
</div>
|
|
<span className={styles.statIconBadge}>{card.icon}</span>
|
|
</div>
|
|
<div className={styles.statValue}>{card.value}</div>
|
|
{card.meta && <div className={styles.statMetaRow}>{card.meta}</div>}
|
|
<div className={styles.statTrend}>
|
|
{card.trend ? (
|
|
<Line className={styles.sparkline} data={card.trend.data} options={sparklineOptions} />
|
|
) : (
|
|
<div className={styles.statTrendPlaceholder}></div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|