feat: combo enable/disable toggle + provider toggle always visible

1. Combos page: added Toggle to each ComboCard — disable/enable combos
   with optimistic UI update. Disabled combos show opacity-50.
2. /v1/models: disabled combos (isActive=false) are excluded from the
   model list.
3. Providers page: toggle is now always visible instead of hover-only.
This commit is contained in:
diegosouzapw
2026-02-20 02:53:14 -03:00
parent 87c7c92ddd
commit 0deeaf5ffb
3 changed files with 76 additions and 44 deletions

View File

@@ -6,6 +6,7 @@ import {
Button,
Modal,
Input,
Toggle,
CardSkeleton,
ModelSelectModal,
ProxyConfigModal,
@@ -170,6 +171,25 @@ export default function CombosPage() {
}
};
const handleToggleCombo = async (combo) => {
const newActive = combo.isActive === false ? true : false;
// Optimistic update
setCombos((prev) => prev.map((c) => (c.id === combo.id ? { ...c, isActive: newActive } : c)));
try {
await fetch(`/api/combos/${combo.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isActive: newActive }),
});
} catch (error) {
// Revert on error
setCombos((prev) =>
prev.map((c) => (c.id === combo.id ? { ...c, isActive: !newActive } : c))
);
notify.error("Failed to toggle combo");
}
};
if (loading) {
return (
<div className="flex flex-col gap-6">
@@ -219,6 +239,7 @@ export default function CombosPage() {
testing={testingCombo === combo.name}
onProxy={() => setProxyTargetCombo(combo)}
hasProxy={!!proxyConfig?.combos?.[combo.id]}
onToggle={() => handleToggleCombo(combo)}
/>
))}
</div>
@@ -287,12 +308,14 @@ function ComboCard({
testing,
onProxy,
hasProxy,
onToggle,
}) {
const strategy = combo.strategy || "priority";
const models = combo.models || [];
const isDisabled = combo.isActive === false;
return (
<Card padding="sm" className="group">
<Card padding="sm" className={`group ${isDisabled ? "opacity-50" : ""}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
{/* Icon */}
@@ -386,47 +409,55 @@ function ComboCard({
</div>
{/* Actions */}
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button
onClick={onTest}
disabled={testing}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-emerald-500 transition-colors"
title="Test combo"
>
<span
className={`material-symbols-outlined text-[16px] ${testing ? "animate-spin" : ""}`}
<div className="flex items-center gap-1 shrink-0">
<Toggle
size="sm"
checked={!isDisabled}
onChange={onToggle}
title={isDisabled ? "Enable combo" : "Disable combo"}
/>
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={onTest}
disabled={testing}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-emerald-500 transition-colors"
title="Test combo"
>
{testing ? "progress_activity" : "play_arrow"}
</span>
</button>
<button
onClick={onDuplicate}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
title="Duplicate"
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
<button
onClick={onProxy}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
title="Proxy configuration"
>
<span className="material-symbols-outlined text-[16px]">vpn_lock</span>
</button>
<button
onClick={onEdit}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
title="Edit"
>
<span className="material-symbols-outlined text-[16px]">edit</span>
</button>
<button
onClick={onDelete}
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
title="Delete"
>
<span className="material-symbols-outlined text-[16px]">delete</span>
</button>
<span
className={`material-symbols-outlined text-[16px] ${testing ? "animate-spin" : ""}`}
>
{testing ? "progress_activity" : "play_arrow"}
</span>
</button>
<button
onClick={onDuplicate}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
title="Duplicate"
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
<button
onClick={onProxy}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
title="Proxy configuration"
>
<span className="material-symbols-outlined text-[16px]">vpn_lock</span>
</button>
<button
onClick={onEdit}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
title="Edit"
>
<span className="material-symbols-outlined text-[16px]">edit</span>
</button>
<button
onClick={onDelete}
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
title="Delete"
>
<span className="material-symbols-outlined text-[16px]">delete</span>
</button>
</div>
</div>
</div>
</Card>

View File

@@ -534,7 +534,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
e.stopPropagation();
onToggle(!allDisabled ? false : true);
}}
className="opacity-0 group-hover:opacity-100 transition-opacity"
className=""
>
<Toggle
size="sm"
@@ -668,7 +668,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
e.stopPropagation();
onToggle(!allDisabled ? false : true);
}}
className="opacity-0 group-hover:opacity-100 transition-opacity"
className=""
>
<Toggle
size="sm"

View File

@@ -131,8 +131,9 @@ export async function GET() {
const models = [];
const timestamp = Math.floor(Date.now() / 1000);
// Add combos first (they appear at the top)
// Add combos first (they appear at the top) — only active ones
for (const combo of combos) {
if (combo.isActive === false) continue;
models.push({
id: combo.name,
object: "model",