"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import ProviderIcon from "@/shared/components/ProviderIcon";
import ProviderLimitCard from "./ProviderLimitCard";
import QuotaTable from "./QuotaTable";
import { parseQuotaData, calculatePercentage } from "./utils";
import Card from "@/shared/components/Card";
import Button from "@/shared/components/Button";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
export default function ProviderLimits() {
const [connections, setConnections] = useState([]);
const [quotaData, setQuotaData] = useState({});
const [loading, setLoading] = useState({});
const [errors, setErrors] = useState({});
const [autoRefresh, setAutoRefresh] = useState(true);
const [lastUpdated, setLastUpdated] = useState(null);
const [refreshingAll, setRefreshingAll] = useState(false);
const [countdown, setCountdown] = useState(60);
const [connectionsLoading, setConnectionsLoading] = useState(true);
const intervalRef = useRef(null);
const countdownRef = useRef(null);
// Fetch all provider connections
const fetchConnections = useCallback(async () => {
try {
const response = await fetch("/api/providers/client");
if (!response.ok) throw new Error("Failed to fetch connections");
const data = await response.json();
const connectionList = data.connections || [];
setConnections(connectionList);
return connectionList;
} catch (error) {
console.error("Error fetching connections:", error);
setConnections([]);
return [];
}
}, []);
// Fetch quota for a specific connection
const fetchQuota = useCallback(async (connectionId, provider) => {
setLoading((prev) => ({ ...prev, [connectionId]: true }));
setErrors((prev) => ({ ...prev, [connectionId]: null }));
try {
console.log(
`[ProviderLimits] Fetching quota for ${provider} (${connectionId})`,
);
const response = await fetch(`/api/usage/${connectionId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.error || response.statusText;
// Handle different error types gracefully
if (response.status === 404) {
// Connection not found - skip silently
console.warn(
`[ProviderLimits] Connection not found for ${provider}, skipping`,
);
return;
}
if (response.status === 401) {
// Auth error - show message instead of throwing
console.warn(
`[ProviderLimits] Auth error for ${provider}:`,
errorMsg,
);
setQuotaData((prev) => ({
...prev,
[connectionId]: {
quotas: [],
message: errorMsg,
},
}));
return;
}
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
}
const data = await response.json();
console.log(`[ProviderLimits] Got quota for ${provider}:`, data);
// Parse quota data using provider-specific parser
const parsedQuotas = parseQuotaData(provider, data);
setQuotaData((prev) => ({
...prev,
[connectionId]: {
quotas: parsedQuotas,
plan: data.plan || null,
message: data.message || null,
raw: data,
},
}));
} catch (error) {
console.error(
`[ProviderLimits] Error fetching quota for ${provider} (${connectionId}):`,
error,
);
setErrors((prev) => ({
...prev,
[connectionId]: error.message || "Failed to fetch quota",
}));
} finally {
setLoading((prev) => ({ ...prev, [connectionId]: false }));
}
}, []);
// Refresh quota for a specific provider
const refreshProvider = useCallback(
async (connectionId, provider) => {
await fetchQuota(connectionId, provider);
setLastUpdated(new Date());
},
[fetchQuota],
);
// Refresh all providers
const refreshAll = useCallback(async () => {
if (refreshingAll) return;
setRefreshingAll(true);
setCountdown(60);
try {
const conns = await fetchConnections();
// Filter only supported OAuth providers
const oauthConnections = conns.filter(
(conn) =>
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) &&
conn.authType === "oauth",
);
// Fetch quota for supported OAuth connections only
await Promise.all(
oauthConnections.map((conn) => fetchQuota(conn.id, conn.provider)),
);
setLastUpdated(new Date());
} catch (error) {
console.error("Error refreshing all providers:", error);
} finally {
setRefreshingAll(false);
}
}, [refreshingAll, fetchConnections, fetchQuota]);
// Initial load: fetch connections first so cards render immediately, then fetch quotas
useEffect(() => {
const initializeData = async () => {
setConnectionsLoading(true);
const conns = await fetchConnections();
setConnectionsLoading(false);
const oauthConnections = conns.filter(
(conn) =>
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) &&
conn.authType === "oauth",
);
// Mark all as loading before fetching
const loadingState = {};
oauthConnections.forEach((conn) => {
loadingState[conn.id] = true;
});
setLoading(loadingState);
await Promise.all(
oauthConnections.map((conn) => fetchQuota(conn.id, conn.provider)),
);
setLastUpdated(new Date());
};
initializeData();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Auto-refresh interval
useEffect(() => {
if (!autoRefresh) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
return;
}
// Main refresh interval
intervalRef.current = setInterval(() => {
refreshAll();
}, REFRESH_INTERVAL_MS);
// Countdown interval
countdownRef.current = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) return 60;
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
if (countdownRef.current) clearInterval(countdownRef.current);
};
}, [autoRefresh, refreshAll]);
// Pause auto-refresh when tab is hidden (Page Visibility API)
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
} else if (autoRefresh) {
// Resume auto-refresh when tab becomes visible
intervalRef.current = setInterval(refreshAll, REFRESH_INTERVAL_MS);
countdownRef.current = setInterval(() => {
setCountdown((prev) => (prev <= 1 ? 60 : prev - 1));
}, 1000);
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [autoRefresh, refreshAll]);
// Format last updated time
const formatLastUpdated = useCallback(() => {
if (!lastUpdated) return "Never";
const now = new Date();
const diffMs = now - lastUpdated;
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) return `${diffDays}d ago`;
if (diffHours > 0) return `${diffHours}h ago`;
if (diffMinutes > 0) return `${diffMinutes}m ago`;
return "Just now";
}, [lastUpdated]);
// Filter only supported providers
const filteredConnections = connections.filter(
(conn) =>
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) &&
conn.authType === "oauth",
);
// Sort providers by USAGE_SUPPORTED_PROVIDERS order, then alphabetically
const sortedConnections = [...filteredConnections].sort((a, b) => {
const orderA = USAGE_SUPPORTED_PROVIDERS.indexOf(a.provider);
const orderB = USAGE_SUPPORTED_PROVIDERS.indexOf(b.provider);
if (orderA !== orderB) return orderA - orderB;
return a.provider.localeCompare(b.provider);
});
// Calculate summary stats
const totalProviders = sortedConnections.length;
const activeWithLimits = Object.values(quotaData).filter(
(data) => data?.quotas?.length > 0,
).length;
// Count low quotas (remaining < 30%)
const lowQuotasCount = Object.values(quotaData).reduce((count, data) => {
if (!data?.quotas) return count;
const hasLowQuota = data.quotas.some((quota) => {
const percentage = calculatePercentage(quota.used, quota.total);
return percentage < 30 && quota.total > 0;
});
return count + (hasLowQuota ? 1 : 0);
}, 0);
// Empty state
if (!connectionsLoading && sortedConnections.length === 0) {
return (
Connect to providers with OAuth to track your API quota limits and
usage.
No Providers Connected
{conn.name}
)}{error}
{quota.message}