/* Vooice — Outbound Call with scrubber + phases. New: play/pause, scrubber, "are you AI?" honesty handoff phase, animated read-back. */ const PHASES = [ { id: 'dial', label: 'Dialing', start: 0, color: '#6A655C' }, { id: 'connect', label: 'Connected', start: 2.4, color: '#0E0E0F' }, { id: 'request', label: 'Stating purpose', start: 6.4, color: '#0E0E0F' }, { id: 'options', label: 'Hearing options', start: 13.5,color: '#0E0E0F' }, { id: 'readback', label: 'Slot read-back', start: 20.0,color: '#F04E23' }, { id: 'honesty', label: 'Are you AI?', start: 30.0,color: '#F04E23' }, { id: 'handoff', label: 'Dialing user in', start: 36.0,color: '#F04E23' }, { id: 'resume', label: 'Resumed', start: 44.0,color: '#7A8A6E' }, { id: 'wrap', label: 'Wrapping up', start: 52.0,color: '#7A8A6E' }, { id: 'closed', label: 'Closed', start: 58.0,color: '#7A8A6E' }, ]; const SCRIPT_LINES_V2 = [ { t: 0, who: 'sys', text: 'Outbound call placed → Bayview Hair Studio · +1 (415) 555-0142' }, { t: 2.4, who: 'sys', text: 'Connected · 3 rings' }, { t: 3.0, who: 'callee', text: 'Bayview Hair Studio, this is Marcus, how can I help you?' }, { t: 6.4, who: 'vooice', text: "Hi Marcus — this is Vooice, calling on behalf of Jordan Reyes. Jordan would like to book a haircut sometime this week, ideally Wednesday or Thursday afternoon." }, { t: 13.5, who: 'callee', text: 'Sure, let me check… we have Wednesday at 3:30 with Lina, or Thursday at 2 with Marcus — that\u2019s me.' }, { t: 20.0, who: 'vooice', text: "Thursday at 2 with you works for Jordan's calendar. Could you confirm that's a 45-minute slot, and you're on the books as Marcus Wu?" }, { t: 26.5, who: 'callee', text: 'Yep — wait, hold on. Are you, like, a real person?' }, { t: 30.0, who: 'vooice', text: "Honest answer: I'm an AI assistant — Vooice — calling for Jordan. If it'd help, I can dial Jordan onto the line right now to confirm I'm working for them." }, { t: 35.0, who: 'callee', text: "Yeah, please — that'd be great." }, { t: 36.0, who: 'sys', text: 'Dialing Jordan onto the line…' }, { t: 39.5, who: 'sys', text: 'Jordan joined · 3-way' }, { t: 40.0, who: 'jordan', text: "Hey Marcus, yeah — Vooice is mine, she's good. Just go ahead with what she said." }, { t: 43.5, who: 'callee', text: "Perfect, thanks Jordan. I'll book Thursday 2pm." }, { t: 44.0, who: 'sys', text: 'Jordan dropped off' }, { t: 46.0, who: 'vooice', text: "Thanks Jordan. Marcus — confirming 45 minutes, Marcus Wu, Thursday at 2." }, { t: 52.0, who: 'callee', text: 'All set. Phone for the reminder?' }, { t: 55.0, who: 'vooice', text: "+1 (415) 555-0188 — that's Jordan's direct line. I'll add it to their calendar and send a confirmation." }, { t: 60.0, who: 'callee', text: 'Great, see them Thursday.' }, { t: 62.0, who: 'vooice', text: 'Thanks Marcus, have a good one.' }, ]; const TOTAL_DUR = 65; function CurrentLineV2(tSec) { let cur = SCRIPT_LINES_V2[0]; for (const l of SCRIPT_LINES_V2) if (l.t <= tSec) cur = l; return cur; } function CurrentPhase(tSec) { let cur = PHASES[0]; for (const p of PHASES) if (p.start <= tSec) cur = p; return cur; } function fmtClockV2(s) { const m = Math.floor(s / 60).toString().padStart(2, '0'); const sec = Math.floor(s % 60).toString().padStart(2, '0'); return `${m}:${sec}`; } function OutboundCall({ width = 1320, height = 880, onNavigate }) { const [tSec, setTSec] = React.useState(8); const [playing, setPlaying] = React.useState(true); React.useEffect(() => { if (!playing) return; const id = setInterval(() => setTSec(t => (t + 0.1) % TOTAL_DUR), 100); return () => clearInterval(id); }, [playing]); const line = CurrentLineV2(tSec); const phase = CurrentPhase(tSec); const speaking = tSec < 2.4 ? 'silent' : line.who === 'sys' ? 'silent' : line.who === 'jordan' ? 'callee' : line.who; const showReadback = tSec >= 20 && tSec < 26.5; const honestyMode = tSec >= 26.5 && tSec < 36; const handoffMode = tSec >= 36 && tSec < 44; const threeWay = tSec >= 39.5 && tSec < 44; return (
{/* HEADER — unified AppHeader */} {/* LEFT — CALL SUBJECT */} {/* CENTER */}
{phase.label.toUpperCase()}
{speaking === 'vooice' ? 'Vooice speaking' : speaking === 'callee' ? `${line.who === 'jordan' ? 'Jordan' : 'Marcus'} speaking` : 'Line silent'} {' · Deepgram Nova‑3 · 220ms endpoint'}
LATENCY 312 ms JITTER 6 ms QUALITY ◼◼◼◼◻
{/* Transcript */}
{SCRIPT_LINES_V2.filter(l => l.t <= tSec + 0.05).slice(-5).map((l, i, arr) => ( ))}
{/* Bottom — handoff */}
{/* RIGHT — pending actions */} {/* SCRUBBER */}
{fmtClockV2(tSec)} / {fmtClockV2(TOTAL_DUR)}
{/* FOOTER */}
); } // ── Sub-components ────────────────────────────────────────── function Participant({ name, sub, speaking, accent }) { return (
{speaking && ( )}
{name}
{sub}
); } function BubbleV2({ line, isCurrent, tSec }) { if (line.who === 'sys') { return (
{line.text}
); } const isVoo = line.who === 'vooice'; const isJordan = line.who === 'jordan'; const align = isVoo ? 'flex-start' : 'flex-end'; let text = line.text; if (isCurrent && isVoo) { const since = Math.max(0, tSec - line.t); const targetChars = Math.min(line.text.length, Math.floor(since * 22)); text = line.text.slice(0, targetChars); } const bg = isVoo ? VC_TOKENS.ink : isJordan ? VC_TOKENS.signalSoft : VC_TOKENS.paper2; const fg = isVoo ? VC_TOKENS.paper : VC_TOKENS.ink; return (
{isVoo ? : } {isVoo ? 'VOOICE' : isJordan ? 'JORDAN (DIALED IN)' : 'MARCUS'} · {fmtClockV2(line.t)}
{text} {isCurrent && isVoo && text.length < line.text.length && ( )}
); } function ActionCard({ step, title, status, rows }) { const palette = { pending: { fg: VC_TOKENS.ink3, label: 'PENDING' }, queued: { fg: VC_TOKENS.ink3, label: 'QUEUED' }, ready: { fg: VC_TOKENS.sage, label: 'READY TO WRITE' }, }[status]; return (
{step} {title}
{palette.label}
{rows.map(([k,v]) => (
{k} {v}
))}
); } function SafetyItem({ label, on, off, hot }) { const color = off ? VC_TOKENS.ink3 : hot ? VC_TOKENS.signal : VC_TOKENS.sage; return (
{off ? '×' : '✓'} {label}
); } function HandoffPanelV2({ honestyMode, handoffMode }) { if (handoffMode) { return (
You're being dialed in.
Vooice will keep talking until you pick up. Drop off whenever.
3-WAY
); } if (honestyMode) { return (
Marcus asked if she's AI. She said yes.
She's offering to dial you in. Auto-accept in 3s if no input.
HONESTY
); } return (
Call going smoothly.
If Marcus asks "are you AI?", I'll answer truthfully and offer to dial you in.
AUTO
); } function BigButton({ label, sub, variant = 'ghost' }) { const isSignal = variant === 'signal'; return ( ); } function Stat({ label, value, last }) { return (
{label} {value}
); } // ── Animated popover ───────────────────────────────── function Popover({ visible, title, countdown, body }) { const [render, setRender] = React.useState(visible); const [show, setShow] = React.useState(visible); React.useEffect(() => { if (visible) { setRender(true); requestAnimationFrame(() => setShow(true)); } else { setShow(false); const t = setTimeout(() => setRender(false), 320); return () => clearTimeout(t); } }, [visible]); if (!render) return null; return (
{title} {countdown}
{body}
); } // ── Scrubber with phase markers ────────────────────── function Scrubber({ tSec, dur, setT }) { const trackRef = React.useRef(null); const onClick = e => { const r = trackRef.current.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width; setT(Math.max(0, Math.min(dur, x * dur))); }; return (
{/* Phase blocks */} {PHASES.map((p, i) => { const next = PHASES[i+1]; const start = p.start / dur; const end = (next ? next.start : dur) / dur; return (
{p.label}
); })} {/* Playhead */}
); } window.OutboundCall = OutboundCall;