"use client"; import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import PropTypes from "prop-types"; import { useSearchParams, useRouter } from "next/navigation"; import Card from "./Card"; import Badge from "./Badge"; import { CardSkeleton } from "./Loading"; import { fmtFull, fmtCost } from "@/shared/utils/formatting"; function SortIcon({ field, currentSort, currentOrder, }: { field: string; currentSort: string; currentOrder: string; }) { if (currentSort !== field) return ; return {currentOrder === "asc" ? "↑" : "↓"}; } SortIcon.propTypes = { field: PropTypes.string.isRequired, currentSort: PropTypes.string.isRequired, currentOrder: PropTypes.string.isRequired, }; function MiniBarGraph({ data, colorClass = "bg-primary", }: { data: number[]; colorClass?: string; }) { const max = Math.max(...data, 1); return (
{data.slice(-9).map((val, idx) => (
))}
); } MiniBarGraph.propTypes = { data: PropTypes.arrayOf(PropTypes.number).isRequired, colorClass: PropTypes.string, }; export default function UsageStats() { const router = useRouter(); const searchParams = useSearchParams(); const sortBy = searchParams.get("sortBy") || "rawModel"; const sortOrder = searchParams.get("sortOrder") || "asc"; const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); const [viewMode, setViewMode] = useState("tokens"); // 'tokens' or 'costs' const [refreshInterval, setRefreshInterval] = useState(5000); // Start with 5s const prevTotalRequestsRef = useRef(0); const toggleSort = (field: string) => { const params = new URLSearchParams(searchParams.toString()); if (sortBy === field) { params.set("sortOrder", sortOrder === "asc" ? "desc" : "asc"); } else { params.set("sortBy", field); params.set("sortOrder", "asc"); } router.replace(`?${params.toString()}`, { scroll: false }); }; const sortData = useCallback( (dataMap: Record, pendingMap: Record = {}) => { return Object.entries(dataMap || {}) .map(([key, data]: [string, any]) => { const totalTokens = (data.promptTokens || 0) + (data.completionTokens || 0); const totalCost = data.cost || 0; // Calculate cost breakdown (estimated based on token ratio) const inputCost = totalTokens > 0 ? (data.promptTokens || 0) * (totalCost / totalTokens) : 0; const outputCost = totalTokens > 0 ? (data.completionTokens || 0) * (totalCost / totalTokens) : 0; return { ...data, key, totalTokens, totalCost, inputCost, outputCost, pending: pendingMap[key] || 0, }; }) .sort((a, b) => { let valA = a[sortBy]; let valB = b[sortBy]; // Handle case-insensitive sorting for strings if (typeof valA === "string") valA = valA.toLowerCase(); if (typeof valB === "string") valB = valB.toLowerCase(); if (valA < valB) return sortOrder === "asc" ? -1 : 1; if (valA > valB) return sortOrder === "asc" ? 1 : -1; return 0; }); }, [sortBy, sortOrder] ); const sortedModels = useMemo( () => sortData(stats?.byModel, stats?.pending?.byModel), [stats?.byModel, stats?.pending?.byModel, sortData] ); const sortedAccounts = useMemo(() => { // For accounts, pendingMap is by connectionId, but dataMap is by accountKey // We need to map connectionId pending counts to accountKeys const accountPendingMap: Record = {}; if (stats?.pending?.byAccount) { Object.entries(stats.byAccount || {}).forEach(([accountKey, data]: [string, any]) => { const connPending = stats.pending.byAccount[data.connectionId]; if (connPending) { // Get modelKey (rawModel (provider)) const modelKey = data.provider ? `${data.rawModel} (${data.provider})` : data.rawModel; accountPendingMap[accountKey] = connPending[modelKey] || 0; } }); } return sortData(stats?.byAccount, accountPendingMap); }, [stats?.byAccount, stats?.pending?.byAccount, sortData]); const fetchStats = useCallback(async (showLoading = true): Promise => { if (showLoading) setLoading(true); try { const res = await fetch("/api/usage/history"); if (res.ok) { const data = await res.json(); setStats(data); // Smart polling: adjust interval based on activity const currentTotal = data.totalRequests || 0; if (currentTotal > prevTotalRequestsRef.current) { // New requests detected - reset to fast polling setRefreshInterval(5000); } else { // No change - increase interval (exponential backoff) setRefreshInterval((prev) => Math.min(prev * 2, 60000)); // Max 60s } prevTotalRequestsRef.current = currentTotal; } } catch (error) { console.error("Failed to fetch usage stats:", error); } finally { if (showLoading) setLoading(false); } }, []); useEffect(() => { fetchStats(); }, [fetchStats]); useEffect(() => { let intervalId: ReturnType | undefined; let isPageVisible = true; // Page Visibility API - pause when tab is hidden const handleVisibilityChange = () => { isPageVisible = !document.hidden; if (isPageVisible && autoRefresh) { fetchStats(false); // Fetch immediately when tab becomes visible } }; document.addEventListener("visibilitychange", handleVisibilityChange); if (autoRefresh) { // Clear any existing interval first if (intervalId) clearInterval(intervalId); intervalId = setInterval(() => { if (isPageVisible) { fetchStats(false); // fetch without loading skeleton } }, refreshInterval); } return () => { if (intervalId) clearInterval(intervalId); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [autoRefresh, refreshInterval, fetchStats]); if (loading) return ; if (!stats) return
Failed to load usage statistics.
; // Format number with commas — delegated to shared module const fmt = (n: number) => fmtFull(n); // Format cost with dollar sign and 2 decimals — delegated to shared module // Time format for "Last Used" const fmtTime = (iso: string) => { if (!iso) return "Never"; const date = new Date(iso); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`; return date.toLocaleDateString(); }; return (
{/* Header with Auto Refresh Toggle and View Toggle */}

Usage Overview

{/* View Toggle */}
{/* Auto Refresh Toggle */}
Auto Refresh ({refreshInterval / 1000}s)
{/* Active Requests Summary */} {(stats.activeRequests || []).length > 0 && (
Active Requests
{stats.activeRequests.map((req) => (
{req.model} | {req.provider} | {req.account} {req.count > 1 && ( x{req.count} )}
))}
)} {/* Overview Cards */}
Total Requests {fmt(stats.totalRequests)}
m.requests)} colorClass="bg-text-muted/30" />
Total Input Tokens {fmt(stats.totalPromptTokens)}
m.promptTokens)} colorClass="bg-primary/50" />
Output Tokens {fmt(stats.totalCompletionTokens)}
Total Cost {fmtCost(stats.totalCost)}
{/* Usage by Model Table */}

Usage by Model

{viewMode === "tokens" ? ( <> ) : ( <> )} {sortedModels.map((data) => ( {viewMode === "tokens" ? ( <> ) : ( <> )} ))} {sortedModels.length === 0 && ( )}
toggleSort("rawModel")} > Model toggleSort("provider")} > Provider{" "} toggleSort("requests")} > Requests{" "} toggleSort("lastUsed")} > Last Used{" "} toggleSort("promptTokens")} > Input Tokens{" "} toggleSort("completionTokens")} > Output Tokens{" "} toggleSort("totalTokens")} > Total Tokens{" "} toggleSort("promptTokens")} > Input Cost{" "} toggleSort("completionTokens")} > Output Cost{" "} toggleSort("cost")} > Total Cost{" "}
0 ? "text-primary" : "" }`} > {data.rawModel} 0 ? "primary" : "default"} size="sm"> {data.provider} {fmt(data.requests)} {fmtTime(data.lastUsed)} {fmt(data.promptTokens)} {fmt(data.completionTokens)} {fmt(data.totalTokens)} {fmtCost(data.inputCost)} {fmtCost(data.outputCost)} {fmtCost(data.totalCost)}
No usage recorded yet. Make some requests to see data here.
{/* Usage by Account Table */}

Usage by Account

{viewMode === "tokens" ? ( <> ) : ( <> )} {sortedAccounts.map((data) => ( {viewMode === "tokens" ? ( <> ) : ( <> )} ))} {sortedAccounts.length === 0 && ( )}
toggleSort("rawModel")} > Model toggleSort("provider")} > Provider{" "} toggleSort("accountName")} > Account{" "} toggleSort("requests")} > Requests{" "} toggleSort("lastUsed")} > Last Used{" "} toggleSort("promptTokens")} > Input Tokens{" "} toggleSort("completionTokens")} > Output Tokens{" "} toggleSort("totalTokens")} > Total Tokens{" "} toggleSort("promptTokens")} > Input Cost{" "} toggleSort("completionTokens")} > Output Cost{" "} toggleSort("cost")} > Total Cost{" "}
0 ? "text-primary" : "" }`} > {data.rawModel} 0 ? "primary" : "default"} size="sm"> {data.provider} 0 ? "text-primary" : "" }`} > {data.accountName || `Account ${data.connectionId?.slice(0, 8)}...`} {fmt(data.requests)} {fmtTime(data.lastUsed)} {fmt(data.promptTokens)} {fmt(data.completionTokens)} {fmt(data.totalTokens)} {fmtCost(data.inputCost)} {fmtCost(data.outputCost)} {fmtCost(data.totalCost)}
No account-specific usage recorded yet. Make requests using OAuth accounts to see data here.
); }