Spaces:
Runtime error
Runtime error
| 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 = ` | |
| <strong>Q${currentSession.questionCount}:</strong> ${currentSession.currentQuestion}<br> | |
| <strong>Your Answer:</strong> ${data.transcribed_answer || document.getElementById('studentAnswer').value.substring(0, 100)}...<br> | |
| <strong>Score:</strong> ${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(); | |
| } |