feat(dashboard): split top user token usage into prompt/completion

This commit is contained in:
jialin
2026-07-02 10:13:52 +08:00
parent 84304eadb7
commit e98651da2b
2 changed files with 80 additions and 29 deletions
+66 -18
View File
@@ -225,29 +225,77 @@ export const toUsagePieData = (
return aggregateUsageByGroup(data, groupBy, metric);
};
export const toUsageRankData = (
export type UsageTokenMetric = 'input_tokens' | 'output_tokens';
export interface UsageTokenSeriesDef {
name: string;
key: UsageTokenMetric;
color: string;
}
// Aggregates each group's input/output tokens into a stacked HBarChart shape,
// so a single bar shows prompt (input) and completion (output) tokens side by
// side. Rows sharing a group (e.g. the same user across dates) are summed, then
// ranked by combined tokens and capped at the top 10.
export const toUsageTokenBreakdownData = (
data: UsageBreakdownResponse | null | undefined,
groupBy: UsageGroupBy,
seriesName: string,
color: string
seriesDefs: UsageTokenSeriesDef[]
) => {
const items = aggregateUsageByGroup(data, groupBy, 'total_tokens');
const names = items.map((item) => item.name);
const itemMap = new Map<string, { total: number; values: number[] }>();
const items = getUsageResponseItems(data);
items.forEach((item: BreakdownItem) => {
const name = buildUsageLabel(item, groupBy);
const entry = itemMap.get(name) || {
total: 0,
values: seriesDefs.map(() => 0)
};
seriesDefs.forEach((def, index) => {
const value = Number((item as any)?.[def.key] ?? 0);
entry.values[index] += value;
entry.total += value;
});
itemMap.set(name, entry);
});
const ranked = Array.from(itemMap.entries())
.filter(([, entry]) => entry.total > 0)
.sort((a, b) => b[1].total - a[1].total)
.slice(0, 10);
const names = ranked.map(([name]) => name);
// Round only the outer ends of the stacked bar; the seam where the input and
// output segments meet stays square ([topLeft, topRight, bottomRight, bottomLeft]).
const radius = 2;
const getBorderRadius = (index: number, count: number) => {
if (count <= 1) {
return [radius, radius, radius, radius];
}
if (index === 0) {
return [radius, 0, 0, radius];
}
if (index === count - 1) {
return [0, radius, radius, 0];
}
return [0, 0, 0, 0];
};
return {
names,
series: [
{
name: seriesName,
color,
data: items.map((item) => ({
name: item.name,
value: item.value,
itemStyle: {
borderRadius: [2, 2, 2, 2]
}
}))
}
]
series: seriesDefs.map((def, index) => ({
name: def.name,
color: def.color,
data: ranked.map(([name, entry]) => ({
name,
value: entry.values[index],
itemStyle: {
borderRadius: getBorderRadius(index, seriesDefs.length)
}
}))
}))
};
};
@@ -1,20 +1,17 @@
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
import { useIntl } from '@umijs/max';
import { useEffect, useMemo } from 'react';
import {
baseColorMap,
DashboardUsageCommonParams,
toUsageRankData
toUsageTokenBreakdownData
} from '../config';
export default function useTopTokenUsageByUser(
commonParams: DashboardUsageCommonParams
) {
const intl = useIntl();
const query = useQueryTimeSeriesData({
key: 'topTokenUsageByUserData'
});
const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' });
useEffect(() => {
query
@@ -31,13 +28,19 @@ export default function useTopTokenUsageByUser(
const rankData = useMemo(
() =>
toUsageRankData(
query.detailData,
'user',
tokenUsageText,
baseColorMap.base
),
[query.detailData, tokenUsageText]
toUsageTokenBreakdownData(query.detailData, 'user', [
{
name: 'Prompt Tokens',
key: 'input_tokens',
color: baseColorMap.base
},
{
name: 'Completion Tokens',
key: 'output_tokens',
color: baseColorMap.baseR3
}
]),
[query.detailData]
);
return {