"use client"; import { useState, useEffect, useMemo } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import Card from "./Card"; import Badge from "./Badge"; import { CardSkeleton } from "./Loading"; function SortIcon({ field, currentSort, currentOrder }) { if (currentSort !== field) return ; return {currentOrder === "asc" ? "↑" : "↓"}; } function MiniBarGraph({ data, colorClass = "bg-primary" }) { const max = Math.max(...data, 1); return (
{data.slice(-9).map((val, i) => (
))}
); } 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 toggleSort = (field) => { 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 = (dataMap, pendingMap = {}) => { return Object.entries(dataMap || {}) .map(([key, data]) => ({ ...data, key, totalTokens: (data.promptTokens || 0) + (data.completionTokens || 0), 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; }); }; const sortedModels = useMemo( () => sortData(stats?.byModel, stats?.pending?.byModel), [stats?.byModel, stats?.pending?.byModel, sortBy, sortOrder] ); 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 = {}; if (stats?.pending?.byAccount) { Object.entries(stats.byAccount || {}).forEach(([accountKey, data]) => { 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, sortBy, sortOrder]); useEffect(() => { fetchStats(); }, []); useEffect(() => { let interval; if (autoRefresh) { interval = setInterval(() => { fetchStats(false); // fetch without loading skeleton }, 1000); } return () => clearInterval(interval); }, [autoRefresh]); const fetchStats = async (showLoading = true) => { if (showLoading) setLoading(true); try { const res = await fetch("/api/usage/history"); if (res.ok) { const data = await res.json(); setStats(data); } } catch (error) { console.error("Failed to fetch usage stats:", error); } finally { if (showLoading) setLoading(false); } }; if (loading) return ; if (!stats) return (
Failed to load usage statistics.
); // Format number with commas const fmt = (n) => new Intl.NumberFormat().format(n || 0); // Time format for "Last Used" const fmtTime = (iso) => { if (!iso) return "Never"; const date = new Date(iso); const now = new Date(); const diffMs = now - date; 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 */}

Usage Overview

{/* Active Requests Summary */} {(stats.activeRequests || []).length > 0 && (
Active Requests
{stats.activeRequests.map((req, i) => (
{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" />
Total Output Tokens {fmt(stats.totalCompletionTokens)}
m.completionTokens)} colorClass="bg-success/50" />
{/* Usage by Model Table */}

Usage by Model

{sortedModels.map((data) => ( ))} {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{" "}
0 ? "text-primary" : "" }`} > {data.rawModel} 0 ? "primary" : "neutral"} size="sm" > {data.provider} {fmt(data.requests)} {fmtTime(data.lastUsed)} {fmt(data.promptTokens)} {fmt(data.completionTokens)} {fmt(data.totalTokens)}
No usage recorded yet. Make some requests to see data here.
{/* Usage by Account Table */}

Usage by Account

{sortedAccounts.map((data) => ( ))} {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{" "}
0 ? "text-primary" : "" }`} > {data.rawModel} 0 ? "primary" : "neutral"} 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)}
No account-specific usage recorded yet. Make requests using OAuth accounts to see data here.
); }