// ─── Agent Settings Page ─────────────────────────────────────────────────────
const GEMINI_DEFAULT_MODEL = 'gemini-3.1-flash-live-preview';
const GEMINI_TTS_DEFAULT_MODEL = 'gemini-3.1-flash-tts-preview';
const GEMINI_LANGUAGE_OPTIONS = [
{ value: '', label: 'Auto-detect' },
{ value: 'en-IN', label: 'English (India)' },
{ value: 'hi-IN', label: 'Hindi' },
{ value: 'mr-IN', label: 'Marathi' },
{ value: 'ta-IN', label: 'Tamil' },
{ value: 'te-IN', label: 'Telugu' },
];
const CFG_DEFAULTS = {
first_line: "Namaste! This is Aryan from SPX AI. We help businesses automate with AI. Hmm, may I ask what kind of business you run?",
agent_instructions: "",
stt_min_endpointing_delay: 0.15, max_endpointing_delay: 0.9,
turn_detection_mode: 'stt', endpointing_mode: 'dynamic',
allow_interruptions: true, interruption_mode: 'adaptive',
discard_audio_if_uninterruptible: true, min_interruption_duration: 0.18,
min_interruption_words: 0, resume_false_interruption: true,
false_interruption_timeout: 1.2, preemptive_generation: true,
preemptive_generation_enable_tts: false, preemptive_generation_max_speech_duration: 8.0,
preemptive_generation_max_retries: 2, use_tts_aligned_transcript: true,
user_away_timeout: 15.0, session_close_transcript_timeout: 2.0, max_turns: 25,
voice_mode: 'pipeline', llm_provider: 'groq', llm_model: 'llama-3.3-70b-versatile',
stt_provider: 'sarvam', stt_language: 'unknown', stt_model: 'saaras:v3', stt_mode: 'translate',
stt_sample_rate: 16000, tts_provider: 'cartesia', tts_language: 'hi-IN',
tts_voice: 'kavya', xai_tts_voice: 'eve', xai_tts_style_preset: 'spirited',
cartesia_voice_id: '', cartesia_tts_model: 'sonic-3',
sarvam_tts_model: 'bulbul:v3', cartesia_sample_rate: 24000, xai_tts_sample_rate: 24000,
gemini_live_model: GEMINI_DEFAULT_MODEL, gemini_live_voice: 'Puck', gemini_live_temperature: 0.8, gemini_live_language: '', gemini_tts_model: GEMINI_TTS_DEFAULT_MODEL,
lang_preset: 'multilingual',
livekit_url: '', sip_trunk_id: '',
livekit_api_key: '', livekit_api_secret: '',
groq_api_key: '', openrouter_api_key: '',
google_api_key: '', sarvam_api_key: '',
cartesia_api_key: '', x_ai_api_key: '',
supabase_url: '', supabase_key: '',
};
function GeneralTab({ cfg, set }) {
return (
set('stt_min_endpointing_delay')(parseFloat(v))} hint="Min endpointing delay before agent responds." />
set('max_turns')(parseInt(v))} hint="Maximum dialogue turns per session." />
);
}
function TurnHandlingTab({ cfg, set }) {
return (
Turn Detection
({ value: v, label: v.toUpperCase() }))} />
({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }))} />
set('max_endpointing_delay')(parseFloat(v))} />
({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }))} />
Session
set('max_turns')(parseInt(v))} />
set('user_away_timeout')(parseFloat(v))} />
);
}
function LatencyTab({ cfg, set }) {
return (
Latency Toggles
Timing Thresholds
set('min_interruption_duration')(parseFloat(v))} />
set('false_interruption_timeout')(parseFloat(v))} />
set('preemptive_generation_max_retries')(parseInt(v))} />
set('session_close_transcript_timeout')(parseFloat(v))} />
set('min_interruption_words')(parseInt(v))} />
set('preemptive_generation_max_speech_duration')(parseFloat(v))} />
set('user_away_timeout')(parseFloat(v))} />
);
}
function ModelsTab({ cfg, set }) {
const vm = cfg.voice_mode || 'pipeline';
const geminiModel = cfg.gemini_live_model || GEMINI_DEFAULT_MODEL;
const geminiLimited = /3\.1/i.test(geminiModel);
return (
Conversation Mode
{[
{ value: 'pipeline', label: 'Pipeline', sub: 'STT → LLM → TTS' },
{ value: 'gemini_live', label: 'Gemini Live', sub: 'Google native audio' },
].map(m => (
set('voice_mode')(m.value)}
style={{ border: `1px solid ${vm === m.value ? '#5a7ef5' : 'rgba(255,255,255,0.08)'}`, borderRadius: 10, padding: '12px 14px', cursor: 'pointer', background: vm === m.value ? 'rgba(90,126,245,0.08)' : 'transparent', transition: 'all 0.15s' }}>
{m.label}
{m.sub}
))}
{vm === 'pipeline' && <>
LLM Provider
Speech-to-Text (STT)
({ value: v, label: v }))} />
set('stt_sample_rate')(parseInt(v))} />
Text-to-Speech (TTS)
{cfg.tts_provider === 'xai' && <>
({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }))} />
set('xai_tts_sample_rate')(parseInt(v))} />
>}
{cfg.tts_provider === 'cartesia' && <>
set('cartesia_sample_rate')(parseInt(v))} />
>}
{cfg.tts_provider === 'sarvam' && <>
({ value: v, label: v }))} />
({ value: v, label: v }))} />
>}
>}
{vm === 'gemini_live' && <>
Gemini Live Settings
set('gemini_live_temperature')(parseFloat(v))} />
{geminiLimited && (
Gemini 3.1 preview has limited LiveKit scripted-speech support. This app uses Gemini TTS with the same selected voice for fixed greetings and wrap-ups.
)}
>}
);
}
function CredentialsTab({ cfg, set }) {
const [show, setShow] = React.useState({});
const toggleShow = k => setShow(p => ({ ...p, [k]: !p[k] }));
const SecretInput = ({ label, k, hint }) => (
{label}
set(k)(e.target.value)}
style={{ flex: 1, background: '#1a1e28', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#e8eaef', fontSize: 13, padding: '9px 12px', fontFamily: 'monospace', outline: 'none' }} />
toggleShow(k)} style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, color: '#7b849a', cursor: 'pointer', padding: '0 12px', fontSize: 12 }}>
{show[k] ? 'Hide' : 'Show'}
{hint &&
{hint} }
);
return (
LiveKit
AI Provider Secrets
Supabase
);
}
function AgentSettingsPage() {
const [tab, setTab] = React.useState('General');
const [cfg, setCfg] = React.useState({ ...CFG_DEFAULTS });
const [loading, setLoading] = React.useState(true);
const [saveState, setSaveState] = React.useState('idle'); // idle | saving | saved | error
const tabs = ['General', 'Turn Handling', 'Latency', 'Models & Voice', 'API Credentials'];
const set = k => v => setCfg(p => ({ ...p, [k]: v }));
React.useEffect(() => {
fetch('/api/config')
.then(r => r.json())
.then(data => { setCfg(p => ({ ...p, ...data })); setLoading(false); })
.catch(() => setLoading(false));
}, []);
const handleSave = async () => {
setSaveState('saving');
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cfg),
});
if (!res.ok) throw new Error('Save failed');
setSaveState('saved');
setTimeout(() => setSaveState('idle'), 2500);
} catch {
setSaveState('error');
setTimeout(() => setSaveState('idle'), 3000);
}
};
const saveLabel = { idle: 'Save Changes', saving: 'Saving…', saved: '✓ Saved', error: '✗ Error' }[saveState];
const saveVariant = saveState === 'saved' ? 'success' : saveState === 'error' ? 'danger' : 'primary';
return (
{saveLabel}} />
{loading ? : (
{tab === 'General' && }
{tab === 'Turn Handling' && }
{tab === 'Latency' && }
{tab === 'Models & Voice' && }
{tab === 'API Credentials' && }
)}
);
}
Object.assign(window, { AgentSettingsPage });