const character = new Character('character-canvas'); const audioHandler = new AudioHandler(); const recordBtn = document.getElementById('record-btn'); const btnText = document.getElementById('btn-text'); const wakeBtn = document.getElementById('wake-btn'); const wakeText = document.getElementById('wake-text'); const statusEl = document.getElementById('status'); const userTextEl = document.getElementById('user-text'); const botTextEl = document.getElementById('bot-text'); let ws = null; let wakeListening = false; let wakeRecorder = null; let wakeStream = null; let processingAudio = false; let waitingForWakeResponse = false; function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${protocol}//${window.location.host}/ws`); ws.onopen = () => { statusEl.textContent = 'Conectat! Spune "EcoBot" sau apasa butonul.'; }; ws.onclose = () => { statusEl.textContent = 'Deconectat. Se reconecteaza...'; setTimeout(connectWebSocket, 2000); }; ws.onmessage = async (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'status': statusEl.textContent = message.text; if (message.text.includes('Ascult')) { character.setState('listening'); } else if (message.text.includes('gandesc')) { character.setState('thinking'); } break; case 'user_text': userTextEl.textContent = `Tu: ${message.text}`; userTextEl.classList.remove('hidden'); break; case 'bot_text': botTextEl.textContent = `EcoBot: ${message.text}`; botTextEl.classList.remove('hidden'); break; case 'audio_response': character.setState('talking'); statusEl.textContent = 'Vorbesc...'; await audioHandler.playAudioFromBase64(message.data, message.mime || 'audio/mpeg'); character.setState('idle'); processingAudio = false; resumeWakeIfActive(); break; case 'text_only_response': botTextEl.textContent = `EcoBot: ${message.text}`; botTextEl.classList.remove('hidden'); character.setState('talking'); await new Promise(r => setTimeout(r, 2000)); character.setState('idle'); processingAudio = false; resumeWakeIfActive(); break; case 'wake_detected': waitingForWakeResponse = false; statusEl.textContent = 'Te-am auzit! Vorbeste acum...'; character.setState('listening'); await recordAndSendQuestion(); break; case 'wake_not_detected': waitingForWakeResponse = false; // Only start next chunk after this one is done if (wakeListening && !processingAudio) { startWakeLoop(); } break; } }; } function resumeWakeIfActive() { if (wakeListening) { statusEl.textContent = 'Ascult... Spune "EcoBot" pentru a activa.'; startWakeLoop(); } else { statusEl.textContent = 'Spune "EcoBot" sau apasa butonul.'; } } // ---- Wake Word Mode ---- function toggleWakeMode() { if (wakeListening) { stopWakeMode(); } else { startWakeMode(); } } function startWakeMode() { wakeListening = true; waitingForWakeResponse = false; wakeBtn.classList.add('listening'); wakeText.textContent = 'Ascult... (click pentru a opri)'; statusEl.textContent = 'Ascult... Spune "EcoBot" pentru a activa.'; character.setState('idle'); startWakeLoop(); } function stopWakeMode() { wakeListening = false; waitingForWakeResponse = false; wakeBtn.classList.remove('listening'); wakeText.textContent = 'Spune "EcoBot" pentru a activa'; statusEl.textContent = 'Spune "EcoBot" sau apasa butonul.'; stopWakeRecorder(); } async function startWakeLoop() { // Don't start a new recording if we're still waiting for the previous response if (!wakeListening || processingAudio || waitingForWakeResponse) return; const micId = audioHandler.selectedMicId; const constraints = { audio: micId ? { deviceId: { exact: micId }, autoGainControl: true, noiseSuppression: true } : { autoGainControl: true, noiseSuppression: true }, }; try { wakeStream = await navigator.mediaDevices.getUserMedia(constraints); wakeRecorder = new MediaRecorder(wakeStream, { mimeType: 'audio/webm' }); const chunks = []; wakeRecorder.ondataavailable = (e) => chunks.push(e.data); wakeRecorder.onstop = async () => { if (!wakeListening) return; // Mark as waiting — don't record another chunk until server responds waitingForWakeResponse = true; statusEl.textContent = 'Procesez...'; const blob = new Blob(chunks, { type: 'audio/webm' }); const wavBlob = await audioHandler.convertToWav(blob); const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'audio', data: base64, mode: 'wake' })); } }; reader.readAsDataURL(wavBlob); }; wakeRecorder.start(); statusEl.textContent = 'Ascult... Spune "EcoBot"'; // Record 3 second chunks setTimeout(() => { if (wakeRecorder && wakeRecorder.state === 'recording') { wakeRecorder.stop(); if (wakeStream) { wakeStream.getTracks().forEach(t => t.stop()); } } }, 3000); } catch (e) { console.error('Wake mode error:', e); statusEl.textContent = 'Eroare microfon: ' + e.message; stopWakeMode(); } } function stopWakeRecorder() { if (wakeRecorder && wakeRecorder.state === 'recording') { wakeRecorder.stop(); } if (wakeStream) { wakeStream.getTracks().forEach(t => t.stop()); wakeStream = null; } wakeRecorder = null; } async function recordAndSendQuestion() { processingAudio = true; stopWakeRecorder(); const micId = audioHandler.selectedMicId; const constraints = { audio: micId ? { deviceId: { exact: micId }, autoGainControl: true, noiseSuppression: true } : { autoGainControl: true, noiseSuppression: true }, }; try { const stream = await navigator.mediaDevices.getUserMedia(constraints); const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); const chunks = []; recorder.ondataavailable = (e) => chunks.push(e.data); recorder.onstop = async () => { stream.getTracks().forEach(t => t.stop()); const blob = new Blob(chunks, { type: 'audio/webm' }); const wavBlob = await audioHandler.convertToWav(blob); const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'audio', data: base64 })); } }; reader.readAsDataURL(wavBlob); }; statusEl.textContent = 'Te ascult... (5 secunde)'; recorder.start(); setTimeout(() => { if (recorder.state === 'recording') { recorder.stop(); statusEl.textContent = 'Procesez...'; } }, 5000); } catch (e) { console.error('Record error:', e); processingAudio = false; if (wakeListening) startWakeLoop(); } } // ---- Manual Record (hold button) ---- async function startRecording() { stopWakeRecorder(); await audioHandler.init(); audioHandler.startRecording(); recordBtn.classList.add('recording'); btnText.textContent = 'Inregistrez... elibereaza pentru a trimite'; character.setState('listening'); statusEl.textContent = 'Te ascult...'; } async function stopRecording() { const audioBlob = await audioHandler.stopRecording(); recordBtn.classList.remove('recording'); btnText.textContent = 'Sau tine apasat si vorbeste'; processingAudio = true; const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'audio', data: base64 })); } }; reader.readAsDataURL(audioBlob); } // ---- Event Listeners ---- wakeBtn.addEventListener('click', () => toggleWakeMode()); recordBtn.addEventListener('mousedown', (e) => { e.preventDefault(); startRecording(); }); recordBtn.addEventListener('mouseup', (e) => { e.preventDefault(); if (audioHandler.isRecording) stopRecording(); }); recordBtn.addEventListener('mouseleave', (e) => { if (audioHandler.isRecording) stopRecording(); }); recordBtn.addEventListener('touchstart', (e) => { e.preventDefault(); startRecording(); }); recordBtn.addEventListener('touchend', (e) => { e.preventDefault(); if (audioHandler.isRecording) stopRecording(); }); // Settings const settings = new SettingsManager(audioHandler); // Start connectWebSocket();