"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import PropTypes from "prop-types";
import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal, ConfirmModal } from "@/shared/components";
// ── CooldownTimer ──────────────────────────────────────────────
function CooldownTimer({ until }) {
const [remaining, setRemaining] = useState("");
useEffect(() => {
const update = () => {
const diff = new Date(until).getTime() - Date.now();
if (diff <= 0) { setRemaining(""); return; }
const s = Math.floor(diff / 1000);
if (s < 60) setRemaining(`${s}s`);
else if (s < 3600) setRemaining(`${Math.floor(s / 60)}m ${s % 60}s`);
else setRemaining(`${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`);
};
update();
const t = setInterval(update, 1000);
return () => clearInterval(t);
}, [until]);
if (!remaining) return null;
return ⏱ {remaining};
}
CooldownTimer.propTypes = { until: PropTypes.string.isRequired };
// ── ConnectionRow ──────────────────────────────────────────────
function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete }) {
const [showProxyDropdown, setShowProxyDropdown] = useState(false);
const [updatingProxy, setUpdatingProxy] = useState(false);
const [isCooldown, setIsCooldown] = useState(false);
const proxyDropdownRef = useRef(null);
const proxyPoolMap = new Map((proxyPools || []).map((p) => [p.id, p]));
const boundProxyPoolId = connection.providerSpecificData?.proxyPoolId || null;
const boundProxyPool = boundProxyPoolId ? proxyPoolMap.get(boundProxyPoolId) : null;
const hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl;
const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy;
const proxyDisplayText = boundProxyPool
? `Pool: ${boundProxyPool.name}`
: boundProxyPoolId ? `Pool: ${boundProxyPoolId} (inactive/missing)`
: hasLegacyProxy ? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}` : "";
let maskedProxyUrl = "";
const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl;
if (rawProxyUrl) {
try {
const p = new URL(rawProxyUrl);
maskedProxyUrl = `${p.protocol}//${p.hostname}${p.port ? `:${p.port}` : ""}`;
} catch { maskedProxyUrl = rawProxyUrl; }
}
const noProxyText = boundProxyPool?.noProxy || connection.providerSpecificData?.connectionNoProxy || "";
const proxyBadgeVariant = boundProxyPool?.isActive === true ? "success" : (boundProxyPoolId || hasLegacyProxy) ? "error" : "default";
const modelLockUntil = Object.entries(connection)
.filter(([k]) => k.startsWith("modelLock_"))
.map(([, v]) => v).filter(Boolean).sort()[0] || null;
useEffect(() => {
const check = () => {
const until = Object.entries(connection)
.filter(([k]) => k.startsWith("modelLock_"))
.map(([, v]) => v).filter(v => v && new Date(v).getTime() > Date.now()).sort()[0] || null;
setIsCooldown(!!until);
};
check();
const t = modelLockUntil ? setInterval(check, 1000) : null;
return () => { if (t) clearInterval(t); };
}, [modelLockUntil]);
useEffect(() => {
if (!showProxyDropdown) return;
const handler = (e) => {
if (proxyDropdownRef.current && !proxyDropdownRef.current.contains(e.target))
setShowProxyDropdown(false);
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [showProxyDropdown]);
const effectiveStatus = connection.testStatus === "unavailable" && !isCooldown ? "active" : connection.testStatus;
const getStatusVariant = () => {
if (connection.isActive === false) return "default";
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
return "default";
};
const displayName = isOAuth
? connection.name || connection.email || connection.displayName || "OAuth Account"
: connection.name;
const handleSelectProxy = async (poolId) => {
setUpdatingProxy(true);
try { await onUpdateProxy(poolId === "__none__" ? null : poolId); }
finally { setUpdatingProxy(false); setShowProxyDropdown(false); }
};
return (
{isOAuth ? "lock" : "key"}
{displayName}
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
{hasAnyProxy && Proxy}
{isCooldown && connection.isActive !== false && }
{connection.lastError && connection.isActive !== false && (
{connection.lastError}
)}
#{connection.priority}
{hasAnyProxy && (
{proxyDisplayText}
{maskedProxyUrl && {maskedProxyUrl}}
{noProxyText && no_proxy: {noProxyText}}
)}
{(proxyPools || []).length > 0 && (
{showProxyDropdown && (
{(proxyPools || []).map((pool) => (
))}
)}
)}
);
}
ConnectionRow.propTypes = {
connection: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
email: PropTypes.string,
displayName: PropTypes.string,
testStatus: PropTypes.string,
isActive: PropTypes.bool,
lastError: PropTypes.string,
priority: PropTypes.number,
}).isRequired,
proxyPools: PropTypes.array,
isOAuth: PropTypes.bool.isRequired,
isFirst: PropTypes.bool.isRequired,
isLast: PropTypes.bool.isRequired,
onMoveUp: PropTypes.func.isRequired,
onMoveDown: PropTypes.func.isRequired,
onToggleActive: PropTypes.func.isRequired,
onUpdateProxy: PropTypes.func,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
// ── AddApiKeyModal ─────────────────────────────────────────────
function AddApiKeyModal({ isOpen, provider, providerName, proxyPools, onSave, onClose }) {
const NONE = "__none__";
const [formData, setFormData] = useState({ name: "", apiKey: "", priority: 1, proxyPoolId: NONE });
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [saving, setSaving] = useState(false);
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch("/api/providers/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, apiKey: formData.apiKey }),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch { setValidationResult("failed"); }
finally { setValidating(false); }
};
const handleSubmit = async () => {
if (!provider || !formData.apiKey) return;
setSaving(true);
try {
let isValid = false;
try {
setValidating(true); setValidationResult(null);
const res = await fetch("/api/providers/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, apiKey: formData.apiKey }),
});
const data = await res.json();
isValid = !!data.valid;
setValidationResult(isValid ? "success" : "failed");
} catch { setValidationResult("failed"); }
finally { setValidating(false); }
await onSave({
name: formData.name,
apiKey: formData.apiKey,
priority: formData.priority,
proxyPoolId: formData.proxyPoolId === NONE ? null : formData.proxyPoolId,
testStatus: isValid ? "active" : "unknown",
});
} finally { setSaving(false); }
};
if (!provider) return null;
return (
);
}
AddApiKeyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
provider: PropTypes.string,
providerName: PropTypes.string,
proxyPools: PropTypes.array,
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
// ── ConnectionsCard ────────────────────────────────────────────
// Self-contained card: fetches, displays and manages all connections for a provider.
export default function ConnectionsCard({ providerId, isOAuth }) {
const [connections, setConnections] = useState([]);
const [proxyPools, setProxyPools] = useState([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedConnection, setSelectedConnection] = useState(null);
const [providerStrategy, setProviderStrategy] = useState(null);
const [providerStickyLimit, setProviderStickyLimit] = useState("1");
const [confirmState, setConfirmState] = useState(null);
const fetch_ = useCallback(async () => {
try {
const [connRes, proxyRes, settingsRes] = await Promise.all([
fetch("/api/providers", { cache: "no-store" }),
fetch("/api/proxy-pools?isActive=true", { cache: "no-store" }),
fetch("/api/settings", { cache: "no-store" }),
]);
const connData = await connRes.json();
const proxyData = await proxyRes.json();
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
if (connRes.ok) setConnections((connData.connections || []).filter((c) => c.provider === providerId));
if (proxyRes.ok) setProxyPools(proxyData.proxyPools || []);
const override = (settingsData.providerStrategies || {})[providerId] || {};
setProviderStrategy(override.fallbackStrategy || null);
setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
} catch (e) { console.log("ConnectionsCard fetch error:", e); }
finally { setLoading(false); }
}, [providerId]);
useEffect(() => { fetch_(); }, [fetch_]);
const saveStrategy = async (strategy, stickyLimit) => {
try {
const res = await fetch("/api/settings", { cache: "no-store" });
const data = res.ok ? await res.json() : {};
const current = data.providerStrategies || {};
const override = {};
if (strategy) override.fallbackStrategy = strategy;
if (strategy === "round-robin" && stickyLimit !== "") override.stickyRoundRobinLimit = Number(stickyLimit) || 3;
const updated = { ...current };
if (Object.keys(override).length === 0) delete updated[providerId];
else updated[providerId] = override;
await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerStrategies: updated }) });
} catch (e) { console.log("saveStrategy error:", e); }
};
const handleSwapPriority = async (i1, i2) => {
const next = [...connections];
[next[i1], next[i2]] = [next[i2], next[i1]];
setConnections(next);
try {
await Promise.all([
fetch(`/api/providers/${next[i1].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: i1 }) }),
fetch(`/api/providers/${next[i2].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: i2 }) }),
]);
} catch { await fetch_(); }
};
const handleDelete = async (id) => {
setConfirmState({
title: "Delete Connection",
message: "Delete this connection?",
onConfirm: async () => {
setConfirmState(null);
try {
const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
if (res.ok) setConnections((prev) => prev.filter((c) => c.id !== id));
} catch (e) { console.log("delete error:", e); }
}
});
};
const handleToggleActive = async (id, isActive) => {
try {
const res = await fetch(`/api/providers/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive }) });
if (res.ok) setConnections((prev) => prev.map((c) => c.id === id ? { ...c, isActive } : c));
} catch (e) { console.log("toggle error:", e); }
};
const handleUpdateProxy = async (connId, proxyPoolId) => {
try {
const res = await fetch(`/api/providers/${connId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyPoolId: proxyPoolId || null }) });
if (res.ok) setConnections((prev) => prev.map((c) => c.id === connId ? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } } : c));
} catch (e) { console.log("proxy error:", e); }
};
const handleSaveApiKey = async (formData) => {
try {
const res = await fetch("/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: providerId, ...formData }) });
if (res.ok) { await fetch_(); setShowAddModal(false); }
} catch (e) { console.log("save apikey error:", e); }
};
const handleUpdateConnection = async (formData) => {
try {
const res = await fetch(`/api/providers/${selectedConnection.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData) });
if (res.ok) { await fetch_(); setShowEditModal(false); }
} catch (e) { console.log("update connection error:", e); }
};
if (loading) return ;
return (
<>
{connections.length === 0 ? (
No connections yet
) : (
<>
{connections.map((conn, idx) => (
handleSwapPriority(idx, idx - 1)}
onMoveDown={() => handleSwapPriority(idx, idx + 1)}
onToggleActive={(isActive) => handleToggleActive(conn.id, isActive)}
onUpdateProxy={(poolId) => handleUpdateProxy(conn.id, poolId)}
onEdit={() => { setSelectedConnection(conn); setShowEditModal(true); }}
onDelete={() => handleDelete(conn.id)}
/>
))}
>
)}
setShowAddModal(false)}
/>
setShowEditModal(false)}
/>
{/* Confirm Modal */}
setConfirmState(null)}
onConfirm={confirmState?.onConfirm}
title={confirmState?.title || "Confirm"}
message={confirmState?.message}
variant="danger"
/>
>
);
}
ConnectionsCard.propTypes = {
providerId: PropTypes.string.isRequired,
isOAuth: PropTypes.bool,
};