Files
Cli-Proxy-API-Management-Ce…/src/components/usage/TokenBreakdownChart.tsx
Supra4E8C e2368ddfd7 Refactor color variables and styles across components for a cohesive design update
- 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.
2026-02-14 02:25:58 +08:00

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>
);
}