// ============================================================================ // public/screens/monitor.jsx — Live Call Monitor (Phase 5) // ---------------------------------------------------------------------------- // Connects to GET /api/calls/live (SSE) for real-time active calls. // Shows cards with waveform, status, duration timer, agent info. // ============================================================================ function MonitorScreen() { const [activeCalls, setActive] = React.useState([]); const [connected, setConnected] = React.useState(false); const [error, setError] = React.useState(null); const [stats, setStats] = React.useState(null); const esRef = React.useRef(null); const timerRef = React.useRef(null); // SSE connection React.useEffect(() => { const base = (window.VOAIS_API_BASE || ""); const sess = VoaisAPI.getSession(); if (!sess?.token) return; // SSE doesn't support custom headers easily, so we'll poll instead // (browser EventSource can't set Authorization header) let alive = true; const poll = async () => { if (!alive) return; try { const r = await VoaisAPI.get("/api/calls?status=in_progress&limit=50&page=1"); if (r.ok && r.data?.ok) { // Also get initiated + ringing const r2 = await VoaisAPI.get("/api/calls?status=initiated&limit=50&page=1"); const r3 = await VoaisAPI.get("/api/calls?status=ringing&limit=50&page=1"); const r4 = await VoaisAPI.get("/api/calls?status=answered&limit=50&page=1"); const all = [ ...(r.data.calls || []), ...(r2.ok ? r2.data.calls || [] : []), ...(r3.ok ? r3.data.calls || [] : []), ...(r4.ok ? r4.data.calls || [] : []), ]; // Deduplicate by id const seen = new Set(); const unique = all.filter(c => { if (seen.has(c.id)) return false; seen.add(c.id); return true; }); setActive(unique); setConnected(true); } } catch (e) { console.warn("[monitor] poll error:", e.message); } }; poll(); const interval = setInterval(poll, 3000); // Also load stats VoaisAPI.get("/api/calls/stats").then(r => { if (r.ok && r.data?.ok) setStats(r.data.stats); }); return () => { alive = false; clearInterval(interval); }; }, []); return (
{/* Header KPIs */}
0 ? "var(--accent-soft)" : "var(--surface-2)", borderRadius: 12, textAlign: "center" }}>
0 ? "var(--accent)" : "var(--ink-3)" }} className="tabular"> {activeCalls.length}
Active calls
{stats && ( <>
{stats.today.total}
Calls today
{stats.today.answered}
Answered
{stats.today.minutes}m
Minutes
)}
{/* Connection status */}
{connected ? "Live — refreshing every 3s" : "Connecting..."}
{/* Active calls grid */} {activeCalls.length === 0 && (

No active calls

When campaigns start dialing, active calls will appear here in real-time.

)} {activeCalls.length > 0 && (
{activeCalls.map(c => ( ))}
)}
); } // ── Live Call Card ────────────────────────────────────────────────────── function LiveCallCard({ call }) { // Live duration timer const [elapsed, setElapsed] = React.useState(0); React.useEffect(() => { if (!call.started_at) return; const start = new Date(call.started_at).getTime(); const tick = () => setElapsed(Math.floor((Date.now() - start) / 1000)); tick(); const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, [call.started_at]); const statusColor = call.status === "in_progress" || call.status === "answered" ? "var(--ok)" : call.status === "ringing" ? "var(--warn)" : "var(--ink-3)"; const statusLabel = call.status === "in_progress" ? "Talking" : call.status === "answered" ? "Connected" : call.status === "ringing" ? "Ringing" : call.status; return ( {/* Accent top */}
{/* Header */}
{call.agent_gradient && (
{(call.agent_name || "?").charAt(0)}
)}
{call.agent_name || "AI Agent"}
{call.number || "—"}
{statusLabel}
{/* Waveform */} {(call.status === "in_progress" || call.status === "answered") && (
)} {/* Footer */}
{formatMonitorDuration(elapsed)}
{call.campaign_name && {call.campaign_name}} {call.direction} {call.mode}
); } function formatMonitorDuration(sec) { const m = Math.floor(sec / 60); const s = sec % 60; return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } window.MonitorScreen = MonitorScreen;