mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-24 16:04:35 +08:00
Merge pull request #247 from router-for-me/revert-235-improve-token-breakdown-readability
Revert "Improve token type distribution readability"
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { ChartData, ChartOptions } from 'chart.js';
|
import { Line } from 'react-chartjs-2';
|
||||||
import { Bar } from 'react-chartjs-2';
|
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import {
|
import {
|
||||||
@@ -13,21 +12,15 @@ import { buildChartOptions, getHourChartMinWidth } from '@/utils/usage/chartConf
|
|||||||
import type { UsagePayload } from './hooks/useUsageData';
|
import type { UsagePayload } from './hooks/useUsageData';
|
||||||
import styles from '@/pages/UsagePage.module.scss';
|
import styles from '@/pages/UsagePage.module.scss';
|
||||||
|
|
||||||
const TOKEN_COLORS: Record<TokenCategory, string> = {
|
const TOKEN_COLORS: Record<TokenCategory, { border: string; bg: string }> = {
|
||||||
input: '#8CC21F',
|
input: { border: '#8b8680', bg: 'rgba(139, 134, 128, 0.25)' },
|
||||||
output: '#FA6450',
|
output: { border: '#22c55e', bg: 'rgba(34, 197, 94, 0.25)' },
|
||||||
cached: '#F5ED58',
|
cached: { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.25)' },
|
||||||
reasoning: '#00ABA5'
|
reasoning: { border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.25)' }
|
||||||
};
|
};
|
||||||
|
|
||||||
const CATEGORIES: TokenCategory[] = ['input', 'output', 'cached', 'reasoning'];
|
const CATEGORIES: TokenCategory[] = ['input', 'output', 'cached', 'reasoning'];
|
||||||
|
|
||||||
function formatTokens(num: number): string {
|
|
||||||
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
|
||||||
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
|
||||||
return num.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenBreakdownChartProps {
|
export interface TokenBreakdownChartProps {
|
||||||
usage: UsagePayload | null;
|
usage: UsagePayload | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -58,94 +51,38 @@ export function TokenBreakdownChart({
|
|||||||
reasoning: t('usage_stats.reasoning_tokens')
|
reasoning: t('usage_stats.reasoning_tokens')
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoryTotals: Record<TokenCategory, number> = {
|
const data = {
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
cached: 0,
|
|
||||||
reasoning: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
CATEGORIES.forEach((cat) => {
|
|
||||||
categoryTotals[cat] = series.dataByCategory[cat].reduce(
|
|
||||||
(sum, val) => sum + (Number(val) || 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const data: ChartData<'bar'> = {
|
|
||||||
labels: series.labels,
|
labels: series.labels,
|
||||||
datasets: CATEGORIES.map((cat) => ({
|
datasets: CATEGORIES.map((cat) => ({
|
||||||
label: categoryLabels[cat],
|
label: categoryLabels[cat],
|
||||||
categoryId: cat,
|
|
||||||
data: series.dataByCategory[cat],
|
data: series.dataByCategory[cat],
|
||||||
backgroundColor: TOKEN_COLORS[cat],
|
borderColor: TOKEN_COLORS[cat].border,
|
||||||
borderColor: isDark ? '#0f172a' : '#ffffff',
|
backgroundColor: TOKEN_COLORS[cat].bg,
|
||||||
borderWidth: 1,
|
pointBackgroundColor: TOKEN_COLORS[cat].border,
|
||||||
borderSkipped: false,
|
pointBorderColor: TOKEN_COLORS[cat].border,
|
||||||
grouped: false,
|
fill: true,
|
||||||
// Higher order is drawn earlier by Chart.js, so larger series stay behind smaller ones.
|
tension: 0.35
|
||||||
order: categoryTotals[cat],
|
|
||||||
categoryPercentage: 0.78,
|
|
||||||
barPercentage: 0.82
|
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseOptions = buildChartOptions({ period, labels: series.labels, isDark, isMobile }) as ChartOptions<'bar'>;
|
const baseOptions = buildChartOptions({ period, labels: series.labels, isDark, isMobile });
|
||||||
const options: ChartOptions<'bar'> = {
|
const options = {
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
scales: {
|
scales: {
|
||||||
...baseOptions.scales,
|
...baseOptions.scales,
|
||||||
y: {
|
y: {
|
||||||
...baseOptions.scales?.y,
|
...baseOptions.scales?.y,
|
||||||
stacked: false
|
stacked: true
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
...baseOptions.scales?.x,
|
...baseOptions.scales?.x,
|
||||||
stacked: false
|
stacked: true
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
...baseOptions.plugins,
|
|
||||||
tooltip: {
|
|
||||||
...baseOptions.plugins?.tooltip,
|
|
||||||
itemSort: (a, b) => a.datasetIndex - b.datasetIndex,
|
|
||||||
callbacks: {
|
|
||||||
...baseOptions.plugins?.tooltip?.callbacks,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
label: function (context: any) {
|
|
||||||
const val = Number(context.raw) || 0;
|
|
||||||
const cat = context.dataset.categoryId;
|
|
||||||
let text = `${context.dataset.label}: ${formatTokens(val)}`;
|
|
||||||
|
|
||||||
if (cat === 'cached') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const inputDs = context.chart.data.datasets.find((ds: any) => ds.categoryId === 'input');
|
|
||||||
const inputVal = inputDs ? (Number(inputDs.data[context.dataIndex]) || 0) : 0;
|
|
||||||
if (inputVal > 0) {
|
|
||||||
const perc = ((val / inputVal) * 100).toFixed(2);
|
|
||||||
text += ` (${perc}%)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
footer: function (tooltipItems: any[]) {
|
|
||||||
let total = 0;
|
|
||||||
const dataIndex = tooltipItems[0].dataIndex;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
tooltipItems[0].chart.data.datasets.forEach((ds: any) => {
|
|
||||||
total += Number(ds.data[dataIndex]) || 0;
|
|
||||||
});
|
|
||||||
return `Total: ${formatTokens(total)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { chartData: data, chartOptions: options };
|
return { chartData: data, chartOptions: options };
|
||||||
}, [usage, period, isDark, isMobile, hourWindowHours, t]);
|
}, [usage, period, isDark, isMobile, hourWindowHours, t]);
|
||||||
const labels = chartData.labels ?? [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -171,7 +108,7 @@ export function TokenBreakdownChart({
|
|||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={styles.hint}>{t('common.loading')}</div>
|
<div className={styles.hint}>{t('common.loading')}</div>
|
||||||
) : labels.length > 0 ? (
|
) : chartData.labels.length > 0 ? (
|
||||||
<div className={styles.chartWrapper}>
|
<div className={styles.chartWrapper}>
|
||||||
<div className={styles.chartLegend} aria-label="Chart legend">
|
<div className={styles.chartLegend} aria-label="Chart legend">
|
||||||
{chartData.datasets.map((dataset, index) => (
|
{chartData.datasets.map((dataset, index) => (
|
||||||
@@ -180,7 +117,7 @@ export function TokenBreakdownChart({
|
|||||||
className={styles.legendItem}
|
className={styles.legendItem}
|
||||||
title={dataset.label}
|
title={dataset.label}
|
||||||
>
|
>
|
||||||
<span className={styles.legendDot} style={{ backgroundColor: TOKEN_COLORS[CATEGORIES[index]] }} />
|
<span className={styles.legendDot} style={{ backgroundColor: dataset.borderColor }} />
|
||||||
<span className={styles.legendLabel}>{dataset.label}</span>
|
<span className={styles.legendLabel}>{dataset.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -191,11 +128,11 @@ export function TokenBreakdownChart({
|
|||||||
className={styles.chartCanvas}
|
className={styles.chartCanvas}
|
||||||
style={
|
style={
|
||||||
period === 'hour'
|
period === 'hour'
|
||||||
? { minWidth: getHourChartMinWidth(labels.length, isMobile) }
|
? { minWidth: getHourChartMinWidth(chartData.labels.length, isMobile) }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Bar data={chartData} options={chartOptions} />
|
<Line data={chartData} options={chartOptions} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
BarElement,
|
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
PointElement,
|
PointElement,
|
||||||
@@ -47,7 +46,6 @@ import styles from './UsagePage.module.scss';
|
|||||||
|
|
||||||
// Register Chart.js components
|
// Register Chart.js components
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
BarElement,
|
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
PointElement,
|
PointElement,
|
||||||
|
|||||||
Reference in New Issue
Block a user