frontendt / src /components /CallSetup.jsx
ayushsinghal1510's picture
Added coaching and hotel
252a04f
Raw
History Blame Contribute Delete
8.87 kB
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { WEBHOOK_URL } from '../lib/webrtc'
import VoiceSettings from './VoiceSettings'
import { buildVoiceCustoms } from '../lib/voiceOptions'
const CALL_META = {
university: {
label: 'University Call Agent',
tagline: 'Admissions',
accent: '#60d0f0',
icon: '⬡',
description: 'AI admissions counsellor Sneha will call the number and guide the prospective student through the admissions process.',
agentName: 'Sneha',
defaultBrand: 'NIMS University',
},
realestate: {
label: 'Real Estate Call Agent',
tagline: 'Sales',
accent: '#f0b060',
icon: '⬢',
description: 'AI sales executive Riya will call the number and qualify the property buyer through a natural conversation.',
agentName: 'Riya',
defaultBrand: 'Skyline Greens',
},
coaching: {
label: 'Coaching Institute Agent',
tagline: 'Admissions',
accent: '#b088f0',
icon: '◆',
description: 'AI counsellor Aarohi will call the lead and book a scholarship test, counselling, or campus visit through a natural conversation.',
agentName: 'Aarohi',
defaultBrand: 'Bansal Classes',
},
medical: {
label: 'Medical OPD Agent',
tagline: 'Front Desk',
accent: '#5fd0a0',
icon: '✚',
description: 'AI front-desk assistant Meera answers the OPD line, books appointments, and routes queries — with a built-in safety interrupt for emergencies.',
agentName: 'Meera',
defaultBrand: 'Sanjeevani Hospital',
},
hotel: {
label: 'Hotel Concierge Agent',
tagline: 'Pre-arrival',
accent: '#f0a0c0',
icon: '❖',
description: 'AI concierge Kabir calls a confirmed guest before check-in to confirm arrival, capture preferences, and offer relevant add-ons.',
agentName: 'Kabir',
defaultBrand: 'The Grand Rambagh',
},
}
// Server URL — same host as WEBHOOK_URL, just port 7777
const SERVER_URL = WEBHOOK_URL
export default function CallSetup({ type }) {
const navigate = useNavigate()
const meta = CALL_META[type]
const [digits, setDigits] = useState('')
const [brand, setBrand] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [voiceSettings, setVoiceSettings] = useState({
sttProvider: 'soniox',
sttLanguages: ['hi', 'en'],
})
if (!meta) {
navigate('/', { replace: true })
return null
}
function handleDigits(e) {
const val = e.target.value.replace(/\D/g, '').slice(0, 10)
setDigits(val)
setError('')
}
async function handleCall() {
if (digits.length < 10) return setError('Enter a valid 10-digit number')
setError('')
setLoading(true)
const sessionId = crypto.randomUUID()
const phone = `+91${digits}`
try {
const res = await fetch(`${SERVER_URL}/call`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type,
phone,
brand: brand.trim() || undefined,
session_id: sessionId,
voiceSettings: Object.keys(voiceSettings).length ? buildVoiceCustoms(voiceSettings) : null,
}),
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Request failed' }))
setError(err.error || 'Failed to place call')
setLoading(false)
return
}
navigate(`/call/${type}/${sessionId}`, {
state: { phone, agentName: meta.agentName, label: meta.label },
})
} catch (e) {
setError(e.message || 'Network error')
setLoading(false)
}
}
return (
<div style={s.page}>
<div style={{ ...s.glow, background: `radial-gradient(ellipse 60% 50% at 30% 60%, ${meta.accent}08 0%, transparent 70%)` }} />
<div style={s.inner}>
<button style={s.back} onClick={() => navigate('/')}>← back</button>
<div style={{ ...s.iconBox, background: `${meta.accent}15`, color: meta.accent }}>
{meta.icon}
</div>
<h1 style={s.title}>{meta.label}</h1>
<p style={{ ...s.tagline, color: meta.accent }}>{(brand.trim() || meta.defaultBrand)} · {meta.tagline}</p>
<p style={s.desc}>{meta.description}</p>
<label style={s.fieldLabel}>Business name</label>
<input
style={s.brandInput}
type="text"
placeholder={meta.defaultBrand}
value={brand}
onChange={e => setBrand(e.target.value)}
maxLength={60}
/>
<label style={s.fieldLabel}>Phone number</label>
<div style={s.inputGroup}>
<div style={s.prefix}>+91</div>
<input
style={s.input}
type="tel"
inputMode="numeric"
placeholder="98XXXXXXXX"
value={digits}
onChange={handleDigits}
onKeyDown={e => e.key === 'Enter' && !loading && handleCall()}
maxLength={10}
autoFocus
/>
</div>
<VoiceSettings value={voiceSettings} onChange={setVoiceSettings} />
{error && <p style={s.error}>{error}</p>}
<button
style={{ ...s.callBtn, background: meta.accent, opacity: loading ? .6 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
onClick={handleCall}
disabled={loading}
>
{loading ? 'Placing call…' : `Call with ${meta.agentName} →`}
</button>
<p style={s.fine}>
The AI agent will call the number. You'll see the live conversation here.
</p>
</div>
</div>
)
}
const s = {
page: {
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '48px 20px',
position: 'relative',
overflowY: 'auto',
},
glow: {
position: 'fixed', inset: 0,
pointerEvents: 'none',
},
inner: {
width: '100%',
maxWidth: 440,
display: 'flex',
flexDirection: 'column',
position: 'relative',
zIndex: 1,
},
back: {
alignSelf: 'flex-start',
fontFamily: 'var(--mono)',
fontSize: 11,
color: 'var(--muted)',
background: 'none',
border: 'none',
cursor: 'pointer',
letterSpacing: '.08em',
padding: 0,
marginBottom: 32,
},
iconBox: {
width: 56,
height: 56,
borderRadius: 14,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 24,
marginBottom: 20,
},
title: {
fontSize: 'clamp(28px, 5vw, 42px)',
fontWeight: 800,
letterSpacing: '-.03em',
lineHeight: 1.05,
marginBottom: 6,
},
tagline: {
fontFamily: 'var(--mono)',
fontSize: 9,
letterSpacing: '.22em',
textTransform: 'uppercase',
marginBottom: 14,
},
desc: {
fontFamily: 'var(--mono)',
fontSize: 11,
color: 'var(--muted)',
lineHeight: 1.7,
marginBottom: 36,
},
fieldLabel: {
fontFamily: 'var(--mono)',
fontSize: 9,
letterSpacing: '.18em',
textTransform: 'uppercase',
color: 'var(--muted)',
marginBottom: 8,
},
brandInput: {
background: 'var(--surface)',
border: '1px solid var(--border2)',
borderRadius: 8,
color: 'var(--text)',
fontFamily: 'var(--mono)',
fontSize: 15,
padding: '14px 16px',
outline: 'none',
marginBottom: 20,
},
inputGroup: {
display: 'flex',
alignItems: 'center',
border: '1px solid var(--border2)',
borderRadius: 8,
overflow: 'hidden',
marginBottom: 12,
},
prefix: {
fontFamily: 'var(--mono)',
fontSize: 14,
color: 'var(--muted)',
padding: '14px 14px',
background: 'var(--surface)',
borderRight: '1px solid var(--border2)',
flexShrink: 0,
letterSpacing: '.06em',
},
input: {
flex: 1,
background: 'var(--surface)',
border: 'none',
color: 'var(--text)',
fontFamily: 'var(--mono)',
fontSize: 18,
padding: '14px 16px',
outline: 'none',
letterSpacing: '.1em',
},
error: {
fontFamily: 'var(--mono)',
fontSize: 11,
color: 'var(--danger)',
marginBottom: 12,
letterSpacing: '.06em',
},
callBtn: {
color: '#0a0a0a',
border: 'none',
fontFamily: 'var(--sans)',
fontSize: 14,
fontWeight: 700,
letterSpacing: '.06em',
padding: '16px 24px',
borderRadius: 8,
cursor: 'pointer',
marginTop: 8,
marginBottom: 16,
transition: 'opacity .15s',
},
fine: {
fontFamily: 'var(--mono)',
fontSize: 10,
color: 'var(--muted)',
letterSpacing: '.1em',
textAlign: 'center',
lineHeight: 1.6,
},
}