"use client"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useProviderNodeMap, resolveProviderName } from "@/lib/display/useProviderNodeMap"; import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import Card from "@/shared/components/Card"; import ProviderIcon from "@/shared/components/ProviderIcon"; import TimeRangeSelector from "@/shared/components/analytics/TimeRangeSelector"; import type { ProviderUtilizationPoint, ProviderUtilizationResponse, UtilizationTimeRange, } from "@/shared/types/utilization"; const RANGE_LABELS: Record = { "1h": "Last hour", "24h": "Last 24 hours", "7d": "Last 7 days", "30d": "Last 30 days", }; const PROVIDER_COLORS = [ "var(--color-primary)", "var(--color-accent)", "var(--color-success)", "var(--color-warning)", "var(--color-error)", "var(--color-text-muted)", ]; function formatTimestamp(value: string, range: UtilizationTimeRange) { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } if (range === "1h" || range === "24h") { return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", }).format(date); } return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", }).format(date); } function formatTooltipTimestamp(value: string, range: UtilizationTimeRange) { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: range === "1h" || range === "24h" ? "2-digit" : undefined, minute: range === "1h" || range === "24h" ? "2-digit" : undefined, }).format(date); } function formatPercent(value: number) { return `${Math.round(value)}%`; } function getLatestPoints(points: ProviderUtilizationPoint[]) { const latestByProvider = new Map(); for (const point of points) { const current = latestByProvider.get(point.provider); if (!current || new Date(point.timestamp).getTime() > new Date(current.timestamp).getTime()) { latestByProvider.set(point.provider, point); } } return Array.from(latestByProvider.values()).sort((a, b) => b.remainingPct - a.remainingPct); } export default function ProviderUtilizationTab() { const t = useTranslations("analytics"); const nodeMap = useProviderNodeMap(); const [range, setRange] = useState("24h"); const [aggregateBy, setAggregateBy] = useState<"provider" | "connection">("provider"); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchUtilization = useCallback( async ( selectedRange: UtilizationTimeRange, selectedAggregate: "provider" | "connection", signal?: AbortSignal ) => { setLoading(true); try { const response = await fetch( `/api/usage/utilization?range=${selectedRange}&aggregateBy=${selectedAggregate}`, { signal, cache: "no-store", } ); if (!response.ok) { throw new Error("Failed to fetch utilization data"); } const json = (await response.json()) as ProviderUtilizationResponse; setData(json); setError(null); } catch (fetchError) { if (fetchError instanceof DOMException && fetchError.name === "AbortError") { return; } setError( fetchError instanceof Error ? fetchError.message : "Failed to fetch utilization data" ); setData(null); } finally { if (!signal?.aborted) { setLoading(false); } } }, [] ); useEffect(() => { const controller = new AbortController(); fetchUtilization(range, aggregateBy, controller.signal); return () => controller.abort(); }, [fetchUtilization, range, aggregateBy]); const providerColors = useMemo(() => { const colors = new Map(); for (const [index, provider] of (data?.providers ?? []).entries()) { colors.set(provider, PROVIDER_COLORS[index % PROVIDER_COLORS.length]); } return colors; }, [data?.providers]); const chartData = useMemo(() => { if (!data?.data.length) { return []; } const byTimestamp = new Map>(); for (const point of data.data) { const entry = byTimestamp.get(point.timestamp) ?? { timestamp: point.timestamp, label: formatTimestamp(point.timestamp, data.timeRange), }; entry[point.provider] = Number(point.remainingPct.toFixed(2)); byTimestamp.set(point.timestamp, entry); } return Array.from(byTimestamp.entries()) .sort(([left], [right]) => new Date(left).getTime() - new Date(right).getTime()) .map(([, value]) => value); }, [data]); const latestPoints = useMemo(() => getLatestPoints(data?.data ?? []), [data?.data]); const hasData = Boolean(data?.data.length); const [retrying, setRetrying] = useState(false); const handleRetry = useCallback(() => { setRetrying(true); setError(null); fetchUtilization(range, aggregateBy).finally(() => setRetrying(false)); }, [range, aggregateBy, fetchUtilization]); return (
} className="overflow-hidden" > {loading && !hasData ? (
progress_activity Loading utilization data…
) : error ? (
error

{t("providerUtilizationFailedToLoad")}

{error}

) : !hasData ? (
timeline

{t("providerUtilizationNoData")}

Provider quota snapshots will appear here after utilization data is collected.

{t("providerUtilizationGettingStarted")}

  • check_circle Connect providers via OAuth or API keys in Providers
  • check_circle Enable quota tracking by using the provider in a combo or direct request
  • check_circle Data will appear automatically as quota snapshots are collected
) : (
formatTimestamp(String(value), range)} tick={{ fill: "var(--color-text-muted)", fontSize: 12 }} axisLine={{ stroke: "var(--color-border)" }} tickLine={{ stroke: "var(--color-border)" }} minTickGap={24} /> formatTooltipTimestamp(String(value), range)} formatter={(value: number, name: string) => [formatPercent(value), name]} contentStyle={{ backgroundColor: "var(--color-surface)", borderColor: "var(--color-border)", borderRadius: 12, color: "var(--color-text-main)", boxShadow: "var(--shadow-soft)", }} itemStyle={{ color: "var(--color-text-main)" }} labelStyle={{ color: "var(--color-text-main)", fontWeight: 600 }} /> {data?.providers.map((provider) => ( ))}
{latestPoints.map((point) => { const isLow = point.remainingPct <= 20; return (

{resolveProviderName(point.provider, nodeMap)}

{t("providerUtilizationLatestSnapshot")}

{point.isExhausted ? "Exhausted" : isLow ? "Low" : "Healthy"}

{point.remainingPct.toFixed(point.remainingPct < 10 ? 1 : 0)}%

{t("providerUtilizationRemainingCapacity")}

{formatTooltipTimestamp(point.timestamp, range)}

{point.windowKey}

0% Remaining quota 100%
); })}
)}
); }