// Shared misc pages: outbound calls, language presets, demo link function OutboundPage() { const [mode, setMode] = React.useState('single'); const [phone, setPhone] = React.useState(''); const [name, setName] = React.useState(''); const [bulkList, setBulkList] = React.useState(''); const [dispatching, setDispatching] = React.useState(false); const [dispatchState, setDispatchState] = React.useState('idle'); // idle | ok | error const [dispatchMsg, setDispatchMsg] = React.useState(''); const [jobs, setJobs] = React.useState([]); const [livekit, setLivekit] = React.useState({ url: '', sip_trunk_id: '', connected: false }); React.useEffect(() => { fetch('/api/config').then(r => r.json()).then(data => { setLivekit({ url: data.livekit_url || '-', sip_trunk_id: data.sip_trunk_id || '-', connected: !!(data.livekit_url && data.livekit_api_key && data.sip_trunk_id), }); }).catch(() => {}); fetch('/api/logs').then(r => r.json()).then(data => { setJobs(Array.isArray(data) ? data.slice(0, 20) : []); }).catch(() => {}); }, []); const handleDispatch = async () => { setDispatching(true); setDispatchState('idle'); try { if (mode === 'single') { const res = await fetch('/api/call/single', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone, caller_name: name }), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || data.error || 'Dispatch failed'); setDispatchState('ok'); setDispatchMsg(`Call dispatched to ${phone}`); setPhone(''); setName(''); } else { const lines = bulkList.trim().split('\n').filter(Boolean); const numbersStr = lines.map(l => l.split(',')[0].trim()).join('\n'); const res = await fetch('/api/call/bulk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ numbers: numbersStr }), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || data.error || 'Bulk dispatch failed'); setDispatchState('ok'); setDispatchMsg(`${lines.length} calls queued`); setBulkList(''); } } catch (e) { setDispatchState('error'); setDispatchMsg(e.message); } finally { setDispatching(false); setTimeout(() => { setDispatchState('idle'); setDispatchMsg(''); }, 4000); } }; const RESULT_C = { booked: 'green', failed: 'red', completed: 'blue', pending: 'amber', unknown: 'gray' }; return (
{mode === 'single' ? (
{dispatching ? 'Dispatching...' : 'Dispatch Call'}
) : (
{bulkList.trim() ? `${bulkList.trim().split('\n').filter(Boolean).length} numbers detected` : 'Paste phone numbers above'}
{dispatching ? 'Queuing...' : `Dispatch Bulk (${bulkList.trim() ? bulkList.trim().split('\n').filter(Boolean).length : 0})`}
)} {dispatchMsg && (
{dispatchMsg}
)}
Trunk Status
{[ { label: 'LiveKit URL', value: livekit.url }, { label: 'SIP Trunk', value: livekit.sip_trunk_id }, { label: 'Status', value: {livekit.connected ? 'Connected' : 'Not configured'} }, ].map(r => (
{r.label} {r.value}
))}
[
{j.phone_number}
{j.caller_name || 'Unknown'}
, {j.created_at ? new Date(j.created_at).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true }) : '-'}, {j.duration_seconds != null ? `${Math.floor(j.duration_seconds / 60)}m ${Math.round(j.duration_seconds % 60)}s` : '-'}, {j.status || 'unknown'}, ])} />
); } const PRESETS = [ { id: 'multilingual', label: 'Multilingual (Auto-detect)', desc: 'Detect the caller language and adapt naturally across Indian campaigns.', sttLanguage: 'unknown', ttsLanguage: 'hi-IN', ttsVoice: 'kavya', geminiLiveLanguage: '' }, { id: 'hinglish', label: 'Hinglish', desc: 'Mix Hindi and English naturally for conversational Indian sales calls.', sttLanguage: 'hi-IN', ttsLanguage: 'hi-IN', ttsVoice: 'kavya', geminiLiveLanguage: '' }, { id: 'english', label: 'English (India)', desc: 'Indian English only. Fast and consistent for urban prospects.', sttLanguage: 'en-IN', ttsLanguage: 'en-IN', ttsVoice: 'dev', geminiLiveLanguage: 'en-IN' }, { id: 'hindi', label: 'Hindi', desc: 'Pure Hindi. Best for Tier 2/3 markets.', sttLanguage: 'hi-IN', ttsLanguage: 'hi-IN', ttsVoice: 'ritu', geminiLiveLanguage: 'hi-IN' }, { id: 'marathi', label: 'Marathi', desc: 'Polite Marathi for Maharashtra-focused outreach.', sttLanguage: 'mr-IN', ttsLanguage: 'mr-IN', ttsVoice: 'shubh', geminiLiveLanguage: 'mr-IN' }, { id: 'tamil', label: 'Tamil', desc: 'Tamil language for Tamil Nadu campaigns.', sttLanguage: 'ta-IN', ttsLanguage: 'ta-IN', ttsVoice: 'priya', geminiLiveLanguage: 'ta-IN' }, { id: 'telugu', label: 'Telugu', desc: 'Telugu for Hyderabad and Andhra campaigns.', sttLanguage: 'te-IN', ttsLanguage: 'te-IN', ttsVoice: 'kavya', geminiLiveLanguage: 'te-IN' }, { id: 'gujarati', label: 'Gujarati', desc: 'Gujarati for western India campaigns and investor outreach.', sttLanguage: 'gu-IN', ttsLanguage: 'gu-IN', ttsVoice: 'rohan', geminiLiveLanguage: '' }, { id: 'bengali', label: 'Bengali', desc: 'Bengali for Kolkata and east India campaigns.', sttLanguage: 'bn-IN', ttsLanguage: 'bn-IN', ttsVoice: 'neha', geminiLiveLanguage: '' }, { id: 'kannada', label: 'Kannada', desc: 'Kannada for Bengaluru and Karnataka outreach.', sttLanguage: 'kn-IN', ttsLanguage: 'kn-IN', ttsVoice: 'rahul', geminiLiveLanguage: '' }, { id: 'malayalam', label: 'Malayalam', desc: 'Malayalam for Kerala prospects and follow-ups.', sttLanguage: 'ml-IN', ttsLanguage: 'ml-IN', ttsVoice: 'ritu', geminiLiveLanguage: '' }, ]; function LanguagePage() { const [active, setActive] = React.useState('multilingual'); const [saved, setSaved] = React.useState(false); const [loading, setLoading] = React.useState(true); React.useEffect(() => { fetch('/api/config') .then(r => r.json()) .then(cfg => { const presetId = PRESETS.some(p => p.id === cfg.lang_preset) ? cfg.lang_preset : 'multilingual'; setActive(presetId); }) .catch(() => {}) .finally(() => setLoading(false)); }, []); const handleSave = async () => { try { const preset = PRESETS.find(p => p.id === active) || PRESETS[0]; await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lang_preset: preset.id, stt_language: preset.sttLanguage, tts_language: preset.ttsLanguage, tts_voice: preset.ttsVoice, gemini_live_language: preset.geminiLiveLanguage, }), }); setSaved(true); setTimeout(() => setSaved(false), 2000); } catch {} }; return (
{loading ? : (
{PRESETS.map(p => (
setActive(p.id)} style={{ border: `1px solid ${active === p.id ? '#5a7ef5' : 'rgba(255,255,255,0.07)'}`, borderRadius: 12, padding: '16px 20px', cursor: 'pointer', background: active === p.id ? 'rgba(90,126,245,0.08)' : '#13161e', transition: 'all 0.15s', display: 'flex', alignItems: 'center', gap: 14 }}>
{active === p.id &&
}
{p.label}
{p.desc}
))} {saved ? 'Saved' : 'Save Preset'}
)}
); } function LegacyDemoPage_unused() { const [generated, setGenerated] = React.useState(false); const [label, setLabel] = React.useState('Escala Realty - Property Demo'); const [expiry, setExpiry] = React.useState('7'); const [copied, setCopied] = React.useState(false); const link = `${window.location.origin}/demo`; const handleCopy = () => { navigator.clipboard?.writeText(link).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; return (
setGenerated(true)}>Generate Demo Link
{generated && (
Your Demo Link
{link}
{copied ? 'Copied' : 'Copy Link'} window.open(link, '_blank')}>Open in Tab
Expires in {expiry === '0' ? 'never' : `${expiry} day${expiry === '1' ? '' : 's'}`} ยท Label: {label}
)}
); } function DemoPage() { const [label, setLabel] = React.useState('Escala Realty - Property Demo'); const [expiry, setExpiry] = React.useState('7'); const [maxRequests, setMaxRequests] = React.useState('3'); const [generating, setGenerating] = React.useState(false); const [generatedLink, setGeneratedLink] = React.useState(null); const [error, setError] = React.useState(''); const [copied, setCopied] = React.useState(false); const link = generatedLink?.url || ''; const handleCopy = () => { if (!link) return; navigator.clipboard?.writeText(link).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; const handleGenerate = async () => { setGenerating(true); setError(''); try { const res = await fetch('/api/demo-links', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label, expiry_days: parseInt(expiry, 10) || 0, max_requests: parseInt(maxRequests, 10) || 1, }), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || data.message || 'Unable to generate demo link'); setGeneratedLink(data.link || null); } catch (e) { setGeneratedLink(null); setError(e.message); } finally { setGenerating(false); } }; return (
{generating ? 'Generating...' : 'Generate Demo Link'} {error && (
{error}
)}
{generatedLink && (
Your Demo Link
{link}
{copied ? 'Copied' : 'Copy Link'} window.open(link, '_blank')}>Open in Tab
Expires in {expiry === '0' ? 'never' : `${expiry} day${expiry === '1' ? '' : 's'}`} | Max requests: {generatedLink.max_requests} | Remaining now: {generatedLink.remaining_requests}
Anyone opening this link can enter their own phone number and trigger a real outbound demo call until the request limit is used up.
)} {generatedLink && (
Preview