const LIVEKIT_URL = "wss://voice.hobbs.farm"; let room = null; function log(msg, level) { const el = document.getElementById("logOutput"); const time = new Date().toLocaleTimeString(); const 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 = ""; } async function connect() { setStatus("connecting", "Connecting..."); log("Requesting token from server..."); try { const response = await fetch("/api/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identity: "jeremy-" + Date.now() }), }); if (!response.ok) { const text = await response.text(); throw new Error("Token request failed: " + response.status + " " + text); } const data = await response.json(); log("Token received. LiveKit URL: " + data.url); room = new LivekitClient.Room({ audioCaptureDefaults: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); room.on(LivekitClient.RoomEvent.Connected, () => { setStatus("connected", "Connected"); document.getElementById("connectBtn").style.display = "none"; document.getElementById("disconnectBtn").style.display = "block"; log("Connected to LiveKit room"); log("Participants in room: " + room.remoteParticipants.size); if (room.remoteParticipants.size === 0) { log("No agent participant yet - waiting for voicecoach-agent to join...", "warn"); } }); room.on(LivekitClient.RoomEvent.Disconnected, (reason) => { setStatus("disconnected", "Disconnected"); document.getElementById("connectBtn").style.display = "block"; document.getElementById("disconnectBtn").style.display = "none"; log("Disconnected from LiveKit" + (reason ? ": " + reason : ""), "warn"); }); room.on(LivekitClient.RoomEvent.ParticipantConnected, (participant) => { log("Participant joined: " + participant.identity); }); room.on(LivekitClient.RoomEvent.ParticipantDisconnected, (participant) => { log("Participant left: " + participant.identity, "warn"); }); room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => { log("Track subscribed: " + track.kind + " from " + participant.identity); if (track.kind === LivekitClient.Track.Kind.Audio) { const element = track.attach(); document.body.appendChild(element); element.play().then(() => { log("Audio playback started"); }).catch((err) => { log("Audio play failed: " + err.message, "error"); }); } }); room.on(LivekitClient.RoomEvent.TrackPublished, (publication, participant) => { log("Track published by " + participant.identity + ": " + publication.kind); }); room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track, publication, participant) => { log("Track unsubscribed: " + track.kind + " from " + participant.identity, "warn"); }); room.on(LivekitClient.RoomEvent.DataReceived, (payload, participant) => { try { const msg = new TextDecoder().decode(payload); log("Data from " + (participant ? participant.identity : "server") + ": " + msg); } catch (e) { log("Data received (binary, " + payload.byteLength + " bytes)"); } }); room.on(LivekitClient.RoomEvent.ConnectionQualityChanged, (quality, participant) => { if (participant.isLocal) { log("Connection quality: " + quality); } }); room.on(LivekitClient.RoomEvent.MediaDevicesError, (err) => { log("Media device error: " + err.message, "error"); }); room.on(LivekitClient.RoomEvent.RoomMetadataChanged, (metadata) => { log("Room metadata changed: " + metadata); }); log("Connecting to " + LIVEKIT_URL + "..."); await room.connect(data.url || LIVEKIT_URL, data.token); log("Enabling microphone..."); await room.localParticipant.setMicrophoneEnabled(true); log("Microphone enabled. Speak now."); // Log local tracks room.localParticipant.audioTrackPublications.forEach((pub) => { log("Local audio track: " + pub.trackSid + " muted=" + pub.isMuted); }); } catch (err) { setStatus("disconnected", "Error: " + err.message); log("Connection error: " + err.message, "error"); console.error(err); } } async function disconnect() { if (room) { log("Disconnecting..."); await room.disconnect(); room = null; } } function setMode(mode) { document.getElementById("modeDisplay").textContent = "Mode: " + mode; log("Mode changed to: " + mode); if (room && room.localParticipant) { const encoder = new TextEncoder(); room.localParticipant.publishData( encoder.encode(JSON.stringify({ type: "mode_change", mode })), { reliable: true } ); } } function setStatus(state, text) { const el = document.getElementById("status"); el.className = "status " + state; el.textContent = text; } document.getElementById("connectBtn").addEventListener("click", connect); document.getElementById("disconnectBtn").addEventListener("click", disconnect); log("VoiceCoach client loaded. Ready to connect.");