Spaces:
Runtime error
Runtime error
File size: 5,270 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | "use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { Button, Card } from "@/shared/components";
import { useNotificationStore } from "@/store/notificationStore";
type CooldownItem = {
provider: string;
model: string;
reason: string;
remainingMs: number;
unavailableSince: string;
};
function formatRemaining(ms: number): string {
const totalSec = Math.max(0, Math.ceil(ms / 1000));
const min = Math.floor(totalSec / 60);
const sec = totalSec % 60;
return `${min}m ${sec}s`;
}
export default function ModelCooldownsCard() {
const t = useTranslations("settings");
const notify = useNotificationStore();
const [items, setItems] = useState<CooldownItem[]>([]);
const [loading, setLoading] = useState(true);
const [busyKey, setBusyKey] = useState<string | null>(null);
const load = useCallback(async () => {
try {
const res = await fetch("/api/resilience/model-cooldowns", { cache: "no-store" });
const json = await res.json();
if (!res.ok) throw new Error(json?.error || `HTTP ${res.status}`);
setItems(Array.isArray(json.items) ? json.items : []);
} catch (error) {
notify.error(error instanceof Error ? error.message : "Failed to load cooldowns");
} finally {
setLoading(false);
}
}, [notify]);
useEffect(() => {
void load();
const timer = setInterval(() => {
void load();
}, 5000);
return () => clearInterval(timer);
}, [load]);
const clearOne = useCallback(
async (provider: string, model: string) => {
const key = `${provider}::${model}`;
setBusyKey(key);
try {
const res = await fetch("/api/resilience/model-cooldowns", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, model }),
});
const json = await res.json();
if (!res.ok) throw new Error(json?.error || `HTTP ${res.status}`);
notify.success(`Model reactivated: ${provider}/${model}`);
await load();
} catch (error) {
notify.error(error instanceof Error ? error.message : "Failed to clear cooldown");
} finally {
setBusyKey(null);
}
},
[load, notify]
);
const clearAll = useCallback(async () => {
setBusyKey("ALL");
try {
const res = await fetch("/api/resilience/model-cooldowns", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ all: true }),
});
const json = await res.json();
if (!res.ok) throw new Error(json?.error || `HTTP ${res.status}`);
notify.success("All models in cooldown have been reactivated.");
await load();
} catch (error) {
notify.error(error instanceof Error ? error.message : "Failed to clear cooldowns");
} finally {
setBusyKey(null);
}
}, [load, notify]);
const hasItems = items.length > 0;
const sorted = useMemo(() => [...items].sort((a, b) => b.remainingMs - a.remainingMs), [items]);
return (
<Card className="p-6">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-bold text-text-main">{t("modelCooldownsTitle")}</h2>
<p className="mt-1 text-sm text-text-muted">
Models temporarily isolated after a failure. When the cooldown expires they come back
automatically.
</p>
</div>
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={() => void load()} disabled={loading}>
Refresh
</Button>
<Button
size="sm"
variant="primary"
onClick={() => void clearAll()}
disabled={!hasItems || busyKey === "ALL"}
>
Reactivate all
</Button>
</div>
</div>
<div className="mt-4 space-y-2">
{loading ? (
<p className="text-sm text-text-muted">Loading...</p>
) : !hasItems ? (
<p className="text-sm text-text-muted">{t("modelCooldownsEmpty")}</p>
) : (
sorted.map((item) => {
const rowKey = `${item.provider}::${item.model}`;
return (
<div
key={rowKey}
className="rounded-lg border border-border bg-bg-subtle px-3 py-2 flex items-center justify-between gap-3"
>
<div className="min-w-0">
<p className="text-sm font-medium text-text-main truncate">
{item.provider}/{item.model}
</p>
<p className="text-xs text-text-muted">
reason: {item.reason} • remaining: {formatRemaining(item.remainingMs)}
</p>
</div>
<Button
size="sm"
variant="secondary"
onClick={() => void clearOne(item.provider, item.model)}
disabled={busyKey === rowKey}
>
Reactivate
</Button>
</div>
);
})
)}
</div>
</Card>
);
}
|