import { useEffect } from "react"; import { useTranslation } from "../../i18n"; import { useCurrency } from "../../hooks/useCurrency"; import { fmtPct, fmtTokens } from "../../lib/dashboard-format"; import type { DailyRow } from "../../types/dashboard"; import { modelColor } from "./DailyBars"; type TrendDimension = "model" | "provider"; interface DailyDetailModalProps { day: DailyRow; dimension: TrendDimension; /** active-dimension name → colour hex (models in model tab, providers in provider tab) */ colorMap: Record; unknownProviderLabel: string; onClose: () => void; } function shortModel(model: string): string { const bare = model.includes("/") ? model.split("/").pop()! : model; return bare; } export function DailyDetailModal({ day, dimension, colorMap, unknownProviderLabel, onClose }: DailyDetailModalProps) { const { t } = useTranslation(); const { money } = useCurrency(); /** Subagent records (Kimi Code `__secondary__` marker) show their own label. */ const modelName = (model: string) => model === "__secondary__" ? t("subagentModel") : shortModel(model); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []); const total = day.totalTokens || 1; const isProvider = dimension === "provider"; // Flat per-model breakdown (model tab) — structured TotalsRow per model const modelSegments = Object.entries(day.byModel || {}) .map(([model, row]) => ({ model, tokens: row.totalTokens, requests: row.requests, costUsd: row.costUsd, cacheHitRate: row.cacheHitRate, pct: ((row.totalTokens || 0) / total) * 100, })) .sort((a, b) => b.tokens - a.tokens); // Two-level provider → model breakdown (provider tab) const providerGroups = Object.entries(day.byProviderModel || {}) .map(([prov, models]) => { const provTotal = Object.values(models).reduce((a, b) => a + b, 0); const modelList = Object.entries(models) .map(([model, tokens]) => ({ model, tokens })) .sort((a, b) => b.tokens - a.tokens); return { provider: prov, displayProvider: prov === "unknown" ? unknownProviderLabel : prov, total: provTotal, pct: (provTotal / total) * 100, color: colorMap[prov] || "#6b7280", models: modelList, }; }) .sort((a, b) => b.total - a.total); return (
e.stopPropagation()} > {/* Header */}
{t("dayDetail")}
{day.date}
{/* Day summary chips */}
{/* Breakdown — flat by model, or two-level by provider → model */}
{isProvider ? ( <>
{t("providerDistribution")} · {providerGroups.length}
{providerGroups.length === 0 ? (
{t("noData")}
) : (
{providerGroups.map((p) => (
{p.displayProvider} {fmtTokens(p.total)} · {p.pct.toFixed(1)}%
{p.models.map((m) => (
{modelName(m.model)} {fmtTokens(m.tokens)}
))}
))}
)} ) : ( <>
{t("modelDistribution")} · {modelSegments.length}
{modelSegments.length === 0 ? (
{t("noData")}
) : (
{modelSegments.map((s, i) => (
{modelName(s.model)} {s.pct.toFixed(1)}%
{t("totalTokens")}: {fmtTokens(s.tokens)} {t("requests")}: {s.requests.toLocaleString()} {s.costUsd > 0 && {t("cost")}: {money(s.costUsd)}} {t("cacheHitRate")}: {fmtPct(s.cacheHitRate)}
))}
)} )}
{/* Footer hint */}
{t("modalEscHint")}
); } function SummaryChip({ label, value, tone }: { label: string; value: string; tone: "default" | "orange" | "blue" | "green" }) { const valueClass = tone === "orange" ? "text-orange-600 dark:text-orange-400" : tone === "blue" ? "text-blue-400" : tone === "green" ? "text-emerald-600 dark:text-emerald-400" : "text-content-primary"; return (
{label}
{value}
); }