const LIVEKIT_URL = "wss://voice.hobbs.farm"; let room = null; // --- Settings --- const DEFAULTS = { confidence: 0.7, min_volume: 0.3, start_secs: 0.4, stop_secs: 0.5, max_tokens: 150, tts_voice: "af_nova" }; function getSettings() { try { const saved = JSON.parse(localStorage.getItem("vc_settings")); return saved ? { ...DEFAULTS, ...saved } : { ...DEFAULTS }; } catch (e) { return { ...DEFAULTS }; } } function loadSettingsUI() { const s = getSettings(); for (const [key, val] of Object.entries(s)) { const input = document.getElementById("set-" + key); const display = document.getElementById("val-" + key); if (input) input.value = val; if (display) display.textContent = val; } // Bind live value display document.querySelectorAll(".settings-body input[type=range]").forEach(function(el) { el.addEventListener("input", function() { const key = el.id.replace("set-", ""); document.getElementById("val-" + key).textContent = el.value; }); }); var sel = document.getElementById("set-tts_voice"); if (sel) sel.addEventListener("change", function() { document.getElementById("val-tts_voice").textContent = sel.value; }); } function saveSettings() { var s = {}; for (var key of Object.keys(DEFAULTS)) { var el = document.getElementById("set-" + key); if (el) s[key] = el.tagName === "SELECT" ? el.value : parseFloat(el.value); } localStorage.setItem("vc_settings", JSON.stringify(s)); // Push to server fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(s), }).then(function(r) { if (r.ok) log("Settings saved and applied"); else log("Settings saved locally but server update failed", "warn"); }).catch(function() { log("Settings saved locally (server unreachable)", "warn"); }); } function resetSettings() { localStorage.removeItem("vc_settings"); loadSettingsUI(); saveSettings(); log("Settings reset to defaults"); } function toggleSettings() { var body = document.getElementById("settingsBody"); var toggle = document.getElementById("settingsToggle"); if (body.style.display === "none") { body.style.display = "block"; toggle.textContent = "-"; } else { body.style.display = "none"; toggle.textContent = "+"; } } // --- Logging --- function log(msg, level) { var el = document.getElementById("logOutput"); var time = new Date().toLocaleTimeString(); var cls = level === "error" ? "log-error" : level === "warn" ? "log-warn" : "log-info"; el.innerHTML += '
' + time + ' ' + msg + '
'; el.scrollTop = el.scrollHeight; } function clearLog() { document.getElementById("logOutput").innerHTML = ""; } // --- Connection --- async function connect() { setStatus("connecting", "Connecting..."); log("Requesting token..."); // Push current settings before connecting var settings = getSettings(); try { await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings), }); log("Settings pushed: confidence=" + settings.confidence + " vol=" + settings.min_volume); } catch (e) { log("Could not push settings", "warn"); } try { var response = await fetch("/api/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identity: "jeremy-" + Date.now() }), }); if (!response.ok) throw new Error("Token " + response.status); var data = await response.json(); log("Token received"); room = new LivekitClient.Room({ audioCaptureDefaults: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); room.on(LivekitClient.RoomEvent.Connected, function() { setStatus("connected", "Connected"); document.getElementById("connectBtn").style.display = "none"; document.getElementById("disconnectBtn").style.display = "block"; log("Connected to LiveKit"); }); room.on(LivekitClient.RoomEvent.Disconnected, function(reason) { setStatus("disconnected", "Disconnected"); document.getElementById("connectBtn").style.display = "block"; document.getElementById("disconnectBtn").style.display = "none"; log("Disconnected" + (reason ? ": " + reason : ""), "warn"); }); room.on(LivekitClient.RoomEvent.ParticipantConnected, function(p) { log("Agent joined: " + p.identity); }); room.on(LivekitClient.RoomEvent.TrackSubscribed, function(track, pub, p) { if (track.kind === LivekitClient.Track.Kind.Audio) { var el = track.attach(); document.body.appendChild(el); el.play().catch(function(e) { log("Audio play error: " + e.message, "error"); }); } }); room.on(LivekitClient.RoomEvent.MediaDevicesError, function(err) { log("Mic error: " + err.message, "error"); }); await room.connect(data.url || LIVEKIT_URL, data.token); await room.localParticipant.setMicrophoneEnabled(true); log("Mic enabled. Speak now."); } catch (err) { setStatus("disconnected", "Error: " + err.message); log("Error: " + err.message, "error"); } } async function disconnect() { if (room) { await room.disconnect(); room = null; } } function setMode(mode) { document.getElementById("modeDisplay").textContent = "Mode: " + mode; log("Mode: " + mode); if (room && room.localParticipant) { room.localParticipant.publishData( new TextEncoder().encode(JSON.stringify({ type: "mode_change", mode })), { reliable: true } ); } } function setStatus(state, text) { var el = document.getElementById("status"); el.className = "status " + state; el.textContent = text; } document.getElementById("connectBtn").addEventListener("click", connect); document.getElementById("disconnectBtn").addEventListener("click", disconnect); // Init loadSettingsUI(); // Push saved settings on load fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(getSettings()), }).catch(function() {}); log("VoiceCoach loaded. Ready.");