Compare commits

...
Author SHA1 Message Date
jialin 2767f883fc fix(dashboard): remove date from group_by
CI / deps (push) Has been cancelled
CI / build (push) Has been cancelled
CI / build-publish (push) Has been cancelled
2026-07-02 11:45:52 +08:00
jialin e98651da2b feat(dashboard): split top user token usage into prompt/completion 2026-07-02 10:13:52 +08:00
3 changed files with 83 additions and 32 deletions
@@ -57,7 +57,7 @@ const UsageByModel: React.FC<UsageByModelProps> = ({
.fetchData({ .fetchData({
...commonParams, ...commonParams,
metric: 'total_tokens', metric: 'total_tokens',
group_by: ['date', 'route'], group_by: ['route'],
page: 1, page: 1,
perPage: 10, perPage: 10,
sort_by: '-total_tokens' sort_by: '-total_tokens'
@@ -67,7 +67,7 @@ const UsageByModel: React.FC<UsageByModelProps> = ({
.fetchData({ .fetchData({
...commonParams, ...commonParams,
metric: 'api_requests', metric: 'api_requests',
group_by: ['date', 'route'], group_by: ['route'],
page: 1, page: 1,
perPage: 10, perPage: 10,
sort_by: '-api_requests' sort_by: '-api_requests'
+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,27 +1,24 @@
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
.fetchData({ .fetchData({
...commonParams, ...commonParams,
metric: 'total_tokens', metric: 'total_tokens',
group_by: ['date', 'user'], group_by: ['user'],
page: 1, page: 1,
perPage: 10, perPage: 10,
sort_by: '-total_tokens' sort_by: '-total_tokens'
@@ -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 {