// 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 }}>
))}
{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
)}
);
}
Object.assign(window, { OutboundPage, LanguagePage, DemoPage });