diff --git a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js new file mode 100644 index 0000000000..7daa1f4f6c --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js @@ -0,0 +1,194 @@ +"use client"; + +/** + * ModelAvailabilityBadge — compact inline status indicator + * + * Replaces the full ModelAvailabilityPanel card with a small badge + * that shows green when all models are operational, or amber/red + * when there are issues, with a hover popover for details. + */ + +import { useState, useEffect, useCallback, useRef } from "react"; +import { Button } from "@/shared/components"; +import { useNotificationStore } from "@/store/notificationStore"; + +const STATUS_CONFIG = { + available: { icon: "check_circle", color: "#22c55e", label: "Available" }, + cooldown: { icon: "schedule", color: "#f59e0b", label: "Cooldown" }, + unavailable: { icon: "error", color: "#ef4444", label: "Unavailable" }, + unknown: { icon: "help", color: "#6b7280", label: "Unknown" }, +}; + +export default function ModelAvailabilityBadge() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [expanded, setExpanded] = useState(false); + const [clearing, setClearing] = useState(null); + const ref = useRef(null); + const notify = useNotificationStore(); + + const fetchStatus = useCallback(async () => { + try { + const res = await fetch("/api/models/availability"); + if (res.ok) { + const json = await res.json(); + setData(json); + } + } catch { + // silent fail — will retry + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchStatus(); + const interval = setInterval(fetchStatus, 30000); + return () => clearInterval(interval); + }, [fetchStatus]); + + // Close popover on outside click + useEffect(() => { + const handleClick = (e) => { + if (ref.current && !ref.current.contains(e.target)) { + setExpanded(false); + } + }; + if (expanded) document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [expanded]); + + const handleClearCooldown = async (provider, model) => { + setClearing(`${provider}:${model}`); + try { + const res = await fetch("/api/models/availability", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "clearCooldown", provider, model }), + }); + if (res.ok) { + notify.success(`Cooldown cleared for ${model}`); + await fetchStatus(); + } else { + notify.error("Failed to clear cooldown"); + } + } catch { + notify.error("Failed to clear cooldown"); + } finally { + setClearing(null); + } + }; + + if (loading) return null; + + const models = data?.models || []; + const unavailableCount = + data?.unavailableCount || models.filter((m) => m.status !== "available").length; + const isHealthy = unavailableCount === 0; + + // Group unhealthy models by provider + const byProvider = {}; + models.forEach((m) => { + if (m.status === "available") return; + const key = m.provider || "unknown"; + if (!byProvider[key]) byProvider[key] = []; + byProvider[key].push(m); + }); + + return ( +
+ All models are responding normally. +
+ ) : ( ++ {provider} +
+Interactive diagram visible on desktop