diff --git a/app/detail/[...slug]/page.tsx b/app/detail/[...slug]/page.tsx index 35164b9..1fd821b 100644 --- a/app/detail/[...slug]/page.tsx +++ b/app/detail/[...slug]/page.tsx @@ -10,6 +10,7 @@ import { TimeRangeSelector } from "@/components/TimeRangeSelector"; import { TrendChart } from "@/components/charts/TrendChart"; import { buildQuery, formatNumber, formatTokens, formatUSD } from "@/lib/utils"; import { sortDetailBreakdown, type DetailBreakdownItem, type DetailBreakdownSortKey } from "@/lib/detail-sort"; +import { getDetailStats, type DetailStatKey } from "@/lib/detail-stats"; import { useTimeRange } from "@/lib/time-range-context"; import { useI18n } from "@/lib/i18n"; @@ -79,6 +80,16 @@ export default function DetailPage() { { key: "quota", label: t("th.cost"), align: "right" }, ]; + const statIcons: Record = { + calls: Hash, + total_tokens: Zap, + quota: DollarSign, + prompt_tokens: MessageSquare, + completion_tokens: MessageSquare, + cache_creation_tokens: DatabaseZap, + cache_read_tokens: BookOpen, + }; + return (
@@ -98,13 +109,17 @@ export default function DetailPage() {
) : data ? ( <> -
- - - - - - +
+ {getDetailStats(data).map((stat, i) => ( + + ))}
diff --git a/lib/detail-stats.test.ts b/lib/detail-stats.test.ts new file mode 100644 index 0000000..17824c8 --- /dev/null +++ b/lib/detail-stats.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { getDetailStats } from "./detail-stats"; + +describe("getDetailStats", () => { + test("includes completion tokens in detail page stat cards", () => { + const stats = getDetailStats({ + calls: 12, + total_tokens: 1000, + quota: 250000, + prompt_tokens: 400, + completion_tokens: 350, + cache_creation_tokens: 150, + cache_read_tokens: 100, + }); + + expect(stats.map((stat) => stat.key)).toEqual([ + "calls", + "total_tokens", + "quota", + "prompt_tokens", + "completion_tokens", + "cache_creation_tokens", + "cache_read_tokens", + ]); + expect(stats.find((stat) => stat.key === "completion_tokens")).toEqual({ + key: "completion_tokens", + labelKey: "th.output", + value: 350, + format: "tokens", + }); + }); +}); diff --git a/lib/detail-stats.ts b/lib/detail-stats.ts new file mode 100644 index 0000000..59a75bd --- /dev/null +++ b/lib/detail-stats.ts @@ -0,0 +1,41 @@ +import type { TranslationKey } from "@/lib/i18n"; + +export type DetailStatKey = + | "calls" + | "total_tokens" + | "quota" + | "prompt_tokens" + | "completion_tokens" + | "cache_creation_tokens" + | "cache_read_tokens"; + +export type DetailStatFormat = "number" | "tokens" | "usd"; + +export interface DetailStatsData { + calls: number; + total_tokens: number; + quota: number; + prompt_tokens: number; + completion_tokens: number; + cache_creation_tokens: number; + cache_read_tokens: number; +} + +export interface DetailStatItem { + key: DetailStatKey; + labelKey: TranslationKey; + value: number; + format?: DetailStatFormat; +} + +export function getDetailStats(data: DetailStatsData): DetailStatItem[] { + return [ + { key: "calls", labelKey: "dash.totalCalls", value: data.calls }, + { key: "total_tokens", labelKey: "th.totalToken", value: data.total_tokens, format: "tokens" }, + { key: "quota", labelKey: "th.cost", value: data.quota / 500000, format: "usd" }, + { key: "prompt_tokens", labelKey: "th.input", value: data.prompt_tokens, format: "tokens" }, + { key: "completion_tokens", labelKey: "th.output", value: data.completion_tokens, format: "tokens" }, + { key: "cache_creation_tokens", labelKey: "th.cacheCreation", value: data.cache_creation_tokens, format: "tokens" }, + { key: "cache_read_tokens", labelKey: "th.cacheRead", value: data.cache_read_tokens, format: "tokens" }, + ]; +}