import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { WEBHOOK_URL } from '../lib/webrtc' export default function WatchRoom({ sessionId }) { const navigate = useNavigate() const chatEndRef = useRef(null) const [messages, setMessages] = useState([]) const [connected, setConnected] = useState(false) const [copied, setCopied] = useState(false) const watchUrl = `${window.location.origin}${window.location.pathname}#/watch/${sessionId}` useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) useEffect(() => { const es = new EventSource(`${WEBHOOK_URL}/events?session=${sessionId}`) es.onopen = () => setConnected(true) es.onmessage = e => { try { const data = JSON.parse(e.data) const responses = data.responses || [] const msgs = [] // User transcript — non-streaming entry with out.user_input const userEntry = responses.find(r => !r.streaming && r.out?.user_input) if (userEntry) { const text = userEntry.out.user_input .replace(/[\s\S]*?<\/turn-visual-context>/g, '') .replace(/[\s\S]*?<\/non-verbal-elements>/g, '') .trim() if (text) msgs.push({ role: 'user', text }) } // AI text — main LLM only (prev_llm is audio filler, not transcript) const aiRaw = responses .filter(r => r.streaming && r.chunk && r.node === 'llm') .map(r => r.chunk) .join('') const aiText = aiRaw .split(/<\|speak\|>/) .map(seg => seg.replace(/<\|hangup\|>\S*/g, '').trim()) .filter(Boolean) .join(' ') .trim() if (aiText) msgs.push({ role: 'ai', text: aiText }) if (msgs.length) setMessages(prev => [...prev, ...msgs]) } catch {} } es.onerror = () => setConnected(false) return () => es.close() }, [sessionId]) function copyLink() { navigator.clipboard.writeText(watchUrl).then(() => { setCopied(true) setTimeout(() => setCopied(false), 2000) }) } return (
{/* sidebar */} {/* main chat area */}
Live Transcript {messages.length > 0 && ( {messages.length} messages )}
{messages.length === 0 ? (

Waiting for the interview to begin

Messages will appear here in real time as the conversation unfolds.

) : ( messages.map((msg, i) => (
{msg.role === 'user' ? 'Candidate' : 'Interviewer'}
{msg.text}
)) )}
) } const s = { root: { height: '100vh', display: 'flex', flexDirection: 'row', overflow: 'hidden', }, sidebar: { width: 220, flexShrink: 0, borderRight: '1px solid var(--border)', background: 'var(--surface)', display: 'flex', flexDirection: 'column', padding: '24px 18px', gap: 0, }, sideTop: { display: 'flex', flexDirection: 'column', gap: 6, }, back: { alignSelf: 'flex-start', fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--muted)', background: 'none', border: 'none', cursor: 'pointer', letterSpacing: '.08em', padding: 0, marginBottom: 20, }, sideLabel: { fontFamily: 'var(--mono)', fontSize: 8, letterSpacing: '.22em', textTransform: 'uppercase', color: 'var(--muted)', }, sessionId: { fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--text)', wordBreak: 'break-all', lineHeight: 1.5, marginBottom: 8, }, dot: { width: 6, height: 6, borderRadius: '50%', flexShrink: 0, }, connLabel: { fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '.18em', textTransform: 'uppercase', color: 'var(--muted)', marginTop: -4, }, sideDivider: { height: 1, background: 'var(--border)', margin: '20px 0', }, sideBottom: { display: 'flex', flexDirection: 'column', gap: 8, }, shareSectionLabel: { fontFamily: 'var(--mono)', fontSize: 8, letterSpacing: '.22em', textTransform: 'uppercase', color: 'var(--muted)', marginBottom: 2, }, shareUrl: { fontFamily: 'var(--mono)', fontSize: 9, color: 'var(--muted)', wordBreak: 'break-all', lineHeight: 1.5, }, copyBtn: { fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '.12em', padding: '7px 0', borderRadius: 6, border: '1px solid var(--border2)', background: 'transparent', color: 'var(--text)', cursor: 'pointer', width: '100%', textAlign: 'center', transition: 'border-color .15s, color .15s', marginTop: 4, }, copyBtnDone: { color: 'var(--accent)', borderColor: 'var(--accent)', }, main: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', }, header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 28px', height: 56, borderBottom: '1px solid var(--border)', flexShrink: 0, }, headerLabel: { fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '.18em', textTransform: 'uppercase', color: 'var(--muted)', }, msgCount: { fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--muted)', background: 'var(--border2)', padding: '2px 8px', borderRadius: 10, }, chatScroll: { flex: 1, overflowY: 'auto', padding: '24px 28px', display: 'flex', flexDirection: 'column', gap: 16, }, emptyState: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, flex: 1, paddingTop: 80, }, emptyIcon: { fontSize: 32, color: 'var(--muted)', opacity: .4, }, emptyTitle: { fontFamily: 'var(--sans)', fontSize: 15, fontWeight: 600, color: 'var(--text)', opacity: .6, }, emptyHint: { fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--muted)', textAlign: 'center', maxWidth: 360, lineHeight: 1.6, letterSpacing: '.06em', }, msgRow: { display: 'flex', flexDirection: 'column', gap: 5, maxWidth: 640, }, msgRowUser: { alignSelf: 'flex-end', alignItems: 'flex-end' }, msgRowAi: { alignSelf: 'flex-start', alignItems: 'flex-start' }, role: { fontFamily: 'var(--mono)', fontSize: 8, letterSpacing: '.2em', textTransform: 'uppercase', }, bubble: { fontSize: 13, lineHeight: 1.6, padding: '10px 14px', borderRadius: 12, wordBreak: 'break-word', }, bubbleUser: { background: 'rgba(200,240,96,.08)', border: '1px solid rgba(200,240,96,.18)', color: 'var(--text)', borderBottomRightRadius: 3, }, bubbleAi: { background: 'var(--surface)', border: '1px solid var(--border2)', color: 'var(--text)', borderBottomLeftRadius: 3, }, }