const micBtn = document.getElementById('micBtn'); const statusText = document.getElementById('statusText'); const visualizer = document.getElementById('visualizer'); const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); // Result Modal Elements const resultModal = document.getElementById('resultModal'); const closeModal = document.getElementById('closeModal'); const resultEmoji = document.getElementById('resultEmoji'); const resultLabel = document.getElementById('resultLabel'); const resultConfidence = document.getElementById('resultConfidence'); const btnCorrect = document.getElementById('btnCorrect'); const btnIncorrect = document.getElementById('btnIncorrect'); const correctionArea = document.getElementById('correctionArea'); const submitCorrection = document.getElementById('submitCorrection'); let mediaRecorder; let audioChunks = []; let currentTempFilename = null; let currentPrediction = null; // Emotion to Emoji Map const emotionEmojis = { 'neutral': '😐', 'calm': '😌', 'happiness': '😄', 'happy': '😄', 'sadness': '😢', 'sad': '😢', 'anger': '😠', 'angry': '😠', 'fear': '😱', 'disgust': '🤢', 'surprise': '😲' }; // --- Recording Logic --- micBtn.addEventListener('mousedown', startRecording); micBtn.addEventListener('mouseup', stopRecording); micBtn.addEventListener('mouseleave', () => { if (mediaRecorder && mediaRecorder.state === 'recording') { stopRecording(); } }); // Training Logic const trainingModal = document.getElementById('trainingModal'); const trainingLog = document.getElementById('trainingLog'); const closeTrainingModal = document.getElementById('closeTrainingModal'); closeTrainingModal.addEventListener('click', () => { trainingModal.classList.add('hidden'); }); // --- Training & Password Logic --- const passwordModal = document.getElementById('passwordModal'); const closePasswordModal = document.getElementById('closePasswordModal'); const submitPasswordBtn = document.getElementById('submitPasswordBtn'); const adminPasswordInput = document.getElementById('adminPasswordInput'); // Open Password Modal document.getElementById('trainBtn').addEventListener('click', () => { passwordModal.classList.remove('hidden'); adminPasswordInput.value = ''; adminPasswordInput.focus(); }); // Close Password Modal closePasswordModal.addEventListener('click', () => { passwordModal.classList.add('hidden'); }); // Handle Password Submission // Handle Password Submission function submitPassword() { const password = adminPasswordInput.value; if (!password) { showToast("Please enter a password", "error"); return; } passwordModal.classList.add('hidden'); startTraining(password); } submitPasswordBtn.addEventListener('click', submitPassword); // Allow Enter key to submit password and Esc to close modals document.addEventListener('keydown', (e) => { // Enter Key in Password Input if (e.key === 'Enter' && document.activeElement === adminPasswordInput) { submitPassword(); } // Escape Key Global if (e.key === 'Escape') { passwordModal.classList.add('hidden'); resultModal.classList.add('hidden'); trainingModal.classList.add('hidden'); } }); async function startTraining(password) { // Open Terminal trainingModal.classList.remove('hidden'); trainingLog.innerHTML = 'Authenticating...'; try { const response = await fetch('/train', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: password }) }); if (response.status === 401) { trainingLog.innerHTML += 'Error: Unauthorized. Incorrect Password.'; showToast("Incorrect Admin Password", "error"); return; } const data = await response.json(); if (data.status === 'training_started') { trainingLog.innerHTML += 'Access Granted. Starting training sequence...'; pollLogs(); } } catch (e) { showToast("Failed to start training.", "error"); trainingLog.innerHTML += `Error: ${e.message}`; } } async function pollLogs(startIndex = 0) { try { const response = await fetch(`/logs?after=${startIndex}`); const data = await response.json(); if (data.logs && data.logs.length > 0) { data.logs.forEach(log => { const line = document.createElement('span'); line.className = 'log-line'; line.innerText = log; if (log.includes("CRITICAL") || log.includes("Error")) line.style.color = '#ff5555'; if (log.includes("Success") || log.includes("complete")) line.style.color = '#55ff55'; trainingLog.appendChild(line); }); // Auto scroll trainingLog.scrollTop = trainingLog.scrollHeight; } // Continue polling if not complete (simple check: if logs stop or specific message?) // Better: The backend just keeps logs coming. We'll poll until we see "Training complete" const lastLog = data.logs.length > 0 ? data.logs[data.logs.length - 1] : ""; if (lastLog.includes("Training complete")) { trainingLog.innerHTML += '>> Process finished. You may close this window.'; return; } setTimeout(() => pollLogs(data.next_index), 500); // Poll every 500ms } catch (e) { console.error("Polling error", e); setTimeout(() => pollLogs(startIndex), 2000); // Retry slower on error } } // Touch support for mobile micBtn.addEventListener('touchstart', (e) => { e.preventDefault(); startRecording(); }); micBtn.addEventListener('touchend', (e) => { e.preventDefault(); stopRecording(); }); function startRecording() { statusText.innerText = "Recording..."; micBtn.classList.add('recording'); visualizer.classList.remove('hidden'); audioChunks = []; navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { mediaRecorder = new MediaRecorder(stream); mediaRecorder.start(); mediaRecorder.addEventListener("dataavailable", event => { audioChunks.push(event.data); }); mediaRecorder.addEventListener("stop", () => { const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); // Default typically webm, assumes backend handles it or we send as file // Usually comes as webm/ogg from browser. We'll verify mimetype. uploadAudio(audioBlob, "recording.wav"); // Naming it .wav but content might be webm, backend pydub handles it. }); }) .catch(err => { console.error("Error accessing mic:", err); statusText.innerText = "Error Accessing Mic"; }); } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); statusText.innerText = "Processing..."; micBtn.classList.remove('recording'); visualizer.classList.add('hidden'); } } // --- File Upload Logic --- fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); } }); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); function handleFile(file) { statusText.innerText = `Uploading ${file.name}...`; uploadAudio(file, file.name); } // --- Toast Notifications --- function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; // Icon selection let icon = 'fa-info-circle'; if (type === 'success') icon = 'fa-check-circle'; if (type === 'error') icon = 'fa-exclamation-circle'; toast.innerHTML = ` ${message} `; container.appendChild(toast); // Auto remove setTimeout(() => { toast.classList.add('hide'); toast.addEventListener('animationend', () => toast.remove()); }, 3000); } // --- API Calls --- async function uploadAudio(fileOrBlob, filename) { const formData = new FormData(); formData.append("file", fileOrBlob, filename); // Append file try { const response = await fetch('/predict', { method: 'POST', body: formData }); if (!response.ok) { const errData = await response.json(); throw new Error(errData.detail || "Prediction failed"); } const data = await response.json(); showResult(data); statusText.innerText = "Click & Hold to Record"; showToast("Analysis Complete", "success"); } catch (error) { console.error(error); statusText.innerText = "Error: " + error.message; showToast("Error: " + error.message, "error"); } } function showResult(data) { currentTempFilename = data.temp_filename; currentPrediction = data.prediction; resultEmoji.innerText = emotionEmojis[data.prediction.toLowerCase()] || '❓'; resultLabel.innerText = data.prediction.charAt(0).toUpperCase() + data.prediction.slice(1); resultConfidence.innerText = `Confidence: ${(data.confidence * 100).toFixed(1)}%`; // Reset feedback UI correctionArea.classList.add('hidden'); resultModal.classList.remove('hidden'); if (data.is_fallback) { showToast("Model not trained. Please label this audio to build the dataset.", "info"); correctionArea.classList.remove('hidden'); resultLabel.innerText = "Label Required"; resultEmoji.innerText = "🏷️"; resultConfidence.innerText = "Help the AI learn!"; } // --- NLP Analysis Display --- let nlpDiv = document.getElementById('nlp-results'); if (!nlpDiv) { nlpDiv = document.createElement('div'); nlpDiv.id = 'nlp-results'; nlpDiv.className = 'nlp-container'; // Insert before feedback section const feedbackSection = resultModal.querySelector('.feedback-section'); resultModal.querySelector('.modal-content').insertBefore(nlpDiv, feedbackSection); } // Clear previous nlpDiv.innerHTML = ''; if (data.nlp_analysis && data.nlp_analysis.transcription) { const textEmotion = data.nlp_analysis.text_emotion; const confidencePct = (textEmotion.score * 100).toFixed(1); // Show Hybrid Breakdown nlpDiv.innerHTML = `
"${data.nlp_analysis.transcription}"
No speech detected or analysis unavailable.
`; } } // --- Modal & Feedback --- closeModal.addEventListener('click', () => resultModal.classList.add('hidden')); window.onclick = (event) => { if (event.target == resultModal) resultModal.classList.add('hidden'); }; btnCorrect.addEventListener('click', () => { submitFeedback(currentPrediction); }); btnIncorrect.addEventListener('click', () => { correctionArea.classList.remove('hidden'); }); submitCorrection.addEventListener('click', () => { const selected = document.getElementById('emotionSelect').value; if (selected) { submitFeedback(selected); } }); async function submitFeedback(correctLabel) { try { const response = await fetch('/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: currentTempFilename, original_emotion: currentPrediction, correct_emotion: correctLabel }) }); const res = await response.json(); if (res.status === 'success') { showToast("Feedback saved successfully!", "success"); resultModal.classList.add('hidden'); } } catch (e) { showToast("Failed to save feedback.", "error"); } }