diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderBarChart.js b/src/app/(dashboard)/dashboard/usage/components/ProviderBarChart.js new file mode 100644 index 00000000..12eafa60 --- /dev/null +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderBarChart.js @@ -0,0 +1,107 @@ +"use client"; + +import { useState, useMemo } from "react"; +import PropTypes from "prop-types"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from "recharts"; +import Card from "@/shared/components/Card"; + +const COLORS = ["#6366f1", "#14b8a6", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#10b981", "#f97316"]; + +const fmtTokens = (n) => { + if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; + if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; + return String(n || 0); +}; + +export default function ProviderBarChart({ byProvider }) { + const [viewMode, setViewMode] = useState("tokens"); + + const chartData = useMemo(() => { + if (!byProvider) return []; + return Object.entries(byProvider) + .map(([id, data]) => ({ + name: id, + tokens: (data.promptTokens || 0) + (data.completionTokens || 0), + requests: data.requests || 0, + })) + .filter((d) => d[viewMode] > 0) + .sort((a, b) => b[viewMode] - a[viewMode]); + }, [byProvider, viewMode]); + + const fmt = viewMode === "tokens" ? fmtTokens : String; + const label = viewMode === "tokens" ? "Tokens" : "Requests"; + + return ( + +
+ By Provider +
+ + +
+
+ + {!chartData.length ? ( +
No provider usage yet
+ ) : ( + + + + v.length > 10 ? v.slice(0, 10) + "…" : v} + /> + + [fmt(value), label]} + /> + + {chartData.map((_, i) => ( + + ))} + + + + )} +
+ ); +} + +ProviderBarChart.propTypes = { + byProvider: PropTypes.object, +}; diff --git a/src/app/(dashboard)/dashboard/usage/components/TopModelsChart.js b/src/app/(dashboard)/dashboard/usage/components/TopModelsChart.js new file mode 100644 index 00000000..abf87cd7 --- /dev/null +++ b/src/app/(dashboard)/dashboard/usage/components/TopModelsChart.js @@ -0,0 +1,114 @@ +"use client"; + +import { useState, useMemo } from "react"; +import PropTypes from "prop-types"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from "recharts"; +import Card from "@/shared/components/Card"; + +const COLORS = ["#6366f1", "#14b8a6", "#f59e0b", "#ef4444", "#8b5cf6"]; + +const fmtTokens = (n) => { + if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; + if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; + return String(n || 0); +}; + +const truncate = (s, max = 22) => (s && s.length > max ? s.slice(0, max) + "…" : s || ""); + +export default function TopModelsChart({ byModel }) { + const [viewMode, setViewMode] = useState("tokens"); + + const chartData = useMemo(() => { + if (!byModel) return []; + return Object.values(byModel) + .map((data) => ({ + name: truncate(data.rawModel || "Unknown"), + tokens: (data.promptTokens || 0) + (data.completionTokens || 0), + requests: data.requests || 0, + })) + .filter((d) => d[viewMode] > 0) + .sort((a, b) => b[viewMode] - a[viewMode]) + .slice(0, 5); + }, [byModel, viewMode]); + + const fmt = viewMode === "tokens" ? fmtTokens : String; + const label = viewMode === "tokens" ? "Tokens" : "Requests"; + + return ( + +
+ Top Models +
+ + +
+
+ + {!chartData.length ? ( +
No model usage yet
+ ) : ( + + + + + + [fmt(value), label]} + /> + + {chartData.map((_, i) => ( + + ))} + + + + )} +
+ ); +} + +TopModelsChart.propTypes = { + byModel: PropTypes.object, +}; diff --git a/src/app/(dashboard)/dashboard/usage/components/UsageChart.js b/src/app/(dashboard)/dashboard/usage/components/UsageChart.js index 14a19c42..8300e97d 100644 --- a/src/app/(dashboard)/dashboard/usage/components/UsageChart.js +++ b/src/app/(dashboard)/dashboard/usage/components/UsageChart.js @@ -10,7 +10,6 @@ import { CartesianGrid, Tooltip, ResponsiveContainer, - Legend, } from "recharts"; import Card from "@/shared/components/Card"; @@ -21,6 +20,19 @@ const fmtTokens = (n) => { }; const fmtCost = (n) => `$${(n || 0).toFixed(4)}`; +const fmtRequests = (n) => String(n || 0); + +const VIEW_MODES = [ + { value: "tokens", label: "Tokens" }, + { value: "requests", label: "Requests" }, + { value: "cost", label: "Cost" }, +]; + +const VIEW_CONFIG = { + tokens: { dataKey: "tokens", color: "#6366f1", gradId: "gradTokens", formatter: fmtTokens, label: "Tokens" }, + requests: { dataKey: "requests", color: "#14b8a6", gradId: "gradRequests", formatter: fmtRequests, label: "Requests" }, + cost: { dataKey: "cost", color: "#f59e0b", gradId: "gradCost", formatter: fmtCost, label: "Cost" }, +}; export default function UsageChart({ period = "7d" }) { const [data, setData] = useState([]); @@ -46,23 +58,24 @@ export default function UsageChart({ period = "7d" }) { fetchData(); }, [fetchData]); - const hasData = data.some((d) => d.tokens > 0 || d.cost > 0); + const cfg = VIEW_CONFIG[viewMode]; + const hasData = data.some((d) => (d[cfg.dataKey] || 0) > 0); return ( -
- - +
+ {VIEW_MODES.map((m) => ( + + ))}
{loading ? ( @@ -77,6 +90,10 @@ export default function UsageChart({ period = "7d" }) { + + + + @@ -94,7 +111,7 @@ export default function UsageChart({ period = "7d" }) { tick={{ fontSize: 10, fill: "currentColor", fillOpacity: 0.5 }} tickLine={false} axisLine={false} - tickFormatter={viewMode === "tokens" ? fmtTokens : fmtCost} + tickFormatter={cfg.formatter} width={50} /> - name === "tokens" ? [fmtTokens(value), "Tokens"] : [fmtCost(value), "Cost"] - } + formatter={(value) => [cfg.formatter(value), cfg.label]} + /> + - {viewMode === "tokens" ? ( - - ) : ( - - )} )} diff --git a/src/lib/db/repos/usageRepo.js b/src/lib/db/repos/usageRepo.js index 6b1966cb..0d8aa96f 100644 --- a/src/lib/db/repos/usageRepo.js +++ b/src/lib/db/repos/usageRepo.js @@ -670,7 +670,7 @@ export async function getChartData(period = "7d") { const startTime = startOfDay.getTime(); const endTime = startTime + bucketCount * bucketMs; const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }); - const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 })); + const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0, requests: 0 })); const rows = db.all( `SELECT timestamp, promptTokens, completionTokens, cost FROM usageHistory WHERE timestamp >= ?`, @@ -683,6 +683,7 @@ export async function getChartData(period = "7d") { if (idx >= 0 && idx < bucketCount) { buckets[idx].tokens += (r.promptTokens || 0) + (r.completionTokens || 0); buckets[idx].cost += r.cost || 0; + buckets[idx].requests += 1; } } return buckets; @@ -693,7 +694,7 @@ export async function getChartData(period = "7d") { const bucketMs = 3600000; const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }); const startTime = now - bucketCount * bucketMs; - const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 })); + const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0, requests: 0 })); const rows = db.all( `SELECT timestamp, promptTokens, completionTokens, cost FROM usageHistory WHERE timestamp >= ?`, @@ -705,6 +706,7 @@ export async function getChartData(period = "7d") { const idx = Math.min(Math.floor((t - startTime) / bucketMs), bucketCount - 1); buckets[idx].tokens += (r.promptTokens || 0) + (r.completionTokens || 0); buckets[idx].cost += r.cost || 0; + buckets[idx].requests += 1; } return buckets; } @@ -727,6 +729,7 @@ export async function getChartData(period = "7d") { label: labelFn(d), tokens: dayData ? (dayData.promptTokens || 0) + (dayData.completionTokens || 0) : 0, cost: dayData ? (dayData.cost || 0) : 0, + requests: dayData ? (dayData.requests || 0) : 0, }; }); } diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index ecab40b1..093c8b3a 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -18,6 +18,8 @@ import dynamic from "next/dynamic"; // Lazy-load: keeps @xyflow/react out of the shared bundle until topology renders const ProviderTopology = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/ProviderTopology"), { ssr: false }); import UsageChart from "@/app/(dashboard)/dashboard/usage/components/UsageChart"; +import ProviderBarChart from "@/app/(dashboard)/dashboard/usage/components/ProviderBarChart"; +import TopModelsChart from "@/app/(dashboard)/dashboard/usage/components/TopModelsChart"; function timeAgo(timestamp) { const diff = Math.floor((Date.now() - new Date(timestamp)) / 1000); @@ -483,6 +485,14 @@ export default function UsageStats({ period: periodProp, setPeriod: setPeriodPro {/* Token / Cost chart - sync period */} {loading ? spinner : } + {/* Provider and model breakdown charts */} + {!loading && (stats.byProvider || stats.byModel) && ( +
+ + +
+ )} + {/* Table with dropdown selector */}