let currentSession = { topic: null, currentQuestion: null, questionIndex: null, totalScore: 0, questionCount: 0 }; // Voice recognition setup let recognition = null; if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = false; recognition.lang = 'en-US'; recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; document.getElementById('studentAnswer').value = transcript; }; recognition.onerror = (event) => { console.error('Speech recognition error:', event.error); stopVoiceInput(); }; } async function startSession(topic) { try { const response = await fetch('/api/start_session', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ topic: topic }) }); const data = await response.json(); if (data.status === 'started') { currentSession.topic = topic; currentSession.totalScore = 0; currentSession.questionCount = 0; displayQuestion(data.first_question); document.getElementById('sessionTopic').textContent = `Topic: ${getTopicName(topic)}`; document.getElementById('topicSelection').classList.add('hidden'); document.getElementById('vivaSession').classList.remove('hidden'); updateProgress(); } } catch (error) { console.error('Error starting session:', error); alert('Failed to start session. Please try again.'); } } function getTopicName(topicKey) { const topicNames = { 'upper_limb': 'Upper Limb Anatomy', 'lower_limb': 'Lower Limb Anatomy', 'cardiology': 'Cardiac Anatomy', 'neuroanatomy': 'Neuroanatomy' }; return topicNames[topicKey] || topicKey; } function displayQuestion(questionData) { if (questionData.completed) { endSession(); return; } currentSession.currentQuestion = questionData.question; currentSession.questionIndex = questionData.question_index; document.getElementById('questionText').textContent = questionData.question; document.getElementById('studentAnswer').value = ''; document.getElementById('feedbackArea').classList.add('hidden'); updateProgress(); } async function speakCurrentQuestion() { const questionText = currentSession.currentQuestion; if (!questionText) return; const listeningIndicator = document.getElementById('listeningIndicator'); listeningIndicator.classList.remove('hidden'); try { const response = await fetch('/api/text_to_speech', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: questionText }) }); const data = await response.json(); if (data.audio_data) { await playAudioData(data.audio_data); } } catch (error) { console.error('TTS failed:', error); // Fallback to browser TTS speakWithBrowserTTS(questionText); } finally { listeningIndicator.classList.add('hidden'); } } async function playAudioData(base64Audio) { return new Promise((resolve) => { const audioBlob = base64ToBlob(base64Audio, 'audio/wav'); const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); audio.onended = () => { URL.revokeObjectURL(audioUrl); resolve(); }; audio.onerror = () => resolve(); audio.play().catch(error => { console.error('Audio play failed:', error); resolve(); }); }); } function speakWithBrowserTTS(text) { if ('speechSynthesis' in window) { const utterance = new SpeechSynthesisUtterance(text); utterance.rate = 0.8; utterance.pitch = 1.0; const voices = speechSynthesis.getVoices(); const englishVoice = voices.find(voice => voice.lang.startsWith('en-')); if (englishVoice) { utterance.voice = englishVoice; } speechSynthesis.speak(utterance); } } function toggleVoiceInput() { const voiceBtn = document.getElementById('voiceBtn'); if (!recognition) { alert('Speech recognition not supported in this browser. Please use Chrome or Edge.'); return; } if (voiceBtn.textContent.includes('Start')) { startVoiceInput(); } else { stopVoiceInput(); } } function startVoiceInput() { const voiceBtn = document.getElementById('voiceBtn'); voiceBtn.textContent = '🛑 Stop Listening'; voiceBtn.style.background = '#e74c3c'; recognition.start(); } function stopVoiceInput() { const voiceBtn = document.getElementById('voiceBtn'); voiceBtn.textContent = '🎤 Voice Input'; voiceBtn.style.background = '#9b59b6'; if (recognition) { recognition.stop(); } } async function submitAnswer() { const answer = document.getElementById('studentAnswer').value.trim(); if (!answer) { alert('Please enter your answer'); return; } try { const response = await fetch('/api/evaluate_answer', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ question_index: currentSession.questionIndex, answer: answer }) }); const data = await response.json(); displayFeedback(data); } catch (error) { console.error('Error submitting answer:', error); alert('Failed to submit answer. Please try again.'); } } function displayFeedback(data) { currentSession.totalScore += data.score; currentSession.questionCount++; document.getElementById('feedbackText').textContent = data.feedback; document.getElementById('scoreValue').textContent = data.score.toFixed(1); document.getElementById('totalScore').textContent = currentSession.totalScore.toFixed(1); document.getElementById('feedbackArea').classList.remove('hidden'); updateProgress(); addToHistory(data); } function addToHistory(data) { const historyContainer = document.getElementById('conversationHistory'); const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.innerHTML = ` Q${currentSession.questionCount}: ${currentSession.currentQuestion}
Your Answer: ${data.transcribed_answer || document.getElementById('studentAnswer').value.substring(0, 100)}...
Score: ${data.score.toFixed(1)}/10 `; historyContainer.appendChild(historyItem); historyContainer.scrollTop = historyContainer.scrollHeight; } function updateProgress() { const progressFill = document.querySelector('.progress-fill'); if (currentSession.questionCount > 0) { const progress = (currentSession.questionCount / 5) * 100; // Assuming 5 questions per topic progressFill.style.width = `${Math.min(progress, 100)}%`; } else { progressFill.style.width = '0%'; } } function nextQuestion() { // In a real implementation, this would get the next question from the server // For now, we'll simulate by ending the session after a few questions if (currentSession.questionCount >= 3) { endSession(); } else { // Simulate getting next question const nextQuestion = { question: `Next question about ${currentSession.topic}...`, question_index: currentSession.questionIndex + 1 }; displayQuestion(nextQuestion); } } function endSession() { const averageScore = currentSession.totalScore / currentSession.questionCount; alert(`Session completed! Your average score: ${averageScore.toFixed(1)}/10\nWell done!`); resetSession(); } function resetSession() { currentSession = { topic: null, currentQuestion: null, questionIndex: null, totalScore: 0, questionCount: 0 }; document.getElementById('vivaSession').classList.add('hidden'); document.getElementById('topicSelection').classList.remove('hidden'); document.getElementById('conversationHistory').innerHTML = ''; document.getElementById('totalScore').textContent = '0'; } // Utility function function base64ToBlob(base64, mimeType) { const byteCharacters = atob(base64); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); return new Blob([byteArray], { type: mimeType }); } // Initialize speech synthesis voices if ('speechSynthesis' in window) { speechSynthesis.getVoices(); }