Compare commits

...
Author SHA1 Message Date
jialin 7a325bebb9 feat(dashboard): split top user token usage into prompt/completion 2026-06-30 14:12:07 +08:00
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); 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, data: UsageBreakdownResponse | null | undefined,
groupBy: UsageGroupBy, groupBy: UsageGroupBy,
seriesName: string, seriesDefs: UsageTokenSeriesDef[]
color: string
) => { ) => {
const items = aggregateUsageByGroup(data, groupBy, 'total_tokens'); const itemMap = new Map<string, { total: number; values: number[] }>();
const names = items.map((item) => item.name); 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 { return {
names, names,
series: [ series: seriesDefs.map((def, index) => ({
{ name: def.name,
name: seriesName, color: def.color,
color, data: ranked.map(([name, entry]) => ({
data: items.map((item) => ({ name,
name: item.name, value: entry.values[index],
value: item.value, itemStyle: {
itemStyle: { borderRadius: getBorderRadius(index, seriesDefs.length)
borderRadius: [2, 2, 2, 2] }
} }))
})) }))
}
]
}; };
}; };
@@ -1,20 +1,17 @@
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data'; import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
import { useIntl } from '@umijs/max';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { import {
baseColorMap, baseColorMap,
DashboardUsageCommonParams, DashboardUsageCommonParams,
toUsageRankData toUsageTokenBreakdownData
} from '../config'; } from '../config';
export default function useTopTokenUsageByUser( export default function useTopTokenUsageByUser(
commonParams: DashboardUsageCommonParams commonParams: DashboardUsageCommonParams
) { ) {
const intl = useIntl();
const query = useQueryTimeSeriesData({ const query = useQueryTimeSeriesData({
key: 'topTokenUsageByUserData' key: 'topTokenUsageByUserData'
}); });
const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' });
useEffect(() => { useEffect(() => {
query query
@@ -31,13 +28,19 @@ export default function useTopTokenUsageByUser(
const rankData = useMemo( const rankData = useMemo(
() => () =>
toUsageRankData( toUsageTokenBreakdownData(query.detailData, 'user', [
query.detailData, {
'user', name: 'Prompt Tokens',
tokenUsageText, key: 'input_tokens',
baseColorMap.base color: baseColorMap.base
), },
[query.detailData, tokenUsageText] {
name: 'Completion Tokens',
key: 'output_tokens',
color: baseColorMap.baseR3
}
]),
[query.detailData]
); );
return { return {