"use client"; import { useState, useMemo, useCallback, useRef, useEffect } from "react"; import { useLocale, useTranslations } from "next-intl"; import Card from "../Card"; import { getModelColor } from "@/shared/constants/colors"; import { fmtCompact as fmt, fmtFull, fmtCost, formatApiKeyLabel as maskApiKeyLabel, } from "@/shared/utils/formatting"; import { getServiceTierDisplayLabel, translateCostText, type TranslationFn, } from "@/shared/utils/serviceTierLabels"; import { BarChart, ComposedChart, Bar, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, PieChart, Pie, AreaChart, Area, } from "recharts"; function createDateFormatter(locale: string, options: Intl.DateTimeFormatOptions) { try { return new Intl.DateTimeFormat(locale, options); } catch { return new Intl.DateTimeFormat(undefined, options); } } // ── Custom Tooltip for dark theme ────────────────────────────────────────── function DarkTooltip({ active, payload, label, formatter, }: { active?: boolean; payload?: any[]; label?: any; formatter?: Function; }) { if (!active || !payload?.length) return null; return (
{label &&
{label}
} {payload.map((entry, i) => (
{entry.name}: {formatter ? formatter(entry.value) : entry.value}
))}
); } // ── Sort Indicator (shared by tables) ────────────────────────────────────── export function SortIndicator({ active, sortOrder }: { active: boolean; sortOrder: string }) { if (!active) { return ( unfold_more ); } return ( {sortOrder === "asc" ? "expand_less" : "expand_more"} ); } // ── StatCard (primary KPI) ───────────────────────────────────────────────── export function StatCard({ icon, label, value, subValue, color = "text-text-main", }: { icon: any; label: any; value: any; subValue?: any; color?: string; }) { return (
{icon} {label}
{value} {subValue && {subValue}}
); } // ── CompactStatGrid (secondary metrics in a single card, grouped) ───────── export type CompactStatSection = { title: string; items: Array<{ icon: string; label: string; value: any; color?: string }>; /** On mobile use 1 column instead of 2 — useful when values can be long (model names, etc.) */ wideValues?: boolean; }; export function CompactStatGrid({ sections }: { sections: CompactStatSection[] }) { return (
{sections.map((section, si) => (
{si > 0 && (
)}
{section.title}
{section.items.map((stat, i) => (
{stat.icon} {stat.label}
{stat.value}
))}
))}
); } // ── ActivityHeatmap ──────────────────────────────────────────────────────── export function ActivityHeatmap({ activityMap }) { const t = useTranslations("analytics"); const scrollRef = useRef(null); const cells = useMemo(() => { const today = new Date(); const days = []; let maxVal = 0; for (let i = 364; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; const val = activityMap?.[key] || 0; if (val > maxVal) maxVal = val; days.push({ date: key, value: val, dayOfWeek: d.getDay() }); } return { days, maxVal }; }, [activityMap]); const weeks = useMemo(() => { const w = []; let current = []; const firstDay = cells.days[0]?.dayOfWeek || 0; for (let i = 0; i < firstDay; i++) { current.push(null); } for (const day of cells.days) { current.push(day); if (current.length === 7) { w.push(current); current = []; } } if (current.length > 0) w.push(current); return w; }, [cells]); // Auto-scroll to the right edge so the current date is visible useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; } }, [weeks]); const monthLabels = useMemo(() => { const labels = []; let lastMonth = -1; weeks.forEach((week, weekIdx) => { const firstDay = week.find((d) => d !== null); if (firstDay) { const m = new Date(firstDay.date).getMonth(); if (m !== lastMonth) { const monthNames = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; labels.push({ weekIdx, label: monthNames[m] }); lastMonth = m; } } }); return labels; }, [weeks]); function getCellColor(value) { if (!value || value === 0) return "bg-white/[0.04]"; const intensity = Math.min(value / (cells.maxVal || 1), 1); if (intensity < 0.25) return "bg-primary/20"; if (intensity < 0.5) return "bg-primary/40"; if (intensity < 0.75) return "bg-primary/60"; return "bg-primary/90"; } return (

{t("overview")}

{Object.keys(activityMap || {}).length} active days ·{" "} {fmt(Object.values(activityMap || {}).reduce((a: number, b: number) => a + b, 0))} tokens · 365 days
{monthLabels.map((m, i) => ( {m.label} ))}
Mon Wed Fri
{weeks.map((week, wi) => (
{week.map((day, di) => (
))}
))}
Less
More
); } // ── DailyTrendChart (Recharts) ───────────────────────────────────────────── export function DailyTrendChart({ dailyTrend }) { const t = useTranslations("analytics"); const chartData = useMemo(() => { return (dailyTrend || []).map((d) => ({ date: d.date.slice(5), [t("chartInput")]: d.promptTokens, [t("chartOutput")]: d.completionTokens, [t("chartCost")]: d.cost || 0, })); }, [dailyTrend, t]); const hasCost = useMemo(() => chartData.some((d) => d[t("chartCost")] > 0), [chartData, t]); if (!chartData.length) { return (

{t("chartModelUsageOverTime")}

{t("chartNoData")}
); } return (

{t("chartModelUsageOverTime")}

{hasCost && ( `$${v.toFixed(2)}`} width={36} /> )} } cursor={{ fill: "rgba(255,255,255,0.04)" }} /> {hasCost && ( )}
{t("chartInput")} {t("chartOutput")} {hasCost && ( {t("chartCost")} ($) )}
); } // ── Cost-aware Tooltip ───────────────────────────────────────────────────── function CostTooltip({ active, payload, label, }: { active?: boolean; payload?: any[]; label?: any; }) { const t = useTranslations("analytics"); if (!active || !payload?.length) return null; return (
{label &&
{label}
} {payload.map((entry, i) => (
{entry.name}: {entry.name === t("chartCost") ? fmtCost(entry.value) : fmt(entry.value)}
))}
); } // ── AccountDonut (Recharts) ──────────────────────────────────────────────── export function AccountDonut({ byAccount }) { const t = useTranslations("analytics"); const data = useMemo(() => byAccount || [], [byAccount]); const hasData = data.length > 0; const pieData = useMemo(() => { return data.slice(0, 8).map((item, i) => ({ name: item.account, value: item.totalTokens, fill: getModelColor(i), })); }, [data]); if (!hasData) { return (

By Account

{t("chartNoData")}
); } return (

By Account

{pieData.map((entry, i) => ( ))} } />
{pieData.map((seg, i) => (
{seg.name}
{fmt(seg.value)}
))}
); } // ── ApiKeyDonut (Recharts) ───────────────────────────────────────────────── export function ApiKeyDonut({ byApiKey }) { const t = useTranslations("analytics"); const data = useMemo(() => byApiKey || [], [byApiKey]); const hasData = data.length > 0; const pieData = useMemo(() => { return data.slice(0, 8).map((item, i) => ({ name: maskApiKeyLabel(item.apiKeyName, item.apiKeyId), fullName: item.apiKeyName || item.apiKeyId || "unknown", value: item.totalTokens, fill: getModelColor(i), })); }, [data]); if (!hasData) { return (

By API Key

{t("chartNoData")}
); } return (

By API Key

{pieData.map((entry, i) => ( ))} } />
{pieData.map((seg, i) => (
{seg.name}
{fmt(seg.value)}
))}
); } // ── ApiKeyTable ──────────────────────────────────────────────────────────── export function ApiKeyTable({ byApiKey }) { const t = useTranslations("analytics"); const [query, setQuery] = useState(""); const [sortBy, setSortBy] = useState("totalTokens"); const [sortOrder, setSortOrder] = useState("desc"); const data = useMemo(() => byApiKey || [], [byApiKey]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return data; return data.filter( (row) => (row.apiKeyName || "").toLowerCase().includes(q) || (row.apiKeyId || "").toLowerCase().includes(q) ); }, [data, query]); const sorted = useMemo(() => { const arr = [...filtered]; arr.sort((a, b) => { const va = a[sortBy] ?? 0; const vb = b[sortBy] ?? 0; if (typeof va === "string") { return sortOrder === "asc" ? va.localeCompare(vb) : vb.localeCompare(va); } return sortOrder === "asc" ? va - vb : vb - va; }); return arr; }, [filtered, sortBy, sortOrder]); const toggleSort = useCallback( (field) => { if (sortBy === field) { setSortOrder((prev) => (prev === "asc" ? "desc" : "asc")); return; } setSortBy(field); setSortOrder("desc"); }, [sortBy] ); const hasData = data.length > 0; if (!hasData) { return (

API Key Breakdown

{t("chartNoData")}
); } return (

API Key Breakdown

setQuery(e.target.value)} placeholder={t("filterSearchKeys")} className="w-full max-w-[220px] px-3 py-1.5 rounded-lg bg-bg-subtle border border-border text-xs text-text-primary placeholder:text-text-muted focus:outline-none focus:border-primary" />
{sorted.map((row, i) => ( ))} {sorted.length === 0 && ( )}
toggleSort("apiKeyName")} > API Key toggleSort("requests")} > {t("chartRequests")}{" "} toggleSort("promptTokens")} > {t("chartInput")}{" "} toggleSort("completionTokens")} > {t("chartOutput")}{" "} toggleSort("totalTokens")} > {t("chartTotal")}{" "} toggleSort("cost")} > {t("chartCost")}
{maskApiKeyLabel(row.apiKeyName, row.apiKeyId)} {fmtFull(row.requests)} {fmt(row.promptTokens)} {fmt(row.completionTokens)} {fmt(row.totalTokens)} {fmtCost(row.cost)}
{t("filterNoKeysMatch")}
); } // ── WeeklyPattern (Recharts) ─────────────────────────────────────────────── export function WeeklyPattern({ weeklyPattern }) { const t = useTranslations("analytics"); const chartData = useMemo(() => { return (weeklyPattern || []).map((w) => ({ day: w.day.slice(0, 3), Tokens: w.totalTokens, })); }, [weeklyPattern]); return (

{t("chartWeekly")}

} cursor={{ fill: "rgba(255,255,255,0.04)" }} />
); } // ── MostActiveDay7d ──────────────────────────────────────────────────────── export function MostActiveDay7d({ activityMap }) { const locale = useLocale(); const weekdayFormatter = useMemo( () => createDateFormatter(locale, { weekday: "long" }), [locale] ); const dateFormatter = useMemo( () => createDateFormatter(locale, { month: "short", day: "numeric" }), [locale] ); const data = useMemo(() => { if (!activityMap) return null; const today = new Date(); let peakKey = null; let peakVal = 0; for (let i = 0; i < 7; i++) { const d = new Date(today); d.setDate(d.getDate() - i); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; const val = activityMap[key] || 0; if (val > peakVal) { peakVal = val; peakKey = key; } } if (!peakKey || peakVal === 0) return null; const peakDate = new Date(peakKey + "T12:00:00"); return { weekday: weekdayFormatter.format(peakDate), label: dateFormatter.format(peakDate), tokens: peakVal, }; }, [activityMap, dateFormatter, weekdayFormatter]); return (

Most Active Day

{data ? ( <> {data.weekday} {data.label} · {fmt(data.tokens)} tokens ) : ( No data in the last 7 days )}
); } // ── WeeklySquares7d ──────────────────────────────────────────────────────── export function WeeklySquares7d({ activityMap }) { const t = useTranslations("analytics"); const locale = useLocale(); const weekdayFormatter = useMemo( () => createDateFormatter(locale, { weekday: "short" }), [locale] ); const dateFormatter = useMemo( () => createDateFormatter(locale, { month: "short", day: "numeric" }), [locale] ); const days = useMemo(() => { if (!activityMap) return []; const today = new Date(); const result = []; let maxVal = 0; for (let i = 6; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; const val = activityMap[key] || 0; if (val > maxVal) maxVal = val; result.push({ key, val, label: weekdayFormatter.format(d), dateLabel: dateFormatter.format(d), }); } return result.map((d) => ({ ...d, intensity: maxVal > 0 ? d.val / maxVal : 0 })); }, [activityMap, dateFormatter, weekdayFormatter]); function getSquareStyle(intensity) { if (intensity === 0) return { background: "rgba(255,255,255,0.04)" }; const opacity = 0.15 + intensity * 0.75; return { background: `rgba(229, 77, 94, ${opacity.toFixed(2)})` }; } return (

{t("chartWeekly")}

{days.map((d, i) => (
{d.label}
))}
); } // ── ModelTable ────────────────────────────────────────────────────────────── export function ModelTable({ byModel, summary }) { const t = useTranslations("analytics"); const [sortBy, setSortBy] = useState("totalTokens"); const [sortOrder, setSortOrder] = useState("desc"); const toggleSort = useCallback( (field) => { if (sortBy === field) { setSortOrder((prev) => (prev === "asc" ? "desc" : "asc")); } else { setSortBy(field); setSortOrder("desc"); } }, [sortBy] ); const sorted = useMemo(() => { const arr = [...(byModel || [])]; arr.sort((a, b) => { const va = a[sortBy] ?? 0; const vb = b[sortBy] ?? 0; if (typeof va === "string") return sortOrder === "asc" ? va.localeCompare(vb) : vb.localeCompare(va); return sortOrder === "asc" ? va - vb : vb - va; }); return arr; }, [byModel, sortBy, sortOrder]); return (

{t("chartModelBreakdown")}

{sorted.map((m, i) => ( ))}
toggleSort("model")} > {t("chartModel")}{" "} toggleSort("requests")} > {t("chartRequests")}{" "} toggleSort("promptTokens")} > {t("chartInput")}{" "} toggleSort("completionTokens")} > {t("chartOutput")}{" "} toggleSort("totalTokens")} > {t("chartTotal")}{" "} toggleSort("cost")} > {t("chartCost")} {t("chartShare")}
{m.model}
{fmtFull(m.requests)} {fmt(m.promptTokens)} {fmt(m.completionTokens)} {fmt(m.totalTokens)} {fmtCost(m.cost)}
{m.pct}%
); } export function ServiceTierBreakdown({ byServiceTier, summary }) { const t = useTranslations("costs") as TranslationFn; const data = useMemo(() => byServiceTier || [], [byServiceTier]); const totalRequests = Number(summary?.totalRequests || 0); const totalCost = Number(summary?.totalCost || 0); if (!data.length) { return null; } return (

{translateCostText(t, "serviceTierBreakdownTitle", "Service Tier")}

{translateCostText(t, "serviceTierBreakdownSubtitle", "Fast / Flex / Standard split")}
{data.map((tier) => { const isFast = tier.serviceTier === "priority"; const isFlex = tier.serviceTier === "flex"; const tierLabel = getServiceTierDisplayLabel(t, tier.serviceTier, tier.label); const requestPct = totalRequests > 0 ? ((Number(tier.requests || 0) / totalRequests) * 100).toFixed(1) : "0"; const costPct = totalCost > 0 ? ((Number(tier.cost || 0) / totalCost) * 100).toFixed(1) : "0"; return (
{isFast ? "bolt" : isFlex ? "savings" : "speed"}
{tierLabel}
{fmtFull(tier.requests)} requests · {fmt(tier.totalTokens)} tokens {isFlex && Number(tier.usageSavingsTokens || 0) > 0 ? ` · ${fmt(tier.usageSavingsTokens)} ${translateCostText( t, "serviceTierUsageSaved", "usage saved" )}` : ""}
{fmtCost(tier.cost)}
{isFlex && Number(tier.savings || 0) > 0 ? `${fmtCost(tier.savings)} ${translateCostText( t, "serviceTierCostSaved", "saved" )}` : `${costPct}% ${translateCostText( t, "serviceTierCostShareSuffix", "of cost" )}`}
); })}
); } // ── UsageDetail ──────────────────────────────────────────────────────────── export function UsageDetail({ summary }) { const t = useTranslations("analytics"); const items = [ { label: t("chartInput"), value: summary?.promptTokens, color: "text-primary" }, { label: t("chartCacheRead"), value: 0, color: "text-text-muted" }, { label: t("chartOutput"), value: summary?.completionTokens, color: "text-emerald-500" }, ]; return (

{t("chartUsageDetail")}

{items.map((item, i) => (
{item.label} {fmtFull(item.value)}
))}
); } // ── ProviderCostDonut ────────────────────────────────────────────────────── const PROVIDER_COLORS = [ "#f59e0b", "#ef4444", "#8b5cf6", "#10b981", "#06b6d4", "#ec4899", "#f97316", "#6366f1", "#14b8a6", "#a855f7", ]; export function ProviderCostDonut({ byProvider }) { const t = useTranslations("analytics"); const data = useMemo(() => byProvider || [], [byProvider]); const hasData = data.length > 0 && data.some((p) => p.cost > 0); const pieData = useMemo(() => { return data .filter((item) => item.cost > 0) .sort((a, b) => b.cost - a.cost) .slice(0, 8) .map((item, i) => ({ name: item.provider, value: item.cost, fill: PROVIDER_COLORS[i % PROVIDER_COLORS.length], })); }, [data]); if (!hasData) { return (

{t("chartCostByProvider")}

{t("chartNoCostData")}
); } return (

{t("chartCostByProvider")}

{pieData.map((entry, i) => ( ))} } />
{pieData.map((seg, i) => (
{seg.name}
{fmtCost(seg.value)}
))}
); } // ── ModelOverTimeChart (Stacked Area) ────────────────────────────────────── export function ModelOverTimeChart({ dailyByModel, modelNames }) { const t = useTranslations("analytics"); const data = useMemo(() => dailyByModel || [], [dailyByModel]); const models = useMemo(() => modelNames || [], [modelNames]); // Prepare chart data — format dates (must be before early return for rules-of-hooks) const chartData = useMemo(() => { return data.map((d) => { const row = { ...d }; // Short date label if (d.date) { const parts = d.date.split("-"); row.dateLabel = `${parts[1]}/${parts[2]}`; } return row; }); }, [data]); if (!data.length || !models.length) { return (

{t("chartModelUsageOverTime")}

{t("chartNoData")}
); } return (

{t("chartModelUsageOverTime")}

fmt(v)} axisLine={false} tickLine={false} width={50} /> } /> {models.map((m, i) => ( ))}
{models.map((m, i) => ( {m} ))}
); } // ── ProviderTable ────────────────────────────────────────────────────────── export function ProviderTable({ byProvider }) { const t = useTranslations("analytics"); const [sortBy, setSortBy] = useState("totalTokens"); const [sortOrder, setSortOrder] = useState("desc"); const data = useMemo(() => byProvider || [], [byProvider]); const totalTokens = useMemo(() => data.reduce((acc, p) => acc + p.totalTokens, 0), [data]); const toggleSort = useCallback( (field) => { if (sortBy === field) { setSortOrder((prev) => (prev === "asc" ? "desc" : "asc")); } else { setSortBy(field); setSortOrder("desc"); } }, [sortBy] ); const sorted = useMemo(() => { const arr = [...data]; arr.sort((a, b) => { const va = a[sortBy] ?? 0; const vb = b[sortBy] ?? 0; if (typeof va === "string") return sortOrder === "asc" ? va.localeCompare(vb) : vb.localeCompare(va); return sortOrder === "asc" ? va - vb : vb - va; }); return arr; }, [data, sortBy, sortOrder]); if (!data.length) { return (

{t("chartProviderBreakdown")}

{t("chartNoData")}
); } return (

{t("chartProviderBreakdown")}

{sorted.map((p, i) => { const pct = totalTokens > 0 ? ((p.totalTokens / totalTokens) * 100).toFixed(1) : "0"; return ( ); })}
toggleSort("provider")} > {t("chartProvider")}{" "} toggleSort("requests")} > {t("chartRequests")}{" "} toggleSort("promptTokens")} > {t("chartInput")}{" "} toggleSort("completionTokens")} > {t("chartOutput")}{" "} toggleSort("totalTokens")} > {t("chartTotal")}{" "} toggleSort("cost")} > {t("chartCost")} {t("chartShare")}
{p.provider}
{fmtFull(p.requests)} {fmt(p.promptTokens)} {fmt(p.completionTokens)} {fmt(p.totalTokens)} {fmtCost(p.cost)}
{pct}%
); }