diff --git a/open-sse/config/userTokenLimits.js b/open-sse/config/userTokenLimits.js index e2e81d99..4cccd032 100644 --- a/open-sse/config/userTokenLimits.js +++ b/open-sse/config/userTokenLimits.js @@ -27,4 +27,5 @@ export const USER_TOKEN_LIMIT_WINDOW_CONFIG = Object.freeze({ }), }); -export const USER_TOKEN_LIMIT_SESSION_MS = 5 * 60 * 60 * 1000; \ No newline at end of file +export const USER_TOKEN_LIMIT_SESSION_MS = 5 * 60 * 60 * 1000; +export const USER_TOKEN_LIMIT_WEEKLY_MS = 7 * 24 * 60 * 60 * 1000; \ No newline at end of file diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index f338073f..3d072ae8 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -211,15 +211,17 @@ export function setQuotaCache(connectionId, quotaEntry) { /** * Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit) * @param {string|Date} date - ISO date string or Date object + * @param {string|Date|number} now - Current time, injectable for live countdowns * @returns {string} Formatted countdown (e.g., "2d 5h 30m", "4h 40m", "15m") or "-" */ -export function formatResetTime(date) { +export function formatResetTime(date, now = new Date()) { if (!date) return "-"; try { const resetDate = typeof date === "string" ? new Date(date) : date; - const now = new Date(); - const diffMs = resetDate - now; + const currentTime = now instanceof Date ? now : new Date(now); + if (!Number.isFinite(resetDate.getTime()) || !Number.isFinite(currentTime.getTime())) return "-"; + const diffMs = resetDate - currentTime; if (diffMs <= 0) return "-"; diff --git a/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js b/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js index 84c4f48b..be05bb85 100644 --- a/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js +++ b/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js @@ -4,10 +4,10 @@ import { useCallback, useEffect, useState } from "react"; import { AI_PROVIDERS } from "@/shared/constants/providers"; import { Button, Card, CardSkeleton } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; -import { REFRESH_INTERVAL_MS } from "./ProviderLimits/utils"; +import { formatResetTime, REFRESH_INTERVAL_MS } from "./ProviderLimits/utils"; import { formatVietnamTime } from "@/shared/utils/dateTime"; import { formatTokenCount } from "@/shared/utils/tokenCount.js"; -import { USER_TOKEN_LIMIT_WINDOW_CONFIG } from "open-sse/config/userTokenLimits.js"; +import { USER_TOKEN_LIMIT_WINDOWS } from "open-sse/config/userTokenLimits.js"; function getProviderInfo(providerId) { return AI_PROVIDERS[providerId] || { @@ -36,6 +36,29 @@ function getQuotaTone(percentage) { return { bar: "bg-red-500", dot: "bg-red-500", text: "text-red-500" }; } +function TokenQuotaResetStatus({ quota }) { + const [now, setNow] = useState(() => new Date()); + + useEffect(() => { + if (!quota.resetAt) return undefined; + + const intervalId = window.setInterval(() => setNow(new Date()), 60 * 1000); + return () => window.clearInterval(intervalId); + }, [quota.resetAt]); + + const countdown = formatResetTime(quota.resetAt, now); + const isSession = quota.windowType === USER_TOKEN_LIMIT_WINDOWS.SESSION; + const text = countdown === "-" + ? (isSession ? "No tokens pending expiry" : "Reset time unavailable") + : (isSession ? `Next tokens restore in ${countdown}` : `Resets in ${countdown}`); + + return ( + + {text} + + ); +} + function QuotaListRow({ quota }) { const tone = getQuotaTone(quota.remainingPercentage); @@ -68,17 +91,16 @@ function QuotaListRow({ quota }) { function TokenQuotaListRow({ quota }) { const isUnlimited = quota.isUnlimited === true; const tone = isUnlimited ? null : getQuotaTone(quota.remainingPercentage); - const windowConfig = USER_TOKEN_LIMIT_WINDOW_CONFIG[quota.windowType]; - const description = windowConfig?.description || "Personal token budget"; return (
Personal token budget
: null}{windowOption.description}
+{isUnlimited ? "No usage cap" : `${formatTokenCount(limit)} tokens`}
+Enter 0 to leave this window unlimited.
+Budget settings
-Set 0 to leave a provider window unlimited.
+Budget settings
+Enter an exact token budget for each usage window. Set 0 for unlimited.
+{activeTokenLimitCount} / {TOKEN_LIMIT_PROVIDER_OPTIONS.length * TOKEN_LIMIT_WINDOW_OPTIONS.length}
+active budgets
+{provider.description}