);
}
// ── Telephony tab (their own Asterisk + GSM gateway) ─────────────────
function TelephonyTab() {
const [data, setData] = React.useState(null);
const [status, setStatus] = React.useState(null);
const [form, setForm] = React.useState({});
const [saving, setSaving] = React.useState(false);
const [testing, setTesting] = React.useState(false);
const [msg, setMsg] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const reload = React.useCallback(() => {
Promise.all([
VoaisAPI.get("/api/telephony/settings"),
VoaisAPI.get("/api/telephony/status"),
]).then(([s, st]) => {
if (s.ok && s.data?.ok) {
setData(s.data);
setForm({ ...s.data.settings });
}
if (st.ok && st.data?.ok) setStatus(st.data);
setLoading(false);
});
}, []);
React.useEffect(reload, [reload]);
const save = async () => {
setSaving(true); setMsg(null);
const r = await VoaisAPI.patch("/api/telephony/settings", form);
setSaving(false);
if (r.ok) {
setMsg({ type: "ok", text: `${r.data?.saved || 0} settings saved. Changes are live — next call will use the new values.` });
reload();
} else {
setMsg({ type: "err", text: r.data?.msg || "Save failed." });
}
};
const testConnection = async () => {
setTesting(true); setMsg(null);
const r = await VoaisAPI.post("/api/telephony/test-connection", {});
setTesting(false);
if (r.ok && r.data?.ok) {
setMsg({ type: "ok", text: "Connected to your Asterisk successfully!" });
} else {
setMsg({ type: "err", text: r.data?.msg || "Connection failed. Check your AMI host/credentials." });
}
reload();
};
if (loading) return
Loading telephony settings…
;
return (
{/* Intro card */}
Bring Your Own Telephony
VoAIs is the AI brain + CRM. You bring your own Asterisk PBX and GSM gateway. Configure how we should connect to your infrastructure below. Don't worry about AI keys — those are managed for you.
The SIM cards in your GSM gateway. Each port maps to a phone line we'll route calls through. Active SIMs are used round-robin across all your campaigns.
Advanced configuration like DNC list, calling hours, TRAI compliance, and webhook settings will be available here in Phase 7.
For now, you can manage:
Profile — your name, phone, title
Telephony — your Asterisk PBX, ARI, GSM gateway, recording
SIM Cards — your Dinstar GSM ports
AI Brain by Hidrogen
All AI models — OpenAI Realtime, Gemini Live, Sarvam, Hidrogen — are managed for you. No API keys to bring, no quotas to track. Per-minute pricing covers AI usage.
);
}
// ── Shared components ───────────────────────────────────────────────────
function StatusDot({ label, on }) {
return (
{label}
);
}
function SettingsGroup({ group, form, onChange, secretSnapshot }) {
const meta = {
AMI_HOST: { hint: "IP or hostname of your Asterisk server", placeholder: "192.168.1.100" },
AMI_PORT: { hint: "Default: 5038", placeholder: "5038", type: "number" },
AMI_USER: { hint: "AMI username from manager.conf", placeholder: "voiceapp" },
AMI_SECRET: { hint: "AMI secret from manager.conf", placeholder: "••••••••", secret: true },
GSM_TRUNK: { hint: "SIP trunk name pointing to your Dinstar", placeholder: "gsm-trunk" },
GSM_AI_CONTEXT: { hint: "Dialplan context for AI calls", placeholder: "gsm-ai-out" },
GSM_DIAL_CONTEXT: { hint: "Dialplan context for plain calls", placeholder: "gsm-out" },
GSM_DINSTAR_IP: { hint: "Dinstar gateway IP (informational)", placeholder: "192.168.1.200" },
GSM_PORTS: { hint: "CSV of port numbers (e.g. 30,31)", placeholder: "30,31" },
GSM_PREFIX_TEMPLATE: { hint: "Auto-prefix template (e.g. '70' → 7030)", placeholder: "70" },
ARI_URL: { hint: "Asterisk REST Interface URL", placeholder: "http://192.168.1.100:8088" },
ARI_USER: { placeholder: "voiceari" },
ARI_PASSWORD: { secret: true, placeholder: "••••••••" },
ARI_APP: { hint: "Stasis app name", placeholder: "voicegsm" },
EXTERNAL_MEDIA_HOST: { hint: "Public IP of VoAIs server (where Asterisk sends RTP)", placeholder: "203.0.113.42" },
EXTERNAL_MEDIA_PORT: { hint: "UDP port for RTP audio", placeholder: "12000", type: "number" },
AI_SYSTEM_PROMPT: { type: "textarea", hint: "Default system prompt for AI calls. Each agent can override this.", placeholder: "You are a helpful, friendly AI assistant on a phone call..." },
AI_GREETING: { hint: "Opening line the AI says", placeholder: "Hello! How can I help you today?" },
AI_DEFAULT_MODEL: { hint: "Default model ID", placeholder: "gemini-native-latest" },
AI_PROTECT_GREETING: { hint: "'true' = don't let caller interrupt greeting", placeholder: "true" },
AI_VAD_SILENCE_MS: { hint: "Silence before AI replies (ms)", placeholder: "600", type: "number" },
AI_VAD_THRESHOLD: { hint: "0-1, higher = less sensitive", placeholder: "0.5" },
AI_VAD_PREFIX_MS: { hint: "Extra audio before speech start (ms)", placeholder: "300", type: "number" },
RECORD_CALLS: { hint: "'true' or 'false'", placeholder: "true" },
};
const icons = {
ami: ,
ari: ,
gsm: ,
ai: ,
vad: ,
recording: ,
};
return (