feat(listening): interactive listening comprehension trainer with neural voiceover and auto-evaluation

This commit is contained in:
2026-10-07 10:57:10 +03:00
parent a6fb4349e9
commit 7893b353aa
8 changed files with 1026 additions and 3 deletions
+23
View File
@@ -183,6 +183,29 @@ export const api = {
}
},
// Listening Comprehension Trainer
listening: {
async getExercise(topic = 'all', level = 'B1') {
const resp = await fetch(`/api/listening/exercise?topic=${encodeURIComponent(topic)}&level=${encodeURIComponent(level)}`);
if (resp.ok) return await resp.json();
throw new Error(`Failed to load listening exercise: ${resp.status}`);
},
async checkAnswer(scenarioId, userAnswer, story = '', idealAnswer = '') {
const resp = await fetch('/api/listening/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scenario_id: scenarioId,
user_answer: userAnswer,
story: story,
ideal_answer: idealAnswer
})
});
if (resp.ok) return await resp.json();
throw new Error(`Failed to check listening answer: ${resp.status}`);
}
},
// Bookshelf & Reader
books: {
async getAll() {
+222 -1
View File
@@ -106,6 +106,34 @@ export function initSrs() {
const dictationDetailsBox = document.getElementById('dictation-details-box');
const dictationRatingBar = document.getElementById('dictation-rating-bar');
// Mode 6: Listening Comprehension
const srsModeListening = document.getElementById('srs-mode-listening');
const listeningLevelBadge = document.getElementById('listening-level-badge');
const listeningTopicBadge = document.getElementById('listening-topic-badge');
const listeningTopicSelect = document.getElementById('listening-topic-select');
const listeningSpeedSelect = document.getElementById('listening-speed-select');
const listeningNewScenarioBtn = document.getElementById('listening-new-scenario-btn');
const listeningTitle = document.getElementById('listening-title');
const listeningPlayStoryBtn = document.getElementById('listening-play-story-btn');
const listeningPlayQuestionBtn = document.getElementById('listening-play-question-btn');
const listeningToggleHintBtn = document.getElementById('listening-toggle-hint-btn');
const listeningHintText = document.getElementById('listening-hint-text');
const listeningForm = document.getElementById('listening-form');
const listeningInput = document.getElementById('listening-input');
const listeningSubmitBtn = document.getElementById('listening-submit-btn');
const listeningFeedbackBox = document.getElementById('listening-feedback-box');
const listeningRevealedBox = document.getElementById('listening-revealed-box');
const listeningIdealAnswer = document.getElementById('listening-ideal-answer');
const listeningRevealedQuestion = document.getElementById('listening-revealed-question');
const listeningRevealedQuestionRu = document.getElementById('listening-revealed-question-ru');
const listeningRevealedStory = document.getElementById('listening-revealed-story');
const listeningRevealedStoryRu = document.getElementById('listening-revealed-story-ru');
const listeningNextBtn = document.getElementById('listening-next-btn');
let currentListeningExercise = null;
let listeningAnswered = false;
let listeningCompletedCount = 0;
// Summary
const srsSummary = document.getElementById('srs-summary');
const sumCount = document.getElementById('sum-count');
@@ -192,7 +220,7 @@ export function initSrs() {
srsLoading.classList.add('hidden');
if (queue.length === 0) {
if (queue.length === 0 && srsCurrentMode !== 'listening') {
srsEmpty.classList.remove('hidden');
return;
}
@@ -216,9 +244,18 @@ export function initSrs() {
srsModeTyping.classList.add('hidden');
srsModeCloze.classList.add('hidden');
srsModeDictation.classList.add('hidden');
if (srsModeListening) srsModeListening.classList.add('hidden');
}
function renderSrsCurrentCard() {
if (srsCurrentMode === 'listening') {
srsEmpty.classList.add('hidden');
srsSummary.classList.add('hidden');
hideAllSrsModeContainers();
renderListeningMode();
return;
}
if (srsCurrentIndex >= srsQueue.length) {
showSrsSummary();
return;
@@ -644,6 +681,178 @@ export function initSrs() {
});
}
// -----------------------------------------------------------------------
// Mode 6: Listening Comprehension
// -----------------------------------------------------------------------
function renderListeningMode() {
if (!srsModeListening) return;
srsModeListening.classList.remove('hidden');
srsCounter.textContent = `📻 Аудирование: решено ${listeningCompletedCount}`;
srsProgressFill.style.width = '100%';
if (!currentListeningExercise) {
loadListeningExercise();
}
}
async function loadListeningExercise(forceNew = false) {
if (!srsModeListening) return;
listeningAnswered = false;
if (listeningFeedbackBox) {
listeningFeedbackBox.classList.add('hidden');
listeningFeedbackBox.innerHTML = '';
}
if (listeningRevealedBox) listeningRevealedBox.classList.add('hidden');
if (listeningHintText) {
listeningHintText.classList.add('hidden');
listeningHintText.textContent = '';
}
if (listeningInput) {
listeningInput.value = '';
listeningInput.disabled = false;
}
if (listeningSubmitBtn) listeningSubmitBtn.disabled = false;
if (listeningTitle) listeningTitle.textContent = 'Загрузка аудио-ситуации...';
const topic = listeningTopicSelect ? listeningTopicSelect.value : 'all';
try {
const data = await api.listening.getExercise(topic, 'B1');
currentListeningExercise = data;
if (listeningTitle) listeningTitle.textContent = data.title || 'Ситуация для аудирования';
if (listeningLevelBadge) {
listeningLevelBadge.textContent = data.level || 'B1';
listeningLevelBadge.className = `level-badge badge-${(data.level || 'b1').toLowerCase()}`;
}
if (listeningTopicBadge) {
listeningTopicBadge.textContent = (data.topic || 'tech').toUpperCase();
}
if (data.hint && listeningToggleHintBtn && listeningHintText) {
listeningToggleHintBtn.classList.remove('hidden');
listeningHintText.textContent = data.hint;
} else if (listeningToggleHintBtn) {
listeningToggleHintBtn.classList.add('hidden');
}
// Smooth focus on input
setTimeout(() => {
if (listeningInput && !srsModeListening.classList.contains('hidden')) {
listeningInput.focus();
}
}, 50);
// Auto-play the story audio
const speed = listeningSpeedSelect ? parseFloat(listeningSpeedSelect.value) || 1.0 : 1.0;
if (data.story_audio_url) {
playAudio(data.story_audio_url, speed);
}
} catch (e) {
console.error('Failed to load listening exercise:', e);
if (listeningTitle) listeningTitle.textContent = 'Не удалось загрузить ситуацию. Нажмите "Другая история".';
showToast('Ошибка загрузки аудирования', 'error');
}
}
if (listeningPlayStoryBtn) {
listeningPlayStoryBtn.addEventListener('click', () => {
if (!currentListeningExercise || !currentListeningExercise.story_audio_url) return;
const speed = listeningSpeedSelect ? parseFloat(listeningSpeedSelect.value) || 1.0 : 1.0;
playAudio(currentListeningExercise.story_audio_url, speed);
});
}
if (listeningPlayQuestionBtn) {
listeningPlayQuestionBtn.addEventListener('click', () => {
if (!currentListeningExercise || !currentListeningExercise.question_audio_url) return;
const speed = listeningSpeedSelect ? parseFloat(listeningSpeedSelect.value) || 1.0 : 1.0;
playAudio(currentListeningExercise.question_audio_url, speed);
});
}
if (listeningToggleHintBtn) {
listeningToggleHintBtn.addEventListener('click', () => {
if (listeningHintText) listeningHintText.classList.toggle('hidden');
});
}
if (listeningNewScenarioBtn) {
listeningNewScenarioBtn.addEventListener('click', () => {
loadListeningExercise(true);
});
}
if (listeningTopicSelect) {
listeningTopicSelect.addEventListener('change', () => {
loadListeningExercise(true);
});
}
if (listeningForm) {
listeningForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (listeningAnswered || !currentListeningExercise) return;
const val = (listeningInput ? listeningInput.value : '').trim();
if (!val) {
showToast('Пожалуйста, напишите ваш ответ', 'info');
if (listeningInput) listeningInput.focus();
return;
}
listeningAnswered = true;
if (listeningInput) listeningInput.disabled = true;
if (listeningSubmitBtn) listeningSubmitBtn.disabled = true;
try {
const res = await api.listening.checkAnswer(
currentListeningExercise.id,
val,
currentListeningExercise.story,
currentListeningExercise.ideal_answer || ''
);
if (listeningFeedbackBox) {
if (res.is_correct) {
listeningFeedbackBox.className = 'trainer-feedback-box feedback-correct';
listeningFeedbackBox.innerHTML = `🎉 <strong>${escapeHtml(res.feedback)}</strong>`;
srsSessionRatings.push(4);
} else {
listeningFeedbackBox.className = 'trainer-feedback-box feedback-wrong';
listeningFeedbackBox.innerHTML = `<strong>${escapeHtml(res.feedback)}</strong>`;
srsSessionRatings.push(2);
}
listeningFeedbackBox.classList.remove('hidden');
}
// Reveal details
if (listeningIdealAnswer) listeningIdealAnswer.textContent = res.ideal_answer || '';
if (listeningRevealedQuestion) listeningRevealedQuestion.textContent = res.question || currentListeningExercise.question || '';
if (listeningRevealedQuestionRu) listeningRevealedQuestionRu.textContent = res.question_ru || currentListeningExercise.question_ru || '';
if (listeningRevealedStory) listeningRevealedStory.textContent = res.story || currentListeningExercise.story || '';
if (listeningRevealedStoryRu) listeningRevealedStoryRu.textContent = res.story_ru || currentListeningExercise.story_ru || '';
if (listeningRevealedBox) listeningRevealedBox.classList.remove('hidden');
listeningCompletedCount++;
srsCounter.textContent = `📻 Аудирование: решено ${listeningCompletedCount}`;
if (listeningNextBtn) listeningNextBtn.focus();
} catch (err) {
console.error('Listening answer check error:', err);
if (listeningInput) listeningInput.disabled = false;
if (listeningSubmitBtn) listeningSubmitBtn.disabled = false;
listeningAnswered = false;
showToast('Ошибка проверки ответа', 'error');
}
});
}
if (listeningNextBtn) {
listeningNextBtn.addEventListener('click', () => {
loadListeningExercise(true);
});
}
function showSrsSummary() {
hideAllSrsModeContainers();
srsSummary.classList.remove('hidden');
@@ -682,6 +891,18 @@ export function initSrs() {
e.preventDefault();
quizNextBtn.click();
}
} else if (srsCurrentMode === 'listening') {
if (listeningAnswered) {
if (e.key === 'Enter') {
e.preventDefault();
if (listeningNextBtn) listeningNextBtn.click();
}
} else {
if (document.activeElement !== listeningInput && e.code === 'Space') {
e.preventDefault();
if (listeningPlayStoryBtn) listeningPlayStoryBtn.click();
}
}
}
}
});