From 20785a8414bb0e364085a4a64f6a958c178d1df8 Mon Sep 17 00:00:00 2001 From: Gemini Nexus Bot Date: Sat, 2 May 2026 00:28:45 -0300 Subject: [PATCH] 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 --- src/components/ui/alert.tsx | 49 +++ src/components/usage/ChartCard.tsx | 12 +- src/components/usage/ForecastCard.tsx | 171 +++++++++ src/components/usage/ModelQuotaBar.tsx | 59 +++ src/components/usage/NivoTooltip.tsx | 34 +- src/components/usage/StatCard.tsx | 15 +- src/lib/nivo-theme.ts | 6 +- src/routes/usage.tsx | 492 ++++++++++++++----------- 8 files changed, 586 insertions(+), 252 deletions(-) create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/usage/ForecastCard.tsx create mode 100644 src/components/usage/ModelQuotaBar.tsx diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..24946fe --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const alertVariants = cva( + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/usage/ChartCard.tsx b/src/components/usage/ChartCard.tsx index 792e80f..4c4b640 100644 --- a/src/components/usage/ChartCard.tsx +++ b/src/components/usage/ChartCard.tsx @@ -29,10 +29,10 @@ export const ChartCard: React.FC = ({ children, }) => { return ( -
+
-

{title}

+

{title}

{isLoading ? ( @@ -41,8 +41,8 @@ export const ChartCard: React.FC = ({
) : error ? (
-
- +
+

{errorMessage}

- - Live - + Live

@@ -347,10 +390,7 @@ function UsagePage() { - setRange(v as TimeRange)} - > + setRange(v as TimeRange)}> 24h @@ -366,75 +406,147 @@ function UsagePage() { -

- {/* ─── Stat cards ─── */} -
- - - - + +
+ + Analytics + Monitor +
+ + {hasQuotaWarning && ( + + + Quota Warning + + One or more models have exceeded 80% of their allocated usage limit. + + + )} + + {/* ─── Stat cards ─── */} + +
+ +
+ + Input Tokens + + + + + +

Tokens sent to the model (prompt, context, files).

+
+
+
+ } + value={formatNumber(totals.promptTokens)} + icon={MessageSquare} + isLoading={dailyLoading} + accent="blue" + trend={ + prevDailyData + ? calcTrend(totals.promptTokens, prevTotals.promptTokens) + : undefined + } + sparkline={sparklinePrompt} + /> +
+
+ + Output Tokens + + + + + +

Tokens generated by the model in response.

+
+
+
+ } + value={formatNumber(totals.completionTokens)} + icon={Activity} + isLoading={dailyLoading} + accent="green" + trend={ + prevDailyData + ? calcTrend(totals.completionTokens, prevTotals.completionTokens) + : undefined + } + sparkline={sparklineCompletion} + /> +
+ +
+ +
+ Estimated Cost + + + + + +

Approximate cost based on configured pricing.

+
+
+
+ +
+ } + value={`$${totalCost.toFixed(2)}`} + icon={DollarSign} + isLoading={modelLoading} + accent="purple" + /> +
+ + {/* ─── Prompt/Completion ratio bar + top models ─── */} {!dailyLoading && totals.totalTokens > 0 && (
-
+
- - Prompt / Completion + + Input / Output - + {formatNumber(totals.totalTokens)} total
@@ -461,76 +573,30 @@ function UsagePage() { className="h-2 w-2 rounded-full" style={{ backgroundColor: COLOR_PROMPT }} /> - Prompt - - {promptRatio}% - + Input + {promptRatio}%
- Completion - + Output + {completionRatio}%
Avg/req:{' '} - + {formatNumber(avgReqSize)}
- {/* Top models */} -
-
- - Top Models - - - tokens - -
-
- {topModels.map((m, i) => { - const max = topModels[0]?.totalTokens || 1; - const pct = Math.max(8, (m.totalTokens / max) * 100); - return ( -
-
- - {m.model} - - - {formatNumber(m.totalTokens)} - -
-
-
-
-
- ); - })} - {topModels.length === 0 && ( -

- No data -

- )} -
-
+
)} @@ -542,9 +608,7 @@ function UsagePage() { isLoading={areaLoading} error={areaError} isEmpty={isAreaEmpty} - onRetry={() => - range === '24h' ? refetchHourly() : refetchDaily() - } + onRetry={() => (range === '24h' ? refetchHourly() : refetchDaily())} emptyTitle="No usage data" emptyDescription="Data will appear here once proxy requests are recorded." errorMessage="Failed to load usage data" @@ -579,8 +643,7 @@ function UsagePage() { tickSize: 0, tickPadding: 8, tickRotation: 0, - format: (v: number) => - v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${v}`, + format: (v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${v}`), }} tooltip={LineTooltip} legends={[ @@ -617,47 +680,46 @@ function UsagePage() { errorMessage="Failed to load model data" retryLabel="Retry" > -
- formatNumber(d.value as number)} - labelSkipWidth={40} - labelTextColor="#e8eaed" - tooltip={({ indexValue, value }) => ( -
-

- {indexValue} -

- - {(value as number).toLocaleString()} tokens - -
- )} - animate={true} - motionConfig="gentle" - /> +
+ {topModelsData.map((m) => ( + + ))}
- + + {/* ─── Cost Report ─── */} + {!dailyLoading && modelData && modelData.length > 0 && ( +
+ acc + r.inputCost, 0)} + totalOutputCost={costReport.reduce((acc, r) => acc + r.outputCost, 0)} + totalCost={totalCost} + isLoading={modelLoading} + /> +
+ )} + + {/* ─── Modals ─── */} + m.model) || []} + /> + + + + + ); }