File size: 8,003 Bytes
4cc00df af6cd33 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df 2f00f43 af6cd33 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df 2f00f43 4cc00df d59a97d 4cc00df 2f00f43 4cc00df d59a97d 4cc00df d59a97d 4cc00df 2f00f43 4cc00df d59a97d 4cc00df 2f00f43 | 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | import { useEffect, useRef, useState } from "react";
import { forgesight } from "@/lib/api";
import { Activity, Cpu, Zap, Thermometer, BarChart3, Database } from "lucide-react";
/* ββ helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
function arc(pct, r = 38) {
const clamp = Math.min(100, Math.max(0, pct));
const angle = (clamp / 100) * 270 - 135; // 270Β° sweep, start -135Β°
const rad = (a) => (a * Math.PI) / 180;
const x = 50 + r * Math.cos(rad(angle));
const y = 50 + r * Math.sin(rad(angle));
const large = clamp > 50 ? 1 : 0;
const sx = 50 + r * Math.cos(rad(-135));
const sy = 50 + r * Math.sin(rad(-135));
return `M ${sx} ${sy} A ${r} ${r} 0 ${large} 1 ${x} ${y}`;
}
function ArcGauge({ pct = 0, label, value, icon: Icon, color = "#ED1C24" }) {
const prev = useRef(pct);
const [displayed, setDisplayed] = useState(pct);
useEffect(() => {
// smooth interpolation
const start = prev.current;
const end = pct;
const dur = 600;
const t0 = performance.now();
let raf;
const step = (now) => {
const progress = Math.min((now - t0) / dur, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setDisplayed(start + (end - start) * eased);
if (progress < 1) raf = requestAnimationFrame(step);
else prev.current = end;
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [pct]);
const bgPath = arc(100);
const fgPath = arc(displayed);
return (
<div className="flex flex-col items-center gap-1 min-w-0">
<div className="relative w-20 h-20 shrink-0">
<svg viewBox="0 0 100 100" className="w-full h-full -rotate-90" style={{ transform: "rotate(0deg)" }}>
{/* Track */}
<path d={bgPath} fill="none" stroke="#27272A" strokeWidth="8" strokeLinecap="round" />
{/* Value */}
<path
d={fgPath}
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
style={{ filter: `drop-shadow(0 0 4px ${color}66)` }}
/>
</svg>
{/* Center icon */}
<div className="absolute inset-0 flex items-center justify-center">
{Icon && <Icon className="w-4 h-4" style={{ color }} />}
</div>
</div>
<div className="font-mono text-sm text-white tabular-nums text-center leading-tight">
{value}
</div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider text-center">
{label}
</div>
</div>
);
}
function StatRow({ label, value, pct, color = "#ED1C24" }) {
return (
<div>
<div className="flex items-baseline justify-between mb-1">
<span className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider">{label}</span>
<span className="font-mono text-xs text-white tabular-nums">{value}</span>
</div>
<div className="h-1 w-full bg-white/5 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-700 ease-out"
style={{
width: `${Math.max(2, Math.min(100, pct))}%`,
background: `linear-gradient(90deg, ${color}99, ${color})`,
boxShadow: `0 0 6px ${color}66`,
}}
/>
</div>
</div>
);
}
/* ββ main component ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
export default function TelemetryWidget() {
const [t, setT] = useState(null);
const [blink, setBlink] = useState(false);
useEffect(() => {
let alive = true;
const tick = async () => {
try {
const data = await forgesight.getTelemetry();
if (alive) {
setT(data);
setBlink((b) => !b);
}
} catch {}
};
tick();
const id = setInterval(tick, 2000);
return () => { alive = false; clearInterval(id); };
}, []);
const status = t?.status ?? "β";
const isLive = status === "Connected";
const isLimited = status === "Limited";
const statusColor = isLive ? "#10B981" : isLimited ? "#F59E0B" : "#71717A";
const statusLabel = isLive ? "LIVE" : isLimited ? "LIMITED" : "OFFLINE";
const vramPct = t ? (t.vram_used_gb / t.vram_total_gb) * 100 : 0;
const tokensPct = t ? (t.tokens_per_sec / 4000) * 100 : 0;
const powerPct = t ? (t.power_watts / 750) * 100 : 0;
return (
<div
className="border bg-[#0d0d10] p-5 fs-corners"
style={{
borderColor: isLive ? "#10B98133" : "#27272A",
boxShadow: isLive ? "0 0 20px #10B98111" : "none",
transition: "border-color 0.6s, box-shadow 0.6s",
}}
data-testid="telemetry-widget"
>
{/* Header */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2">
<Activity className="w-3.5 h-3.5" style={{ color: statusColor }} />
<span className="fs-label">Live Telemetry</span>
</div>
<div className="flex items-center gap-2">
{/* Pulse dot */}
<span
className="w-2 h-2 rounded-full inline-block"
style={{
background: statusColor,
boxShadow: isLive ? `0 0 6px ${statusColor}` : "none",
opacity: isLive && blink ? 1 : isLive ? 0.5 : 0.3,
transition: "opacity 0.5s",
}}
/>
<span
className="font-mono text-[10px] tracking-widest px-2 py-0.5 border rounded"
style={{ color: statusColor, borderColor: `${statusColor}44`, background: `${statusColor}11` }}
data-testid="telemetry-status-badge"
>
{statusLabel}
</span>
</div>
</div>
{/* Device */}
<div className="font-mono text-[10px] text-zinc-600 mb-4 flex items-center justify-between">
<span>{t?.device ?? "AMD Instinct MI300X"}</span>
{t?.persistence && (
<span className="flex items-center gap-1 text-zinc-600">
<Database className="w-2.5 h-2.5" />
{t.persistence}
</span>
)}
</div>
{/* Arc gauges row */}
<div className="grid grid-cols-3 gap-2 mb-5 pb-5 border-b border-white/5">
<ArcGauge
pct={t?.gpu_util_pct ?? 0}
label="GPU Util"
value={t?.gpu_util_pct != null ? `${t.gpu_util_pct.toFixed(0)}%` : "β"}
icon={Cpu}
color={isLive ? "#ED1C24" : "#3F3F46"}
/>
<ArcGauge
pct={vramPct}
label="VRAM"
value={t?.vram_used_gb != null ? `${t.vram_used_gb.toFixed(0)}G` : "β"}
icon={BarChart3}
color={isLive ? "#F59E0B" : "#3F3F46"}
/>
<ArcGauge
pct={t?.temp_c ? (t.temp_c / 90) * 100 : 0}
label="Temp"
value={t?.temp_c != null ? `${t.temp_c.toFixed(0)}Β°C` : "β"}
icon={Thermometer}
color={isLive ? "#06B6D4" : "#3F3F46"}
/>
</div>
{/* Bar stats */}
<div className="space-y-3">
<StatRow
label="Tokens/sec"
value={t ? t.tokens_per_sec.toLocaleString() : "β"}
pct={tokensPct}
color={isLive ? "#10B981" : "#3F3F46"}
/>
<StatRow
label="Power Draw"
value={t ? `${t.power_watts} W` : "β"}
pct={powerPct}
color={isLive ? "#ED1C24" : "#3F3F46"}
/>
<StatRow
label="VRAM Used"
value={t?.vram_used_gb != null ? `${t.vram_used_gb.toFixed(1)} / ${t.vram_total_gb} GB` : "β"}
pct={vramPct}
color={isLive ? "#F59E0B" : "#3F3F46"}
/>
</div>
</div>
);
}
|