diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index 23ab42c..25c4dfa 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -1,10 +1,16 @@ "use client"; -import { useState, useEffect } from "react"; +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 ( @@ -22,10 +28,51 @@ function MiniBarGraph({ data, colorClass = "bg-primary" }) { } 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(false); + 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) => { + return Object.entries(dataMap || {}) + .map(([key, data]) => ({ + ...data, + key, + totalTokens: (data.promptTokens || 0) + (data.completionTokens || 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?.byModel, sortBy, sortOrder]); + const sortedAccounts = useMemo(() => sortData(stats?.byAccount), [stats?.byAccount, sortBy, sortOrder]); + useEffect(() => { fetchStats(); }, []); @@ -131,17 +178,29 @@ export default function UsageStats() { - - - - - - + + + + + + - {Object.entries(stats.byModel || {}).map(([key, data]) => ( - + {sortedModels.map((data) => ( + - + ))} - {Object.keys(stats.byModel || {}).length === 0 && ( + {sortedModels.length === 0 && (
ModelProviderRequestsInput TokensOutput TokensTotal Tokens toggleSort("rawModel")}> + Model + toggleSort("provider")}> + Provider + toggleSort("requests")}> + Requests + toggleSort("promptTokens")}> + Input Tokens + toggleSort("completionTokens")}> + Output Tokens + toggleSort("totalTokens")}> + Total Tokens +
{data.rawModel} {data.provider} @@ -149,10 +208,10 @@ export default function UsageStats() { {fmt(data.requests)} {fmt(data.promptTokens)} {fmt(data.completionTokens)}{fmt(data.promptTokens + data.completionTokens)}{fmt(data.totalTokens)}
No usage recorded yet. Make some requests to see data here. @@ -173,18 +232,32 @@ export default function UsageStats() { - - - - - - - + + + + + + + - {Object.entries(stats.byAccount || {}).map(([key, data]) => ( - + {sortedAccounts.map((data) => ( + - + ))} - {Object.keys(stats.byAccount || {}).length === 0 && ( + {sortedAccounts.length === 0 && (
ModelProviderAccountRequestsInput TokensOutput TokensTotal Tokens toggleSort("rawModel")}> + Model + toggleSort("provider")}> + Provider + toggleSort("accountName")}> + Account + toggleSort("requests")}> + Requests + toggleSort("promptTokens")}> + Input Tokens + toggleSort("completionTokens")}> + Output Tokens + toggleSort("totalTokens")}> + Total Tokens +
{data.rawModel} {data.provider} @@ -195,10 +268,10 @@ export default function UsageStats() { {fmt(data.requests)} {fmt(data.promptTokens)} {fmt(data.completionTokens)}{fmt(data.promptTokens + data.completionTokens)}{fmt(data.totalTokens)}
No account-specific usage recorded yet. Make requests using OAuth accounts to see data here.