import { Icon } from "./Icon"; import { useMemo, useState } from "react"; import { useFetch } from "../hooks"; import { fmtCompact, fmtCost, fmtDate, fmtNumber, fmtPercent, getLang, useT, weekdayNames, type Key } from "../i18n"; import { navigate } from "../router"; import { useStore } from "../store"; import type { User } from "../types"; import type { Aggregate } from "../types"; import { Heatmap, Legend, SERIES, StackedBars, type Series } from "./Charts"; import { BudgetPanel } from "./Budget"; type Row = Aggregate & Record; interface StatsData { total: Aggregate; by_day: Row<"day">[]; by_day_model: { day: string; model: string; cost: number; input_tokens: number; output_tokens: number }[]; by_model: Row<"model">[]; by_provider: Row<"provider">[]; by_agent: Row<"agent">[]; by_user: Row<"user">[]; heatmap: (Aggregate & { slot: number })[]; top_sessions: (Aggregate & { session_id: string; title: string })[]; currency: string; from: number; to: number; } const RANGES: { key: string; days: number; label: Key }[] = [ { key: "7", days: 7, label: "stats.range7" }, { key: "30", days: 30, label: "stats.range30" }, { key: "90", days: 90, label: "stats.range90" }, { key: "365", days: 365, label: "stats.range365" }, { key: "all", days: 0, label: "stats.rangeAll" }, ]; type HeatMetric = "requests" | "tokens" | "cost"; function dayKey(ms: number) { const d = new Date(ms); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } /** All days between from and to (local time), so empty days show as gaps. */ function dayRange(fromMs: number, toMs: number, firstDataDay?: string): string[] { const start = new Date(firstDataDay && fromMs === 0 ? `${firstDataDay}T00:00:00` : fromMs); start.setHours(0, 0, 0, 0); const out: string[] = []; for (let d = new Date(start); d.getTime() <= toMs && out.length < 800; d.setDate(d.getDate() + 1)) out.push(dayKey(d.getTime())); return out; } function Kpi({ label, value, sub }: { label: string; value: string; sub?: string }) { return (
{label}
{value}
{sub &&
{sub}
}
); } /** Effective cost per 1M tokens (input incl. cache + output). */ function perMillion(r: Aggregate, currency: string) { const tokens = r.input_tokens + r.cache_read_tokens + r.cache_write_tokens + r.output_tokens; return tokens > 0 && r.cost > 0 ? fmtCost((r.cost / tokens) * 1_000_000, currency) : "–"; } function AggTable({ rows, keyName, label, currency, link }: { rows: (Aggregate & Record)[]; keyName: string; label: string; currency: string; link?: (r: any) => void }) { const t = useT(); return (
{rows.map((r) => ( ))}
{label} {t("stats.requests")} {t("stats.input")} {t("stats.cacheRead")} {t("stats.output")} {t("stats.ttft")} {t("stats.outputSpeed")} {t("stats.prefillSpeed")} {t("stats.cost")} {t("stats.perMillion")}
{link ? (e.preventDefault(), link(r))}>{r[keyName]} : r[keyName]} {fmtNumber(r.requests)} {fmtCompact(r.input_tokens + r.cache_write_tokens)} {fmtCompact(r.cache_read_tokens)} {fmtCompact(r.output_tokens)} {fmtNumber(r.avg_ttft_ms)} ms {fmtNumber(r.output_tps, 1)} {fmtNumber(r.prefill_tps)} {fmtCost(r.cost, currency)} {perMillion(r, currency)}
); } export function Stats({ onMenu }: { onMenu: () => void }) { const t = useT(); const [range, setRange] = useState("30"); const [heat, setHeat] = useState("requests"); const [tableView, setTableView] = useState(false); const isAdmin = useStore((s) => s.me?.role === "admin"); const users = useFetch(isAdmin ? "/users" : null).data ?? []; const [userFilter, setUserFilter] = useState(""); const days = RANGES.find((r) => r.key === range)!.days; const tz = -new Date().getTimezoneOffset(); // Stable per range: rounded to the minute so the URL doesn't change on every render. const now = Math.floor(Date.now() / 60000) * 60000 + 60000; const from = days ? now - days * 86400000 : 0; const { data, error } = useFetch(`/stats?from=${from}&to=${now}&tz=${tz}${userFilter ? `&user=${userFilter}` : ""}`); const view = useMemo(() => { if (!data) return undefined; const cats = dayRange(data.from, data.to, data.by_day[0]?.day); // Up to 7 models by cost get their own color; the rest fold into "Other". const ranked = [...data.by_model].sort((a, b) => b.cost - a.cost || b.requests - a.requests); const named = ranked.slice(0, ranked.length > 8 ? 7 : 8).map((m) => m.model); const other = t("stats.other"); const modelSeries: Series[] = [...named, ...(ranked.length > 8 ? [other] : [])].map((m, i) => ({ key: m, label: m, color: SERIES[i] })); const costByDay = cats.map(() => ({}) as Record); const idx = new Map(cats.map((c, i) => [c, i])); for (const r of data.by_day_model) { const i = idx.get(r.day); if (i === undefined) continue; const key = named.includes(r.model) ? r.model : other; costByDay[i][key] = (costByDay[i][key] ?? 0) + r.cost; } const tokenSeries: Series[] = [ { key: "input", label: t("stats.input"), color: SERIES[0] }, { key: "cache", label: t("stats.cacheRead"), color: SERIES[1] }, { key: "output", label: t("stats.output"), color: SERIES[2] }, ]; const tokensByDay = cats.map((c): Record => { const d = data.by_day.find((x) => x.day === c); return d ? { input: d.input_tokens + d.cache_write_tokens, cache: d.cache_read_tokens, output: d.output_tokens } : {}; }); const cells = Array(168).fill(0); for (const h of data.heatmap) { cells[h.slot] = heat === "requests" ? h.requests : heat === "cost" ? h.cost : h.input_tokens + h.cache_read_tokens + h.cache_write_tokens + h.output_tokens; } return { cats, modelSeries, costByDay, tokenSeries, tokensByDay, cells }; }, [data, heat, t]); const dayLabel = (d: string) => new Intl.DateTimeFormat(getLang(), { day: "numeric", month: "short" }).format(new Date(`${d}T00:00:00`)); const cur = data?.currency ?? "USD"; const tot = data?.total; const heatFormat = (v: number) => (heat === "cost" ? fmtCost(v, cur) : fmtCompact(v)); return ( <>

{t("nav.stats")}

{data && (
{data.from ? fmtDate(data.from, false) : t("stats.rangeAll")} – {fmtDate(data.to, false)}
)}
{RANGES.map((r) => ( ))}
{isAdmin && ( )}
{error &&
{error}
} {!data && !error &&
{t("common.loading")}
} {isAdmin && } {data && tot && view && ( <>
{tot.requests === 0 ? (
{t("stats.noData")}
) : tableView ? (

{t("stats.perDay")}

) : ( <>

{t("stats.costPerDay")}

fmtCost(v, cur)} />

{t("stats.tokensPerDay")}

{t("stats.heatmap")}

)} {tot.requests > 0 && ( <>

{t("stats.byModel")}

{t("stats.byProvider")}

{t("stats.byAgent")}

{isAdmin && !userFilter && data.by_user.length > 1 && (

{t("stats.byUser")}

)}

{t("stats.topSessions")}

navigate(`/s/${r.session_id}`)} />
)} )}
); }