From a516e6ce72ba9fa6f7cb05104a10f957be5d104b Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 30 Jun 2026 12:39:57 +0800 Subject: [PATCH] feat(dashboard): split top user token usage into prompt/completion --- src/pages/dashboard/config/index.ts | 84 +++++++++++++++---- .../hooks/use-top-token-usage-by-user.ts | 25 +++--- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/pages/dashboard/config/index.ts b/src/pages/dashboard/config/index.ts index d74c3532..34bf7bd2 100644 --- a/src/pages/dashboard/config/index.ts +++ b/src/pages/dashboard/config/index.ts @@ -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(); + 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) + } + })) + })) }; }; diff --git a/src/pages/dashboard/hooks/use-top-token-usage-by-user.ts b/src/pages/dashboard/hooks/use-top-token-usage-by-user.ts index ab64f828..9b614da0 100644 --- a/src/pages/dashboard/hooks/use-top-token-usage-by-user.ts +++ b/src/pages/dashboard/hooks/use-top-token-usage-by-user.ts @@ -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 {