mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
- Updated active state colors in ToastSelect component for better visibility. - Adjusted box-shadow and border colors in ModelMappingDiagram styles. - Changed provider colors in ModelMappingDiagram for improved aesthetics. - Modified background and border styles in ProviderNav for a more modern look. - Updated accent colors in StatCards to align with new color scheme. - Refined token colors in TokenBreakdownChart for consistency. - Adjusted sparkline colors in useSparklines hook to match new design. - Changed error icon color in AiProvidersOpenAIEditPage for better contrast. - Updated failure badge styles in AiProvidersPage for a cleaner appearance. - Refined various status styles in AuthFilesPage for improved clarity. - Updated colors in ConfigPage to use new variable definitions. - Refined error and warning styles in LoginPage for better user feedback. - Adjusted log status colors in LogsPage for consistency with new theme. - Updated OAuthPage styles to reflect new color variables. - Refined quota styles in QuotaPage for better visual hierarchy. - Updated system page styles for improved user experience. - Adjusted usage page styles to align with new design language. - Refactored component styles to use new color variables in components.scss. - Updated layout styles to reflect new primary color definitions. - Refined theme colors in themes.scss for a more cohesive look. - Updated color variables in variables.scss to reflect new design choices. - Adjusted chart colors in usage.ts for consistency with new color scheme.
146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Line } from 'react-chartjs-2';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { Button } from '@/components/ui/Button';
|
|
import {
|
|
buildHourlyTokenBreakdown,
|
|
buildDailyTokenBreakdown,
|
|
type TokenCategory
|
|
} from '@/utils/usage';
|
|
import { buildChartOptions, getHourChartMinWidth } from '@/utils/usage/chartConfig';
|
|
import type { UsagePayload } from './hooks/useUsageData';
|
|
import styles from '@/pages/UsagePage.module.scss';
|
|
|
|
const TOKEN_COLORS: Record<TokenCategory, { border: string; bg: string }> = {
|
|
input: { border: '#8b8680', bg: 'rgba(139, 134, 128, 0.25)' },
|
|
output: { border: '#22c55e', bg: 'rgba(34, 197, 94, 0.25)' },
|
|
cached: { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.25)' },
|
|
reasoning: { border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.25)' }
|
|
};
|
|
|
|
const CATEGORIES: TokenCategory[] = ['input', 'output', 'cached', 'reasoning'];
|
|
|
|
export interface TokenBreakdownChartProps {
|
|
usage: UsagePayload | null;
|
|
loading: boolean;
|
|
isDark: boolean;
|
|
isMobile: boolean;
|
|
hourWindowHours?: number;
|
|
}
|
|
|
|
export function TokenBreakdownChart({
|
|
usage,
|
|
loading,
|
|
isDark,
|
|
isMobile,
|
|
hourWindowHours
|
|
}: TokenBreakdownChartProps) {
|
|
const { t } = useTranslation();
|
|
const [period, setPeriod] = useState<'hour' | 'day'>('hour');
|
|
|
|
const { chartData, chartOptions } = useMemo(() => {
|
|
const series =
|
|
period === 'hour'
|
|
? buildHourlyTokenBreakdown(usage, hourWindowHours)
|
|
: buildDailyTokenBreakdown(usage);
|
|
const categoryLabels: Record<TokenCategory, string> = {
|
|
input: t('usage_stats.input_tokens'),
|
|
output: t('usage_stats.output_tokens'),
|
|
cached: t('usage_stats.cached_tokens'),
|
|
reasoning: t('usage_stats.reasoning_tokens')
|
|
};
|
|
|
|
const data = {
|
|
labels: series.labels,
|
|
datasets: CATEGORIES.map((cat) => ({
|
|
label: categoryLabels[cat],
|
|
data: series.dataByCategory[cat],
|
|
borderColor: TOKEN_COLORS[cat].border,
|
|
backgroundColor: TOKEN_COLORS[cat].bg,
|
|
pointBackgroundColor: TOKEN_COLORS[cat].border,
|
|
pointBorderColor: TOKEN_COLORS[cat].border,
|
|
fill: true,
|
|
tension: 0.35
|
|
}))
|
|
};
|
|
|
|
const baseOptions = buildChartOptions({ period, labels: series.labels, isDark, isMobile });
|
|
const options = {
|
|
...baseOptions,
|
|
scales: {
|
|
...baseOptions.scales,
|
|
y: {
|
|
...baseOptions.scales?.y,
|
|
stacked: true
|
|
},
|
|
x: {
|
|
...baseOptions.scales?.x,
|
|
stacked: true
|
|
}
|
|
}
|
|
};
|
|
|
|
return { chartData: data, chartOptions: options };
|
|
}, [usage, period, isDark, isMobile, hourWindowHours, t]);
|
|
|
|
return (
|
|
<Card
|
|
title={t('usage_stats.token_breakdown')}
|
|
extra={
|
|
<div className={styles.periodButtons}>
|
|
<Button
|
|
variant={period === 'hour' ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => setPeriod('hour')}
|
|
>
|
|
{t('usage_stats.by_hour')}
|
|
</Button>
|
|
<Button
|
|
variant={period === 'day' ? 'primary' : 'secondary'}
|
|
size="sm"
|
|
onClick={() => setPeriod('day')}
|
|
>
|
|
{t('usage_stats.by_day')}
|
|
</Button>
|
|
</div>
|
|
}
|
|
>
|
|
{loading ? (
|
|
<div className={styles.hint}>{t('common.loading')}</div>
|
|
) : chartData.labels.length > 0 ? (
|
|
<div className={styles.chartWrapper}>
|
|
<div className={styles.chartLegend} aria-label="Chart legend">
|
|
{chartData.datasets.map((dataset, index) => (
|
|
<div
|
|
key={`${dataset.label}-${index}`}
|
|
className={styles.legendItem}
|
|
title={dataset.label}
|
|
>
|
|
<span className={styles.legendDot} style={{ backgroundColor: dataset.borderColor }} />
|
|
<span className={styles.legendLabel}>{dataset.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className={styles.chartArea}>
|
|
<div className={styles.chartScroller}>
|
|
<div
|
|
className={styles.chartCanvas}
|
|
style={
|
|
period === 'hour'
|
|
? { minWidth: getHourChartMinWidth(chartData.labels.length, isMobile) }
|
|
: undefined
|
|
}
|
|
>
|
|
<Line data={chartData} options={chartOptions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className={styles.hint}>{t('usage_stats.no_data')}</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|