Files
gemini-nexus/src/components/usage/ForecastCard.tsx
T
Gemini Nexus Bot 20785a8414 feat(usage): redesign Usage Analytics with accurate token averages
- Fix token averaging: calculate over active hours/days only (not full period)
- Enable hourlyData query for all time ranges (not just 24h)
- Rename 'Usage Forecast' to 'Usage Averages' (real data, not projections)
- Add avg/request metric and active hours/days context
- Update terminology: Input/Output Tokens with info tooltips
- Add Estimated Cost stat card with per-model calculation
- Add quota warning alert banner when models exceed 80%
- Add ModelQuotaBar component with color thresholds
2026-05-02 00:28:45 -03:00

172 lines
6.5 KiB
TypeScript

import { useMemo } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { TrendingUp, Clock, Calendar, CalendarDays } from 'lucide-react';
interface UsageBucket {
totalTokens: number;
requests: number;
}
interface ForecastCardProps {
/** Hourly buckets for the selected period (always fetched for all ranges). */
hourlyData?: UsageBucket[];
/** Daily buckets for the selected period. */
dailyData?: UsageBucket[];
range: '24h' | '7d' | '30d';
}
function formatNumber(n: number): string {
if (n >= 1_000_000) {
return `${(n / 1_000_000).toFixed(1)}M`;
}
if (n >= 1_000) {
return `${(n / 1_000).toFixed(1)}K`;
}
return n.toLocaleString(undefined, { maximumFractionDigits: 0 });
}
/**
* Usage Averages Card.
*
* Calculates REAL averages over the time the user actually used the proxy,
* not projections over the entire selected window.
*
* Logic:
* - totalTokens = sum of all buckets in the period
* - activeHours = number of hourly buckets with totalTokens > 0
* - activeDays = number of daily buckets with totalTokens > 0
* - avgPerHour = totalTokens / activeHours (real average, no padding)
* - avgPerDay = totalTokens / activeDays (real average, no padding)
*
* For the 24h range, avgPerDay equals totalTokens (since it's a single day).
* For 7d/30d, avgPerDay = totalTokens / activeDays.
*
* The card title says "Usage Averages" (not "Forecast") because these are
* real historical averages, not projections.
*/
export function ForecastCard({ hourlyData, dailyData, range }: ForecastCardProps) {
const stats = useMemo(() => {
const hours = hourlyData ?? [];
const days = dailyData ?? [];
const totalTokens =
hours.length > 0
? hours.reduce((acc, d) => acc + d.totalTokens, 0)
: days.reduce((acc, d) => acc + d.totalTokens, 0);
const totalRequests =
hours.length > 0
? hours.reduce((acc, d) => acc + d.requests, 0)
: days.reduce((acc, d) => acc + d.requests, 0);
// Active hours = hourly buckets where there was actual usage
const activeHours = hours.filter((d) => d.totalTokens > 0).length;
// Active days = daily buckets where there was actual usage
const activeDays = days.filter((d) => d.totalTokens > 0).length;
// Real average per active hour
const avgPerHour = activeHours > 0 ? totalTokens / activeHours : 0;
// Real average per active day
const avgPerDay = activeDays > 0 ? totalTokens / activeDays : 0;
// Average tokens per request
const avgPerRequest = totalRequests > 0 ? totalTokens / totalRequests : 0;
return {
totalTokens,
totalRequests,
activeHours,
activeDays,
avgPerHour,
avgPerDay,
avgPerRequest,
};
}, [hourlyData, dailyData]);
const rangeLabel = range === '24h' ? 'Last 24h' : range === '7d' ? 'Last 7 days' : 'Last 30 days';
return (
<Card className="bg-card overflow-hidden rounded-xl border border-white/[0.06]">
<CardHeader className="p-4 pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-muted-foreground text-[11px] font-medium tracking-wider uppercase">
Usage Averages
</CardTitle>
<span className="text-muted-foreground/70 text-[10px]">{rangeLabel}</span>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="mt-2 space-y-3">
{/* Total consumed in period */}
<div>
<div className="mb-0.5 flex items-center gap-1.5">
<TrendingUp className="h-3 w-3 text-emerald-400" />
<span className="text-muted-foreground text-xs">Total in period</span>
</div>
<div className="text-foreground font-mono text-lg font-semibold">
{formatNumber(stats.totalTokens)}{' '}
<span className="text-muted-foreground font-sans text-xs font-normal">tokens</span>
</div>
<div className="text-muted-foreground/70 mt-0.5 text-[10px]">
{stats.activeHours > 0 &&
`${stats.activeHours} active hour${stats.activeHours !== 1 ? 's' : ''}`}
{stats.activeHours > 0 && stats.activeDays > 0 && ' · '}
{stats.activeDays > 0 &&
`${stats.activeDays} active day${stats.activeDays !== 1 ? 's' : ''}`}
{stats.totalRequests > 0 && ` · ${stats.totalRequests.toLocaleString()} requests`}
</div>
</div>
<div className="h-px bg-white/[0.06]" />
{/* Average per active hour */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Clock className="h-3 w-3 text-blue-400" />
<span className="text-muted-foreground text-xs">Avg / active hour</span>
</div>
<div className="text-foreground font-mono text-sm font-semibold tabular-nums">
{formatNumber(stats.avgPerHour)}
<span className="text-muted-foreground ml-1 font-sans text-[10px] font-normal">
tokens/h
</span>
</div>
</div>
{/* Average per active day */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Calendar className="h-3 w-3 text-violet-400" />
<span className="text-muted-foreground text-xs">Avg / active day</span>
</div>
<div className="text-foreground font-mono text-sm font-semibold tabular-nums">
{formatNumber(stats.avgPerDay)}
<span className="text-muted-foreground ml-1 font-sans text-[10px] font-normal">
tokens/d
</span>
</div>
</div>
{/* Average per request */}
{stats.totalRequests > 0 && (
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<CalendarDays className="h-3 w-3 text-amber-400" />
<span className="text-muted-foreground text-xs">Avg / request</span>
</div>
<div className="text-foreground font-mono text-sm font-semibold tabular-nums">
{formatNumber(stats.avgPerRequest)}
<span className="text-muted-foreground ml-1 font-sans text-[10px] font-normal">
tokens/req
</span>
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
}