Ecobot_Scoala_Verde/frontend/js/app.js
Stefan Caramizoiu d7a7d2cafd Initial commit
2026-04-01 11:14:26 +03:00

307 lines
9.6 KiB
JavaScript

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();