/** * StatusPanel.jsx — Live pipeline status display. * * Subscribes to /social/orchestrator/state (std_msgs/String, JSON payload): * { * state: "idle"|"listening"|"thinking"|"speaking"|"throttled", * gpu_free_mb: number, * gpu_total_mb: number, * latency: { * wakeword_to_transcript: { mean_ms, p95_ms, n }, * transcript_to_llm: { mean_ms, p95_ms, n }, * llm_to_tts: { mean_ms, p95_ms, n }, * end_to_end: { mean_ms, p95_ms, n } * }, * persona_name: string, * active_person: string * } */ import { useEffect, useState } from 'react'; const STATE_CONFIG = { idle: { label: 'IDLE', color: 'text-gray-400', bg: 'bg-gray-800', border: 'border-gray-600', pulse: '' }, listening: { label: 'LISTENING', color: 'text-blue-300', bg: 'bg-blue-950', border: 'border-blue-600', pulse: 'pulse-blue' }, thinking: { label: 'THINKING', color: 'text-amber-300', bg: 'bg-amber-950', border: 'border-amber-600', pulse: 'pulse-amber' }, speaking: { label: 'SPEAKING', color: 'text-green-300', bg: 'bg-green-950', border: 'border-green-600', pulse: 'pulse-green' }, throttled: { label: 'THROTTLED', color: 'text-red-300', bg: 'bg-red-950', border: 'border-red-600', pulse: 'pulse-amber' }, }; function LatencyRow({ label, stat }) { if (!stat || stat.n === 0) return null; const warn = stat.mean_ms > 500; const crit = stat.mean_ms > 1500; const cls = crit ? 'text-red-400' : warn ? 'text-amber-400' : 'text-cyan-400'; return (
{label}
{Math.round(stat.mean_ms)}ms p95:{Math.round(stat.p95_ms)}ms
); } export function StatusPanel({ subscribe }) { const [status, setStatus] = useState(null); const [lastUpdate, setLastUpdate] = useState(null); useEffect(() => { const unsub = subscribe( '/social/orchestrator/state', 'std_msgs/String', (msg) => { try { const data = JSON.parse(msg.data); setStatus(data); setLastUpdate(Date.now()); } catch { // ignore parse errors } } ); return unsub; }, [subscribe]); const state = status?.state ?? 'idle'; const cfg = STATE_CONFIG[state] ?? STATE_CONFIG.idle; const gpuFree = status?.gpu_free_mb ?? 0; const gpuTotal = status?.gpu_total_mb ?? 1; const gpuUsed = gpuTotal - gpuFree; const gpuPct = Math.round((gpuUsed / gpuTotal) * 100); const gpuWarn = gpuPct > 80; const lat = status?.latency ?? {}; const stale = lastUpdate && Date.now() - lastUpdate > 5000; return (
{/* Pipeline State */}
PIPELINE STATE
{cfg.label}
{status?.persona_name && (
Persona: {status.persona_name}
)} {status?.active_person && (
Talking to: {status.active_person}
)}
{stale && (
STALE
)} {!status && (
No signal
)}
{/* GPU Memory */}
GPU MEMORY
{gpuTotal > 1 ? ( <>
{Math.round(gpuUsed)} MB used {Math.round(gpuTotal)} MB total
90 ? '#ef4444' : gpuPct > 75 ? '#f59e0b' : '#06b6d4', }} />
{gpuPct}%
) : (
No GPU data
)}
{/* Latency */}
LATENCY
{Object.keys(lat).length > 0 ? (
) : (
No latency data yet
)}
); }