frontendt / src /components /CallRoom.jsx
ayushsinghal1510's picture
Added coaching and hotel
252a04f
Raw
History Blame Contribute Delete
10.2 kB
import { useEffect, useRef, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { WEBHOOK_URL } from '../lib/webrtc'
const TYPE_META = {
university: { label: 'University Call Agent', accent: '#60d0f0', icon: '⬡' },
realestate: { label: 'Real Estate Call Agent', accent: '#f0b060', icon: '⬢' },
coaching: { label: 'Coaching Institute Agent', accent: '#b088f0', icon: '◆' },
medical: { label: 'Medical OPD Agent', accent: '#5fd0a0', icon: '✚' },
hotel: { label: 'Hotel Concierge Agent', accent: '#f0a0c0', icon: '❖' },
}
export default function CallRoom({ type, sessionId }) {
const navigate = useNavigate()
const { state } = useLocation()
const chatEndRef = useRef(null)
const [messages, setMessages] = useState([])
const [connected, setConnected] = useState(false)
const [copied, setCopied] = useState(false)
const [elapsed, setElapsed] = useState(0)
const meta = TYPE_META[type] || TYPE_META.university
const phone = state?.phone || ''
const agentName = state?.agentName || 'Agent'
const watchUrl = `${window.location.origin}${window.location.pathname}#/call/${type}/${sessionId}`
useEffect(() => {
const t = setInterval(() => setElapsed(s => s + 1), 1000)
return () => clearInterval(t)
}, [])
const fmtTime = s => {
const m = Math.floor(s / 60).toString().padStart(2, '0')
const ss = (s % 60).toString().padStart(2, '0')
return `${m}:${ss}`
}
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)
console.log('[sse] raw event:', JSON.stringify(data).slice(0, 400))
const responses = data.responses || []
const msgs = []
// User transcript
const userEntry = responses.find(r => !r.streaming && r.out?.user_input)
if (userEntry) {
const text = userEntry.out.user_input
.replace(/<turn-visual-context>[\s\S]*?<\/turn-visual-context>/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 (err) {
console.error('[sse] parse/render error:', err, 'raw:', e.data?.slice(0, 200))
}
}
es.onerror = () => setConnected(false)
return () => es.close()
}, [sessionId])
function copyLink() {
navigator.clipboard.writeText(watchUrl).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
})
}
return (
<div style={s.root}>
{/* topbar */}
<header style={s.topbar}>
<div style={s.topLeft}>
<span style={{ ...s.typeChip, color: meta.accent, borderColor: `${meta.accent}40` }}>
{meta.icon} {meta.label}
</span>
{phone && (
<span style={s.phone}>{phone}</span>
)}
</div>
<div style={s.topRight}>
<span style={s.timer}>{fmtTime(elapsed)}</span>
<div style={{ ...s.liveChip, ...(connected ? s.liveOn : s.liveOff) }}>
<div style={{ ...s.liveDot, background: connected ? 'var(--accent)' : 'var(--muted)', animation: connected ? 'blink .8s step-end infinite' : 'none' }} />
{connected ? 'Live' : 'Waiting'}
</div>
</div>
</header>
{/* body */}
<div style={s.body}>
{/* main chat */}
<main style={s.chatArea}>
<div style={s.chatScroll}>
{messages.length === 0 ? (
<div style={s.emptyState}>
<span style={{ ...s.emptyIcon, color: meta.accent }}>{meta.icon}</span>
<p style={s.emptyTitle}>
{connected ? 'Call connected — waiting for conversation…' : 'Connecting to call…'}
</p>
<p style={s.emptyHint}>
Messages will appear here as the conversation unfolds.
</p>
</div>
) : (
messages.map((msg, i) => (
<div key={i} style={{ ...s.msgRow, ...(msg.role === 'user' ? s.msgRowUser : s.msgRowAi) }}>
<span style={{ ...s.role, color: msg.role === 'user' ? 'var(--user-ring)' : meta.accent }}>
{msg.role === 'user' ? 'Caller' : agentName}
</span>
<div style={{ ...s.bubble, ...(msg.role === 'user' ? s.bubbleUser : { ...s.bubbleAi, borderColor: `${meta.accent}30` }) }}>
{msg.text}
</div>
</div>
))
)}
<div ref={chatEndRef} />
</div>
</main>
{/* sidebar */}
<aside style={s.sidebar}>
<div style={s.sideSection}>
<p style={s.sideLabel}>Session</p>
<p style={s.sideValue}>{sessionId.slice(0, 8)}…</p>
</div>
<div style={s.sideDivider} />
<div style={s.sideSection}>
<p style={s.sideLabel}>Share view</p>
<p style={s.shareUrl}>{watchUrl}</p>
<button
style={{ ...s.copyBtn, borderColor: copied ? meta.accent : 'var(--border2)', color: copied ? meta.accent : 'var(--text)' }}
onClick={copyLink}
>
{copied ? 'Copied!' : 'Copy link'}
</button>
</div>
<div style={s.sideDivider} />
<button style={s.homeBtn} onClick={() => navigate('/')}>
← New session
</button>
</aside>
</div>
</div>
)
}
const s = {
root: { height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' },
topbar: {
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0 28px', height: 56, borderBottom: '1px solid var(--border)', flexShrink: 0,
},
topLeft: { display: 'flex', alignItems: 'center', gap: 12 },
topRight: { display: 'flex', alignItems: 'center', gap: 12 },
typeChip: {
fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '.12em',
textTransform: 'uppercase', padding: '3px 9px', borderRadius: 20, border: '1px solid',
},
phone: { fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--muted)' },
timer: { fontFamily: 'var(--mono)', fontSize: 12, color: 'var(--muted)', letterSpacing: '.1em' },
liveChip: {
display: 'flex', alignItems: 'center', gap: 6,
fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '.14em', textTransform: 'uppercase',
padding: '4px 10px', borderRadius: 20, border: '1px solid var(--border2)',
},
liveOn: { color: 'var(--accent)', borderColor: 'rgba(200,240,96,.3)' },
liveOff: { color: 'var(--muted)' },
liveDot: { width: 5, height: 5, borderRadius: '50%', flexShrink: 0 },
body: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'row', overflow: 'hidden' },
chatArea: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' },
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, 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',
color: 'var(--text)', borderBottomLeftRadius: 3,
},
sidebar: {
width: 220, flexShrink: 0, borderLeft: '1px solid var(--border)',
background: 'var(--surface)', display: 'flex', flexDirection: 'column',
padding: '24px 18px', gap: 0,
},
sideSection: { display: 'flex', flexDirection: 'column', gap: 6 },
sideLabel: {
fontFamily: 'var(--mono)', fontSize: 8, letterSpacing: '.22em',
textTransform: 'uppercase', color: 'var(--muted)',
},
sideValue: { fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--text)' },
sideDivider: { height: 1, background: 'var(--border)', margin: '20px 0' },
shareUrl: {
fontFamily: 'var(--mono)', fontSize: 9, color: 'var(--muted)',
wordBreak: 'break-all', lineHeight: 1.5, marginBottom: 8,
},
copyBtn: {
fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '.12em',
padding: '7px 0', borderRadius: 6, border: '1px solid',
background: 'transparent', cursor: 'pointer', width: '100%',
textAlign: 'center', transition: 'border-color .15s, color .15s',
},
homeBtn: {
fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--muted)',
background: 'none', border: 'none', cursor: 'pointer',
letterSpacing: '.08em', padding: 0, textAlign: 'left', marginTop: 'auto',
},
}