Spaces:
Running
Running
| 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, | |
| }, | |
| } | |