import { useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "../../i18n"; import { useCurrency } from "../../hooks/useCurrency"; import { fmtPct, fmtTokens } from "../../lib/dashboard-format"; import type { DailyRow } from "../../types/dashboard"; import { DailyDetailModal } from "./DailyDetailModal"; type TrendDimension = "model" | "provider"; interface DailyBarsProps { daily: DailyRow[]; /** Which per-day breakdown to render */ dimension: TrendDimension; /** Ordered names for the active dimension (colour palette & legend) */ names: string[]; /** Label for the unresolved-provider bucket */ unknownProviderLabel: string; } /** Deterministic colour palette — one hue per index */ const PALETTE = [ "#3b82f6", // blue "#22c55e", // green "#f97316", // orange "#a855f7", // purple "#ec4899", // pink "#eab308", // yellow "#06b6d4", // cyan "#f43f5e", // rose "#84cc16", // lime "#8b5cf6", // violet ]; export function modelColor(index: number): string { return PALETTE[index % PALETTE.length]; } /** Short display name for a raw model id */ function shortModel(model: string): string { const bare = model.includes("/") ? model.split("/").pop()! : model; return bare; } export function DailyBars({ daily, dimension, names, unknownProviderLabel }: DailyBarsProps) { const { t } = useTranslation(); const { money } = useCurrency(); const [selected, setSelected] = useState(null); const colorMap = useMemo(() => { const map: Record = {}; names.forEach((m, i) => { map[m] = modelColor(i); }); return map; }, [names]); const displayName = (key: string): string => { if (dimension === "provider") { return key === "unknown" ? unknownProviderLabel : key; } // Subagent records (Kimi Code `__secondary__` marker) show their own label. if (key === "__secondary__") { return t("subagentModel"); } return shortModel(key); }; if (!daily?.length) { return (
{t("noData")}
); } const max = Math.max(...daily.map((d) => d.totalTokens || 0), 1); return (
{/* Legend */}
{names.map((m) => ( {displayName(m)} ))}
{/* Bars */}
{daily.map((d) => { const h = Math.max(4, Math.round(((d.totalTokens || 0) / max) * 170)); const total = d.totalTokens || 1; const breakdown = dimension === "provider" ? d.byProvider : d.byModel; const segments = Object.entries(breakdown || {}) .map(([key, tokens]) => ({ key, tokens, color: colorMap[key] || "#6b7280", height: Math.max(1, Math.round((tokens / total) * h)), })) .sort((a, b) => b.tokens - a.tokens); const tooltipLines = [ d.date, `${t("totalTokens")}: ${fmtTokens(d.totalTokens)} · ${money(d.costUsd)} · ${t("colCacheHit")} ${fmtPct(d.cacheHitRate)}`, ...segments.map((s) => `${displayName(s.key)}: ${fmtTokens(s.tokens)}`), "", t("doubleClickHint"), ].join("\n"); return ( ); })}
{selected && createPortal( setSelected(null)} />, document.body )}
); }