This commit is contained in:
decolua
2026-06-15 18:18:04 +07:00
parent 8ab5af0052
commit b282f05549
66 changed files with 2328 additions and 213 deletions
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import ProviderIcon from "@/shared/components/ProviderIcon";
import QuotaTable from "./QuotaTable";
import Toggle from "@/shared/components/Toggle";
import Tooltip from "@/shared/components/Tooltip";
import {
parseQuotaData,
calculatePercentage,
@@ -34,9 +35,15 @@ import {
QUOTA_SORT_OPTIONS,
} from "./utils";
import Card from "@/shared/components/Card";
import { EditConnectionModal } from "@/shared/components";
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
function getCodexResetCreditCount(quota) {
const value = quota?.raw?.resetCredits?.availableCount;
const count = typeof value === "number" ? value : Number(value);
return Number.isFinite(count) ? Math.max(0, count) : 0;
}
export default function ProviderLimits() {
const [connections, setConnections] = useState([]);
const [quotaData, setQuotaData] = useState({});
@@ -50,6 +57,8 @@ export default function ProviderLimits() {
const [connectionsLoading, setConnectionsLoading] = useState(true);
const [deletingId, setDeletingId] = useState(null);
const [togglingId, setTogglingId] = useState(null);
const [resettingLimitId, setResettingLimitId] = useState(null);
const [resetConfirmState, setResetConfirmState] = useState(null);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedConnection, setSelectedConnection] = useState(null);
const [proxyPools, setProxyPools] = useState([]);
@@ -207,6 +216,32 @@ export default function ProviderLimits() {
[fetchQuota],
);
const handleResetCodexLimit = useCallback(
async (connectionId, provider) => {
if (provider !== "codex" || resettingLimitId) return;
setResettingLimitId(connectionId);
setErrors((prev) => ({ ...prev, [connectionId]: null }));
try {
const response = await fetch(`/api/usage/${connectionId}/codex-reset-credits`, { method: "POST" });
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.message || result.error || result.code || "Failed to reset Codex limit");
}
await fetchQuota(connectionId, provider);
setLastUpdated(new Date());
} catch (error) {
setErrors((prev) => ({ ...prev, [connectionId]: error.message || "Failed to reset Codex limit" }));
} finally {
setResettingLimitId(null);
}
},
[fetchQuota, resettingLimitId],
);
const handleDeleteConnection = useCallback(
async (id) => {
if (!confirm("Delete this connection?")) return;
@@ -791,7 +826,10 @@ export default function ProviderLimits() {
// Use table layout for all providers
const isInactive = conn.isActive === false;
const rowBusy = deletingId === conn.id || togglingId === conn.id;
const isCodex = conn.provider === "codex";
const resetCreditCount = getCodexResetCreditCount(quota);
const isResettingLimit = resettingLimitId === conn.id;
const rowBusy = deletingId === conn.id || togglingId === conn.id || isResettingLimit;
return (
<Card
@@ -822,53 +860,90 @@ export default function ProviderLimits() {
{getConnectionLabel(conn)}
</p>
) : null}
{isCodex && (
<p className="text-[11px] text-text-muted truncate">
Reset eligible: {resetCreditCount}
</p>
)}
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={() => refreshProvider(conn.id, conn.provider)}
disabled={isLoading || rowBusy}
aria-label="Refresh quota"
className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
title="Refresh quota"
>
<span
className={`material-symbols-outlined text-[18px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
{isCodex && (
<Tooltip text={`Codex reset credits remaining: ${resetCreditCount}`}>
<div
className={`hidden h-8 items-center gap-1 rounded-lg border px-2 text-[11px] sm:flex ${
resetCreditCount > 0
? "border-primary/30 bg-primary/5 text-primary"
: "border-black/10 bg-black/[0.02] text-text-muted dark:border-white/10 dark:bg-white/[0.03]"
}`}
>
<span className="material-symbols-outlined text-[14px]">restart_alt</span>
<span className="tabular-nums">{resetCreditCount}</span>
</div>
</Tooltip>
)}
{isCodex && resetCreditCount > 0 && (
<Tooltip text={`Use one Codex reset credit. Available: ${resetCreditCount}`}>
<button
type="button"
onClick={() => setResetConfirmState({ connection: conn, resetCreditCount })}
disabled={isLoading || rowBusy}
className="flex h-8 items-center gap-1 rounded-lg border border-primary/30 px-2 text-[11px] text-primary transition-colors hover:bg-primary/10 disabled:opacity-50"
>
<span className={`material-symbols-outlined text-[15px] ${isResettingLimit ? "animate-spin" : ""}`}>
{isResettingLimit ? "progress_activity" : "bolt"}
</span>
<span className="hidden lg:inline">Reset limit</span>
</button>
</Tooltip>
)}
<Tooltip text="Refresh quota">
<button
type="button"
onClick={() => refreshProvider(conn.id, conn.provider)}
disabled={isLoading || rowBusy}
aria-label="Refresh quota"
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
>
refresh
</span>
</button>
<button
type="button"
onClick={() => {
setSelectedConnection(conn);
setShowEditModal(true);
}}
disabled={rowBusy}
aria-label="Edit connection"
className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors disabled:opacity-50"
title="Edit connection"
>
<span className="material-symbols-outlined text-[18px]">
edit
</span>
</button>
<button
type="button"
onClick={() => handleDeleteConnection(conn.id)}
disabled={rowBusy}
aria-label="Delete connection"
className="p-1.5 rounded-lg hover:bg-red-500/10 text-red-500 transition-colors disabled:opacity-50"
title="Delete connection"
>
<span
className={`material-symbols-outlined text-[18px] ${deletingId === conn.id ? "animate-pulse" : ""}`}
<span
className={`material-symbols-outlined text-[18px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
>
refresh
</span>
</button>
</Tooltip>
<Tooltip text="Edit connection">
<button
type="button"
onClick={() => {
setSelectedConnection(conn);
setShowEditModal(true);
}}
disabled={rowBusy}
aria-label="Edit connection"
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors disabled:opacity-50"
>
delete
</span>
</button>
<span className="material-symbols-outlined text-[18px]">
edit
</span>
</button>
</Tooltip>
<Tooltip text="Delete connection">
<button
type="button"
onClick={() => handleDeleteConnection(conn.id)}
disabled={rowBusy}
aria-label="Delete connection"
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-red-500/10 text-red-500 transition-colors disabled:opacity-50"
>
<span
className={`material-symbols-outlined text-[18px] ${deletingId === conn.id ? "animate-pulse" : ""}`}
>
delete
</span>
</button>
</Tooltip>
<div
className="inline-flex items-center pl-0.5"
title={
@@ -1047,6 +1122,25 @@ export default function ProviderLimits() {
</div>
</div>
<ConfirmModal
isOpen={Boolean(resetConfirmState)}
onClose={() => {
if (!resettingLimitId) setResetConfirmState(null);
}}
onConfirm={async () => {
const connection = resetConfirmState?.connection;
if (!connection) return;
await handleResetCodexLimit(connection.id, connection.provider);
setResetConfirmState(null);
}}
title="Reset Codex limit?"
message={`Use 1 Codex reset credit for ${getConnectionLabel(resetConfirmState?.connection || {}) || "this account"}. This cannot be undone. Remaining credits: ${resetConfirmState?.resetCreditCount ?? 0}.`}
confirmText="Reset limit"
cancelText="Cancel"
variant="danger"
loading={Boolean(resettingLimitId)}
/>
<EditConnectionModal
isOpen={showEditModal}
connection={selectedConnection}