// ======================================================================= // Notes Application - SRS Training & Analytics Module (Native ES Module) // ======================================================================= import { api } from './api.js'; import { escapeHtml, escapeRegex, shuffleArray, showToast, openModal, closeModal, playAudio, cleanPromptTranslation } from './ui.js'; import { getAllCards } from './dictionary.js'; let srsQueue = []; let srsCurrentIndex = 0; let srsCurrentMode = 'flashcards'; // 'flashcards' | 'quiz' | 'typing' | 'cloze' | 'dictation' let srsDirection = 'en-ru'; let srsSmartMix = true; let srsSessionRatings = []; let srsIsFlipped = false; let quizCurrentOptions = []; let quizAnswered = false; export function initSrs() { const srsView = document.getElementById('srs-view'); const srsExitBtn = document.getElementById('srs-exit-btn'); const srsTabs = document.querySelectorAll('.srs-tab'); const srsProgressFill = document.getElementById('srs-progress-fill'); const srsCounter = document.getElementById('srs-counter'); const srsDirBtn = document.getElementById('srs-dir-btn'); const srsDirLabel = document.getElementById('srs-dir-label'); const srsMixBtn = document.getElementById('srs-mix-btn'); const srsMixLabel = document.getElementById('srs-mix-label'); const srsLoading = document.getElementById('srs-loading'); const srsEmpty = document.getElementById('srs-empty'); const srsTrainAllBtn = document.getElementById('srs-train-all-btn'); const srsEmptyBackBtn = document.getElementById('srs-empty-back-btn'); // Mode 1: Flashcards const srsModeFlashcards = document.getElementById('srs-mode-flashcards'); const flashcardFlipWrap = document.getElementById('flashcard-flip-wrap'); const flashcardInner = document.getElementById('flashcard-inner'); const fcFrontDirBadge = document.getElementById('fc-front-dir-badge'); const fcFrontPos = document.getElementById('fc-front-pos'); const fcFrontMain = document.getElementById('fc-front-main'); const fcFrontPhoneticWrap = document.getElementById('fc-front-phonetic-wrap'); const fcFrontPhonetic = document.getElementById('fc-front-phonetic'); const fcFrontAudioBtn = document.getElementById('fc-front-audio-btn'); const fcHintBtn = document.getElementById('fc-hint-btn'); const fcHintBox = document.getElementById('fc-hint-box'); const fcBackPos = document.getElementById('fc-back-pos'); const fcBackTerm = document.getElementById('fc-back-term'); const fcBackAudioBtn = document.getElementById('fc-back-audio-btn'); const fcBackTranslation = document.getElementById('fc-back-translation'); const fcBackDefBox = document.getElementById('fc-back-def-box'); const fcBackDef = document.getElementById('fc-back-def'); const fcBackContextBox = document.getElementById('fc-back-context-box'); const fcBackContext = document.getElementById('fc-back-context'); const fcBackContextAudioBtn = document.getElementById('fc-back-context-audio-btn'); const fcBackNotesBox = document.getElementById('fc-back-notes-box'); const fcBackNotes = document.getElementById('fc-back-notes'); const fcFlipBtnWrap = document.getElementById('fc-flip-btn-wrap'); const fcFlipBtn = document.getElementById('fc-flip-btn'); const fcRatingBar = document.getElementById('fc-rating-bar'); // Mode 2: Multiple Choice Quiz const srsModeQuiz = document.getElementById('srs-mode-quiz'); const quizPos = document.getElementById('quiz-pos'); const quizTermDisplay = document.getElementById('quiz-term-display'); const quizPhoneticDisplay = document.getElementById('quiz-phonetic-display'); const quizContextHint = document.getElementById('quiz-context-hint'); const quizAudioBtn = document.getElementById('quiz-audio-btn'); const quizOptionsGrid = document.getElementById('quiz-options-grid'); const quizFeedbackBox = document.getElementById('quiz-feedback-box'); const quizDetailsBox = document.getElementById('quiz-details-box'); const quizNextBar = document.getElementById('quiz-next-bar'); const quizNextBtn = document.getElementById('quiz-next-btn'); // Mode 3: Active Typing const srsModeTyping = document.getElementById('srs-mode-typing'); const typingPos = document.getElementById('typing-pos'); const typingPromptRu = document.getElementById('typing-prompt-ru'); const typingContextHint = document.getElementById('typing-context-hint'); const typingForm = document.getElementById('typing-form'); const typingInput = document.getElementById('typing-input'); const typingFeedbackBox = document.getElementById('typing-feedback-box'); const typingDetailsBox = document.getElementById('typing-details-box'); const typingRatingBar = document.getElementById('typing-rating-bar'); // Mode 4: Cloze Deletion const srsModeCloze = document.getElementById('srs-mode-cloze'); const clozePos = document.getElementById('cloze-pos'); const clozeTargetTranslation = document.getElementById('cloze-target-translation'); const clozeSentenceDisplay = document.getElementById('cloze-sentence-display'); const clozeForm = document.getElementById('cloze-form'); const clozeInput = document.getElementById('cloze-input'); const clozeFeedbackBox = document.getElementById('cloze-feedback-box'); const clozeDetailsBox = document.getElementById('cloze-details-box'); const clozeRatingBar = document.getElementById('cloze-rating-bar'); // Mode 5: Dictation const srsModeDictation = document.getElementById('srs-mode-dictation'); const dictationBigPlayBtn = document.getElementById('dictation-big-play-btn'); const dictationReplayBtn = document.getElementById('dictation-replay-btn'); const dictationToggleHintBtn = document.getElementById('dictation-toggle-hint-btn'); const dictationHintText = document.getElementById('dictation-hint-text'); const dictationForm = document.getElementById('dictation-form'); const dictationInput = document.getElementById('dictation-input'); const dictationFeedbackBox = document.getElementById('dictation-feedback-box'); 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 listeningVoiceSelect = document.getElementById('listening-voice-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'); const sumRetention = document.getElementById('sum-retention'); const sumRepeatBtn = document.getElementById('sum-repeat-btn'); const sumReturnBtn = document.getElementById('sum-return-btn'); // Stats & Heatmap Modal const statsOpenBtn = document.getElementById('stats-open-btn'); const mobileStatsBtn = document.getElementById('mobile-stats-btn'); const drawerNavStats = document.getElementById('drawer-nav-stats'); const statsModal = document.getElementById('stats-modal'); const closeStatsModalBtn = document.getElementById('close-stats-modal-btn'); const closeStatsBtn = document.getElementById('close-stats-btn'); const kpiCurrentStreak = document.getElementById('kpi-current-streak'); const kpiLongestStreak = document.getElementById('kpi-longest-streak'); const kpiRetentionRate = document.getElementById('kpi-retention-rate'); const kpiTotalReviews = document.getElementById('kpi-total-reviews'); const kpiTotalWords = document.getElementById('kpi-total-words'); const kpiTodayAdded = document.getElementById('kpi-today-added'); const kpiCefrLevel = document.getElementById('kpi-cefr-level'); const kpiCefrDesc = document.getElementById('kpi-cefr-desc'); const cntMastered = document.getElementById('cnt-mastered'); const cntReview = document.getElementById('cnt-review'); const cntLearning = document.getElementById('cnt-learning'); const cntNew = document.getElementById('cnt-new'); const cntLeeches = document.getElementById('cnt-leeches'); const progMastered = document.getElementById('prog-mastered'); const progReview = document.getElementById('prog-review'); const progLearning = document.getElementById('prog-learning'); const progNew = document.getElementById('prog-new'); const heatmapGrid = document.getElementById('heatmap-grid'); // ----------------------------------------------------------------------- // View & Queue Controls // ----------------------------------------------------------------------- srsExitBtn.addEventListener('click', () => { window.location.hash = '#dictionary'; }); srsMixBtn.addEventListener('click', () => { srsSmartMix = !srsSmartMix; srsMixLabel.textContent = srsSmartMix ? '🎲 Микс дня: ВКЛ' : '📚 Обычная очередь'; srsMixBtn.classList.toggle('text-success', srsSmartMix); startSrsTrainingSession(false); }); // Restore saved SRS mode if present const savedSrsMode = localStorage.getItem('srs_active_mode'); if (savedSrsMode && ['flashcards', 'quiz', 'typing', 'cloze', 'dictation', 'listening'].includes(savedSrsMode)) { srsCurrentMode = savedSrsMode; srsTabs.forEach(t => { t.classList.toggle('active', t.dataset.mode === savedSrsMode); }); } srsTabs.forEach(tab => { tab.addEventListener('click', () => { srsTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); srsCurrentMode = tab.dataset.mode; localStorage.setItem('srs_active_mode', srsCurrentMode); renderSrsCurrentCard(); }); }); srsDirBtn.addEventListener('click', () => { srsDirection = srsDirection === 'en-ru' ? 'ru-en' : 'en-ru'; srsDirLabel.textContent = srsDirection === 'en-ru' ? 'EN ➔ RU' : 'RU ➔ EN'; renderSrsCurrentCard(); }); async function startSrsTrainingSession(forceAll = false, tagFilter = null) { srsLoading.classList.remove('hidden'); srsEmpty.classList.add('hidden'); srsSummary.classList.add('hidden'); hideAllSrsModeContainers(); srsSessionRatings = []; srsCurrentIndex = 0; let queue = []; try { const data = await api.srs.getQueue(forceAll, tagFilter); queue = data.cards || []; } catch (e) { console.error('SRS Queue error:', e); } const cards = getAllCards(); if (queue.length === 0 && forceAll && cards.length > 0) { queue = [...cards]; } srsLoading.classList.add('hidden'); if (queue.length === 0 && srsCurrentMode !== 'listening') { srsEmpty.classList.remove('hidden'); return; } srsQueue = queue; renderSrsCurrentCard(); } function closeSrsView() { window.location.hash = '#dictionary'; } srsTrainAllBtn.addEventListener('click', () => startSrsTrainingSession(true)); srsEmptyBackBtn.addEventListener('click', closeSrsView); sumRepeatBtn.addEventListener('click', () => startSrsTrainingSession(true)); sumReturnBtn.addEventListener('click', closeSrsView); function hideAllSrsModeContainers() { srsModeFlashcards.classList.add('hidden'); srsModeQuiz.classList.add('hidden'); 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; } srsEmpty.classList.add('hidden'); srsSummary.classList.add('hidden'); hideAllSrsModeContainers(); const total = srsQueue.length; const currentNum = srsCurrentIndex + 1; srsCounter.textContent = `Карточка ${currentNum} из ${total}`; const pct = Math.round(((currentNum - 1) / total) * 100); srsProgressFill.style.width = `${pct}%`; const card = srsQueue[srsCurrentIndex]; if (srsCurrentMode === 'flashcards') { renderFlashcardMode(card); } else if (srsCurrentMode === 'quiz') { renderQuizMode(card); } else if (srsCurrentMode === 'typing') { renderTypingMode(card); } else if (srsCurrentMode === 'cloze') { renderClozeMode(card); } else if (srsCurrentMode === 'dictation') { renderDictationMode(card); } } // ----------------------------------------------------------------------- // Mode 1: Flashcards // ----------------------------------------------------------------------- function renderFlashcardMode(card) { srsModeFlashcards.classList.remove('hidden'); srsIsFlipped = false; flashcardInner.classList.remove('is-flipped'); fcRatingBar.classList.add('hidden'); fcHintBox.classList.add('hidden'); fcFlipBtnWrap.style.display = 'block'; if (srsDirection === 'en-ru') { fcFrontDirBadge.textContent = 'EN ➔ RU'; fcFrontMain.textContent = card.term; fcFrontPos.textContent = card.part_of_speech || ''; fcFrontPos.style.display = card.part_of_speech ? 'inline-block' : 'none'; if (card.phonetic) { fcFrontPhonetic.textContent = card.phonetic; fcFrontPhoneticWrap.style.display = 'flex'; } else { fcFrontPhoneticWrap.style.display = 'none'; } fcHintBtn.style.display = card.context_sentence ? 'inline-flex' : 'none'; fcHintBox.textContent = card.context_sentence ? `“${card.context_sentence}”` : ''; fcBackTerm.textContent = card.term; fcBackTranslation.textContent = card.translation; fcBackPos.textContent = card.part_of_speech || ''; fcBackPos.style.display = card.part_of_speech ? 'inline-block' : 'none'; } else { fcFrontDirBadge.textContent = 'RU ➔ EN'; fcFrontMain.textContent = cleanPromptTranslation(card.translation); fcFrontPos.textContent = card.part_of_speech || ''; fcFrontPhoneticWrap.style.display = 'none'; fcHintBtn.style.display = 'none'; fcBackTerm.textContent = card.term; fcBackTranslation.textContent = card.translation; fcBackPos.textContent = card.part_of_speech || ''; } fcBackDef.textContent = card.definition_en || ''; fcBackDefBox.style.display = card.definition_en ? 'block' : 'none'; fcBackContext.textContent = card.context_sentence ? `“${card.context_sentence}”` : ''; fcBackContextBox.style.display = card.context_sentence ? 'block' : 'none'; fcBackNotes.textContent = card.notes || ''; fcBackNotesBox.style.display = card.notes ? 'block' : 'none'; if (fcFrontAudioBtn) fcFrontAudioBtn.style.display = 'inline-flex'; if (fcBackAudioBtn) fcBackAudioBtn.style.display = 'inline-flex'; if (fcBackContextAudioBtn) { fcBackContextAudioBtn.style.display = card.context_sentence ? 'inline-flex' : 'none'; } } if (fcFrontAudioBtn) { fcFrontAudioBtn.addEventListener('click', (e) => { e.stopPropagation(); const card = srsQueue[srsCurrentIndex]; if (!card) return; const url = card.audio_url || `/api/tts?text=${encodeURIComponent(card.term)}&lang=en`; playAudio(url); }); } if (fcBackAudioBtn) { fcBackAudioBtn.addEventListener('click', (e) => { e.stopPropagation(); const card = srsQueue[srsCurrentIndex]; if (!card) return; const url = card.audio_url || `/api/tts?text=${encodeURIComponent(card.term)}&lang=en`; playAudio(url); }); } if (fcBackContextAudioBtn) { fcBackContextAudioBtn.addEventListener('click', (e) => { e.stopPropagation(); const card = srsQueue[srsCurrentIndex]; if (!card || !card.context_sentence) return; const url = card.context_audio_url || `/api/tts?text=${encodeURIComponent(card.context_sentence)}&lang=en`; playAudio(url); }); } function flipFlashcard() { if (srsIsFlipped) return; srsIsFlipped = true; flashcardInner.classList.add('is-flipped'); fcRatingBar.classList.remove('hidden'); fcFlipBtnWrap.style.display = 'none'; } flashcardFlipWrap.addEventListener('click', flipFlashcard); fcFlipBtn.addEventListener('click', flipFlashcard); fcHintBtn.addEventListener('click', (e) => { e.stopPropagation(); fcHintBox.classList.toggle('hidden'); }); document.querySelectorAll('.btn-rate').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const rating = parseInt(btn.dataset.rating, 10); await submitCardReview(rating); }); }); async function submitCardReview(rating) { const card = srsQueue[srsCurrentIndex]; if (!card) return; srsSessionRatings.push(rating); try { await api.srs.submitReview(card.id, rating); } catch (e) { console.error('Review submission error:', e); } srsCurrentIndex++; renderSrsCurrentCard(); } // ----------------------------------------------------------------------- // Mode 2: Multiple Choice Quiz // ----------------------------------------------------------------------- function renderQuizMode(card) { srsModeQuiz.classList.remove('hidden'); quizAnswered = false; quizFeedbackBox.classList.add('hidden'); quizDetailsBox.classList.add('hidden'); quizNextBar.classList.add('hidden'); quizPos.textContent = card.part_of_speech || ''; quizTermDisplay.textContent = card.term; quizPhoneticDisplay.textContent = card.phonetic || ''; quizPhoneticDisplay.style.display = card.phonetic ? 'block' : 'none'; if (card.context_sentence) { quizContextHint.textContent = `“${card.context_sentence}”`; quizContextHint.classList.remove('hidden'); } else { quizContextHint.classList.add('hidden'); } if (quizAudioBtn) quizAudioBtn.classList.remove('hidden'); const correctText = cleanPromptTranslation(card.translation); const all = getAllCards(); const candidates = all .filter(c => c.id !== card.id && c.translation && cleanPromptTranslation(c.translation) !== correctText) .map(c => cleanPromptTranslation(c.translation)); shuffleArray(candidates); let options = [correctText, ...candidates.slice(0, 3)]; while (options.length < 4) { options.push(`Вариант ${options.length + 1}`); } options = shuffleArray(options); quizCurrentOptions = options; quizOptionsGrid.innerHTML = ''; options.forEach((optText, idx) => { const btn = document.createElement('button'); btn.className = 'quiz-option-btn'; btn.innerHTML = ` ${idx + 1} ${escapeHtml(optText)} `; btn.onclick = () => handleQuizSelection(optText, correctText, card, btn); quizOptionsGrid.appendChild(btn); }); } async function handleQuizSelection(selectedText, correctText, card, clickedBtn) { if (quizAnswered) return; quizAnswered = true; const isCorrect = selectedText === correctText; const allOptionBtns = quizOptionsGrid.querySelectorAll('.quiz-option-btn'); allOptionBtns.forEach(btn => { btn.disabled = true; const text = btn.querySelector('.quiz-opt-text').textContent; if (text === correctText) { btn.classList.add('opt-correct'); } else if (btn === clickedBtn && !isCorrect) { btn.classList.add('opt-wrong'); } }); if (card.audio_url) playAudio(card.audio_url); if (isCorrect) { quizFeedbackBox.className = 'trainer-feedback-box feedback-correct'; quizFeedbackBox.innerHTML = '🎉 Отлично! Правильный ответ.'; srsSessionRatings.push(4); api.srs.submitReview(card.id, 4); } else { quizFeedbackBox.className = 'trainer-feedback-box feedback-wrong'; quizFeedbackBox.innerHTML = `❌ Ошибка! Правильно: ${escapeHtml(correctText)}`; srsSessionRatings.push(1); api.srs.submitReview(card.id, 1); } quizFeedbackBox.classList.remove('hidden'); if (card.definition_en || card.context_sentence) { quizDetailsBox.innerHTML = ` ${card.definition_en ? `
Толкование: ${escapeHtml(card.definition_en)}
` : ''} ${card.context_sentence ? `Контекст: “${escapeHtml(card.context_sentence)}”
` : ''} `; quizDetailsBox.classList.remove('hidden'); } quizNextBar.classList.remove('hidden'); quizNextBtn.focus(); } if (quizAudioBtn) { quizAudioBtn.addEventListener('click', (e) => { e.stopPropagation(); const card = srsQueue[srsCurrentIndex]; if (!card) return; const url = card.audio_url || `/api/tts?text=${encodeURIComponent(card.term)}&lang=en`; playAudio(url); }); } quizNextBtn.addEventListener('click', () => { srsCurrentIndex++; renderSrsCurrentCard(); }); // ----------------------------------------------------------------------- // Mode 3: Active Typing // ----------------------------------------------------------------------- function renderTypingMode(card) { srsModeTyping.classList.remove('hidden'); typingFeedbackBox.classList.add('hidden'); typingDetailsBox.classList.add('hidden'); typingRatingBar.classList.add('hidden'); typingPos.textContent = card.part_of_speech || ''; typingPromptRu.textContent = cleanPromptTranslation(card.translation); if (card.context_sentence) { typingContextHint.textContent = `Контекст: “${card.context_sentence}”`; typingContextHint.classList.remove('hidden'); } else { typingContextHint.classList.add('hidden'); } typingInput.value = ''; typingInput.disabled = false; typingInput.focus(); } typingForm.addEventListener('submit', (e) => { e.preventDefault(); const card = srsQueue[srsCurrentIndex]; if (!card) return; const userInput = typingInput.value.trim().toLowerCase(); const target = card.term.trim().toLowerCase(); const isMatch = userInput === target; if (card.audio_url) playAudio(card.audio_url); typingInput.disabled = true; if (isMatch) { typingFeedbackBox.className = 'trainer-feedback-box feedback-correct'; typingFeedbackBox.innerHTML = `✅ Верно! ${escapeHtml(card.term)} ${card.phonetic ? `${escapeHtml(card.phonetic)}` : ''}`;
} else {
typingFeedbackBox.className = 'trainer-feedback-box feedback-wrong';
typingFeedbackBox.innerHTML = `❌ Не совсем: правильное написание — ${escapeHtml(card.term)} ${card.phonetic ? `${escapeHtml(card.phonetic)}` : ''}`;
}
typingFeedbackBox.classList.remove('hidden');
renderDynamicRatingBar(typingRatingBar, isMatch ? 3 : 1);
});
// -----------------------------------------------------------------------
// Mode 4: Cloze Deletion
// -----------------------------------------------------------------------
function renderClozeMode(card) {
srsModeCloze.classList.remove('hidden');
clozeFeedbackBox.classList.add('hidden');
clozeDetailsBox.classList.add('hidden');
clozeRatingBar.classList.add('hidden');
clozePos.textContent = card.part_of_speech || '';
clozeTargetTranslation.textContent = cleanPromptTranslation(card.translation);
const context = card.context_sentence || `The word is ${card.term}.`;
const re = new RegExp(escapeRegex(card.term), 'gi');
const gapText = context.replace(re, '_____');
clozeSentenceDisplay.innerHTML = gapText;
clozeInput.value = '';
clozeInput.disabled = false;
clozeInput.focus();
}
clozeForm.addEventListener('submit', (e) => {
e.preventDefault();
const card = srsQueue[srsCurrentIndex];
if (!card) return;
const userInput = clozeInput.value.trim().toLowerCase();
const target = card.term.trim().toLowerCase();
const isMatch = userInput === target;
if (card.audio_url) playAudio(card.audio_url);
clozeInput.disabled = true;
if (isMatch) {
clozeFeedbackBox.className = 'trainer-feedback-box feedback-correct';
clozeFeedbackBox.innerHTML = `✅ Верно! целевое слово: ${escapeHtml(card.term)}`;
} else {
clozeFeedbackBox.className = 'trainer-feedback-box feedback-wrong';
clozeFeedbackBox.innerHTML = `❌ Правильный ответ: ${escapeHtml(card.term)}`;
}
clozeFeedbackBox.classList.remove('hidden');
renderDynamicRatingBar(clozeRatingBar, isMatch ? 3 : 1);
});
// -----------------------------------------------------------------------
// Mode 5: Dictation
// -----------------------------------------------------------------------
function renderDictationMode(card) {
srsModeDictation.classList.remove('hidden');
dictationFeedbackBox.classList.add('hidden');
dictationDetailsBox.classList.add('hidden');
dictationRatingBar.classList.add('hidden');
dictationHintText.classList.add('hidden');
dictationHintText.textContent = cleanPromptTranslation(card.translation);
dictationInput.value = '';
dictationInput.disabled = false;
const play = () => { if (card.audio_url) playAudio(card.audio_url); };
dictationBigPlayBtn.onclick = play;
dictationReplayBtn.onclick = play;
play();
dictationInput.focus();
}
dictationToggleHintBtn.addEventListener('click', () => {
dictationHintText.classList.toggle('hidden');
});
dictationForm.addEventListener('submit', (e) => {
e.preventDefault();
const card = srsQueue[srsCurrentIndex];
if (!card) return;
const userInput = dictationInput.value.trim().toLowerCase();
const target = card.term.trim().toLowerCase();
const isMatch = userInput === target;
dictationInput.disabled = true;
if (isMatch) {
dictationFeedbackBox.className = 'trainer-feedback-box feedback-correct';
dictationFeedbackBox.innerHTML = `✅ Идеально! ${escapeHtml(card.term)} ${card.phonetic ? `${escapeHtml(card.phonetic)}` : ''} — ${escapeHtml(card.translation)}`;
} else {
dictationFeedbackBox.className = 'trainer-feedback-box feedback-wrong';
dictationFeedbackBox.innerHTML = `❌ Ошибка спеллинга: правильное слово — ${escapeHtml(card.term)} ${card.phonetic ? `${escapeHtml(card.phonetic)}` : ''}`;
}
dictationFeedbackBox.classList.remove('hidden');
renderDynamicRatingBar(dictationRatingBar, isMatch ? 3 : 1);
});
function renderDynamicRatingBar(container, defaultRating) {
container.innerHTML = `
`;
container.classList.remove('hidden');
container.querySelectorAll('.btn-rate').forEach(btn => {
btn.onclick = async () => {
const r = parseInt(btn.dataset.rating, 10);
await submitCardReview(r);
};
});
}
// -----------------------------------------------------------------------
// 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';
const voice = listeningVoiceSelect ? listeningVoiceSelect.value : (localStorage.getItem('notes_tts_voice_en') || 'en_US-lessac');
try {
const data = await api.listening.getExercise(topic, 'B1', voice);
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 (listeningVoiceSelect) {
listeningVoiceSelect.value = localStorage.getItem('notes_tts_voice_en') || 'en_US-lessac';
listeningVoiceSelect.addEventListener('change', () => {
localStorage.setItem('notes_tts_voice_en', listeningVoiceSelect.value);
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 = `🎉 ${escapeHtml(res.feedback)}`;
srsSessionRatings.push(4);
} else {
listeningFeedbackBox.className = 'trainer-feedback-box feedback-wrong';
listeningFeedbackBox.innerHTML = `${escapeHtml(res.feedback)}`;
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');
srsProgressFill.style.width = '100%';
srsCounter.textContent = 'Тренировка завершена!';
const total = srsSessionRatings.length;
sumCount.textContent = total;
const goodCount = srsSessionRatings.filter(r => r >= 3).length;
const retention = total > 0 ? Math.round((goodCount / total) * 100) : 100;
sumRetention.textContent = `${retention}%`;
}
// Hotkeys handler
document.addEventListener('keydown', (e) => {
if (!srsView.classList.contains('hidden')) {
if (srsCurrentMode === 'flashcards') {
if (e.code === 'Space' && !srsIsFlipped) {
e.preventDefault();
flipFlashcard();
} else if (srsIsFlipped && ['1', '2', '3', '4'].includes(e.key)) {
e.preventDefault();
submitCardReview(parseInt(e.key, 10));
}
} else if (srsCurrentMode === 'quiz' && !quizAnswered) {
if (['1', '2', '3', '4'].includes(e.key)) {
const idx = parseInt(e.key, 10) - 1;
const btns = quizOptionsGrid.querySelectorAll('.quiz-option-btn');
if (btns[idx]) {
e.preventDefault();
btns[idx].click();
}
}
} else if (srsCurrentMode === 'quiz' && quizAnswered) {
if (e.key === 'Enter' || e.code === 'Space') {
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();
}
}
}
}
});
// -----------------------------------------------------------------------
// Analytics Dashboard & Heatmap Modal
// -----------------------------------------------------------------------
if (statsOpenBtn) statsOpenBtn.addEventListener('click', openStatsDashboard);
if (mobileStatsBtn) mobileStatsBtn.addEventListener('click', openStatsDashboard);
if (drawerNavStats) drawerNavStats.addEventListener('click', openStatsDashboard);
closeStatsModalBtn.addEventListener('click', () => closeModal(statsModal));
closeStatsBtn.addEventListener('click', () => closeModal(statsModal));
async function openStatsDashboard() {
openModal(statsModal);
await Promise.all([loadStatsKpis(), loadActivityHeatmap()]);
}
async function loadStatsKpis() {
try {
const s = await api.stats.getSrsStats();
if (s) {
kpiCurrentStreak.textContent = `🔥 ${s.streak_days}`;
kpiLongestStreak.textContent = `Рекорд: ${s.longest_streak} дн.`;
kpiRetentionRate.textContent = `${s.retention_rate}%`;
kpiTotalReviews.textContent = `Всего повторений: ${s.total_reviews}`;
kpiTotalWords.textContent = s.total_cards;
kpiTodayAdded.textContent = `Сегодня добавлено: +${s.today_added}`;
if (s.cefr) {
kpiCefrLevel.textContent = s.cefr.level;
kpiCefrDesc.textContent = `${s.cefr.title} • ${s.cefr.message}`;
} else {
const count = s.total_cards;
let lvl = 'A1';
let desc = 'Базовый словарный запас';
if (count >= 2500) { lvl = 'C1'; desc = 'Продвинутый свободный уровень'; }
else if (count >= 1200) { lvl = 'B2'; desc = 'Уверенное чтение тех. документации'; }
else if (count >= 600) { lvl = 'B1'; desc = 'Понимание общих и рабочих тем'; }
else if (count >= 200) { lvl = 'A2'; desc = 'Элементарный словарный запас'; }
kpiCefrLevel.textContent = lvl;
kpiCefrDesc.textContent = desc;
}
cntMastered.textContent = s.mastered;
cntReview.textContent = s.review;
cntLearning.textContent = s.learning;
cntNew.textContent = s.new_cards;
cntLeeches.textContent = s.leech_count;
const total = s.total_cards || 1;
progMastered.style.width = `${(s.mastered / total) * 100}%`;
progReview.style.width = `${(s.review / total) * 100}%`;
progLearning.style.width = `${(s.learning / total) * 100}%`;
progNew.style.width = `${(s.new_cards / total) * 100}%`;
}
} catch (e) {
console.error('Stats load error:', e);
}
}
async function loadActivityHeatmap() {
try {
const data = await api.stats.getHeatmap(180);
renderHeatmap(data.days);
} catch (e) {
console.error('Heatmap load error:', e);
}
}
function renderHeatmap(days) {
heatmapGrid.innerHTML = '';
if (!days || days.length === 0) return;
days.forEach(d => {
const cell = document.createElement('div');
cell.className = `heatmap-cell lvl-${d.level}`;
cell.title = `${d.date}: ${d.count} повторений / тестов`;
heatmapGrid.appendChild(cell);
});
}
initSrs.startSession = startSrsTrainingSession;
initSrs.openStats = openStatsDashboard;
}
export function startSrsTrainingSession(forceAll = false, tagFilter = null) {
if (initSrs.startSession) {
return initSrs.startSession(forceAll, tagFilter);
}
}
export function openStatsDashboard() {
if (initSrs.openStats) {
return initSrs.openStats();
}
}