// ============================================================================ // public/screens/settings.jsx — Tenant Settings (SaaS-aware) // ---------------------------------------------------------------------------- // Tenants configure their OWN telephony infrastructure (Asterisk, GSM, SIMs). // They do NOT see AI provider keys — those are platform-managed (Hidrogen IP). // // Tabs: Profile | Telephony | SIM Cards | Advanced // ============================================================================ function SettingsScreen({ session }) { const [tab, setTab] = React.useState("telephony"); const tabs = [ { id: "profile", label: "Profile" }, { id: "telephony", label: "Telephony" }, { id: "sims", label: "SIM Cards" }, { id: "advanced", label: "Advanced" }, ]; return (
{tabs.map((t) => ( ))}
{tab === "profile" && } {tab === "telephony" && } {tab === "sims" && } {tab === "advanced" && }
); } // ── Profile tab ───────────────────────────────────────────────────────── function ProfileTab({ session }) { const [form, setForm] = React.useState({ name: session?.user?.name || "", phone: session?.user?.phone || "", title: session?.user?.title || "", }); const [saving, setSaving] = React.useState(false); const [msg, setMsg] = React.useState(null); const save = async () => { setSaving(true); setMsg(null); const r = await VoaisAPI.patch("/api/me/profile", form); setSaving(false); setMsg(r.ok ? { type: "ok", text: "Profile updated." } : { type: "err", text: r.data?.msg || "Failed." }); }; return (
{msg &&
{msg.text}
} setForm({ ...form, name: e.target.value })}/> setForm({ ...form, phone: e.target.value })}/> setForm({ ...form, title: e.target.value })}/> {saving ? "Saving…" : "Save profile"}
); } // ── 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.

{/* Status strip */} {status && (
{status.ready ? ( Ready to dial ) : ( Setup incomplete )}
{status.ai?.providers?.length > 0 && (
AI providers available: {status.ai.providers.join(", ")} · Default model: {status.ai.defaultModel}
)} )} {msg && (
{msg.text}
)} {/* Settings groups (all tenant-level — no AI keys exposed) */} {(data?.groups || []).map((group) => ( setForm({ ...form, [k]: v })} secretSnapshot={data?.settings || {}} /> ))} {/* Sticky action bar */}
}> {saving ? "Saving…" : "Save telephony settings"} }> {testing ? "Testing…" : "Test connection"} setForm({ ...data?.settings || {} })}>Reset
); } // ── SIM Cards tab ─────────────────────────────────────────────────────── function SimsTab() { const [sims, setSims] = React.useState([]); const [loading, setLoading] = React.useState(true); const [showAdd, setShowAdd] = React.useState(false); const load = () => { setLoading(true); VoaisAPI.get("/api/telephony/sims").then((r) => { if (r.ok && r.data?.ok) setSims(r.data.sims || []); setLoading(false); }); }; React.useEffect(load, []); const toggleActive = async (sim) => { await VoaisAPI.patch("/api/telephony/sims/" + sim.id, { active: sim.active ? 0 : 1 }); load(); }; const deleteSim = async (sim) => { if (!confirm("Delete SIM port " + sim.port + "?")) return; await VoaisAPI.del("/api/telephony/sims/" + sim.id); load(); }; return (

Your SIM Cards

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.

{sims.length} SIM{sims.length !== 1 ? "s" : ""} {sims.filter((s) => s.active).length} active
setShowAdd(true)} icon={}>Add SIM }/>
{loading ? (
Loading…
) : sims.length === 0 ? (
) : ( {sims.map((s) => ( ))}
Port Label Phone Number Carrier Prefix Status
{s.port} {s.label || "—"} {s.phone_number || "—"} {s.carrier || "—"} {s.prefix || "—"} {s.active ? "Active" : "Inactive"}
toggleActive(s)}> {s.active ? "Disable" : "Enable"} deleteSim(s)} style={{ color: "var(--err)" }}>
)} {showAdd && setShowAdd(false)} onAdded={() => { setShowAdd(false); load(); }}/>}
); } function AddSimModal({ onClose, onAdded }) { const [form, setForm] = React.useState({ port: "", label: "", phone_number: "", carrier: "", prefix: "", notes: "", }); const [submitting, setSubmitting] = React.useState(false); const [err, setErr] = React.useState(null); const submit = async (e) => { e.preventDefault(); setErr(null); if (!form.port) return setErr("Port number required."); setSubmitting(true); const r = await VoaisAPI.post("/api/telephony/sims", form); setSubmitting(false); if (r.ok) onAdded(); else setErr(r.data?.msg || "Failed to add SIM."); }; return (
{err &&
{err}
} setForm({ ...form, port: e.target.value })}/> setForm({ ...form, label: e.target.value })}/>
setForm({ ...form, phone_number: e.target.value })}/> setForm({ ...form, carrier: e.target.value })}/>
setForm({ ...form, prefix: e.target.value })}/>
Cancel {submitting ? "Adding…" : "Add SIM"}
); } // ── Advanced tab ──────────────────────────────────────────────────────── function AdvancedTab() { return (

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 (
{icons[group.id] || }

{group.label}

{group.description && (

{group.description}

)}
meta[k]?.type === "textarea") ? "1fr" : "1fr 1fr", gap: 14, marginTop: 16 }}> {group.keys.map((key) => { const m = meta[key] || {}; const isSecret = m.secret; const val = form[key] ?? ""; const isSet = isSecret && secretSnapshot[key] === "set" && val === "set"; if (m.type === "textarea") { return (