fix(lesson): support quiz mode and improve responsive layout for mobile and laptop

This commit is contained in:
Antigravity committed 2026-10-07 13:50:13 +03:00
1 parent 640da1f27c
commit e0775fd0d4
3 files changed
+1210 -130

No files matched your search

+651 -91
View File
File diff suppressed because it is too large. Load diff
+549 -39
View File
@@ -80,6 +80,14 @@ function setupEventListeners() {
if (historyBtn) { if (historyBtn) {
historyBtn.addEventListener('click', () => openHistoryModal()); historyBtn.addEventListener('click', () => openHistoryModal());
} }
// Runner keyboard shortcuts listener
window.addEventListener('keydown', (e) => {
if (!session.active) return;
if (typeof session.currentStepKeyHandler === 'function') {
session.currentStepKeyHandler(e);
}
});
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -250,6 +258,7 @@ function getStepDefaultTitle(type) {
function renderStepControls(step, idx) { function renderStepControls(step, idx) {
if (step.type === 'vocab') { if (step.type === 'vocab') {
const currentMode = step.mode || 'flashcards';
return ` return `
<div class="step-param-row"> <div class="step-param-row">
<div class="param-group"> <div class="param-group">
@@ -260,14 +269,14 @@ function renderStepControls(step, idx) {
`).join('')} `).join('')}
</div> </div>
</div> </div>
<div class="param-group"> <div class="param-group flex-2">
<label>Режим тренировки:</label> <label>Режим тренировки слов:</label>
<select class="input-styled step-select-param" data-param="mode" data-idx="${idx}"> <div class="segmented-control mode-segmented-control" data-param="mode" data-idx="${idx}">
<option value="flashcards" ${step.mode === 'flashcards' ? 'selected' : ''}>📇 Карточки</option> <button type="button" class="seg-btn ${currentMode === 'flashcards' ? 'active' : ''}" data-val="flashcards">📇 Карточки</button>
<option value="quiz" ${step.mode === 'quiz' ? 'selected' : ''}>🎯 4 Варианта</option> <button type="button" class="seg-btn ${currentMode === 'quiz' ? 'active' : ''}" data-val="quiz">🎯 4 Варианта</button>
<option value="typing" ${step.mode === 'typing' ? 'selected' : ''}>✍️ Набор слова</option> <button type="button" class="seg-btn ${currentMode === 'typing' ? 'active' : ''}" data-val="typing">✍️ Набор</button>
<option value="dictation" ${step.mode === 'dictation' ? 'selected' : ''}>🎧 Диктант</option> <button type="button" class="seg-btn ${currentMode === 'dictation' ? 'active' : ''}" data-val="dictation">🎧 Диктант</button>
</select> </div>
</div> </div>
</div> </div>
`; `;
@@ -418,10 +427,14 @@ function attachStepControlsEvents(container) {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const idx = parseInt(ctrl.dataset.idx, 10); const idx = parseInt(ctrl.dataset.idx, 10);
const param = ctrl.dataset.param; const param = ctrl.dataset.param;
const val = parseInt(btn.dataset.val, 10); const rawVal = btn.dataset.val;
const val = (rawVal !== '' && !isNaN(Number(rawVal))) ? Number(rawVal) : rawVal;
if (customSteps[idx]) {
customSteps[idx][param] = val; customSteps[idx][param] = val;
}
ctrl.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active')); ctrl.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active'); btn.classList.add('active');
updateEstimatedDuration();
}); });
}); });
}); });
@@ -430,7 +443,45 @@ function attachStepControlsEvents(container) {
sel.addEventListener('change', () => { sel.addEventListener('change', () => {
const idx = parseInt(sel.dataset.idx, 10); const idx = parseInt(sel.dataset.idx, 10);
const param = sel.dataset.param; const param = sel.dataset.param;
if (customSteps[idx]) {
customSteps[idx][param] = sel.value; customSteps[idx][param] = sel.value;
}
updateEstimatedDuration();
});
});
}
function updateEstimatedDuration() {
const timeEl = document.getElementById('lesson-preset-duration');
if (timeEl) {
timeEl.textContent = `~${calculateEstimatedTime(customSteps)} мин`;
}
}
function syncDomToCustomSteps() {
const list = document.getElementById('lesson-steps-list');
if (!list) return;
list.querySelectorAll('.lesson-step-card').forEach(cardEl => {
const idx = parseInt(cardEl.dataset.idx, 10);
if (isNaN(idx) || !customSteps[idx]) return;
const cb = cardEl.querySelector('.step-toggle-checkbox');
if (cb) customSteps[idx].enabled = cb.checked;
cardEl.querySelectorAll('.segmented-control').forEach(ctrl => {
const param = ctrl.dataset.param;
const activeBtn = ctrl.querySelector('.seg-btn.active');
if (param && activeBtn) {
const rawVal = activeBtn.dataset.val;
customSteps[idx][param] = (rawVal !== '' && !isNaN(Number(rawVal))) ? Number(rawVal) : rawVal;
}
});
cardEl.querySelectorAll('.step-select-param').forEach(sel => {
const param = sel.dataset.param;
if (param && sel.value !== undefined) {
customSteps[idx][param] = sel.value;
}
}); });
}); });
} }
@@ -488,6 +539,9 @@ function calculateEstimatedTime(steps) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export async function startLessonSession(preset = null) { export async function startLessonSession(preset = null) {
if (!preset) {
syncDomToCustomSteps();
}
const stepsToRun = (preset ? preset.steps : customSteps).filter(s => s.enabled); const stepsToRun = (preset ? preset.steps : customSteps).filter(s => s.enabled);
if (!stepsToRun || stepsToRun.length === 0) { if (!stepsToRun || stepsToRun.length === 0) {
ui.showToast('В уроке нет активных блоков. Включите хотя бы один блок.', 'warning'); ui.showToast('В уроке нет активных блоков. Включите хотя бы один блок.', 'warning');
@@ -622,7 +676,7 @@ async function runVocabStep(step, container) {
// Fetch queue // Fetch queue
const res = await api.srs.getQueue({ limit: limit }); const res = await api.srs.getQueue({ limit: limit });
let cards = res.cards || []; let cards = res.cards || (Array.isArray(res) ? res : []);
if (cards.length === 0) { if (cards.length === 0) {
// Fallback: fetch random cards from library // Fallback: fetch random cards from library
@@ -630,6 +684,16 @@ async function runVocabStep(step, container) {
cards = Array.isArray(allRes) ? allRes : (allRes.cards || []); cards = Array.isArray(allRes) ? allRes : (allRes.cards || []);
} }
// Distractor candidate pool for quiz mode
let distractorPool = [];
if (mode === 'quiz') {
try {
const extraRes = await api.cards.getAll({ limit: 80 });
const extraCards = Array.isArray(extraRes) ? extraRes : (extraRes.cards || []);
distractorPool = extraCards.map(c => c.translation).filter(Boolean);
} catch (_) {}
}
if (cards.length === 0) { if (cards.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="step-card-box text-center"> <div class="step-card-box text-center">
@@ -645,17 +709,20 @@ async function runVocabStep(step, container) {
let cardIdx = 0; let cardIdx = 0;
let correctCount = 0; let correctCount = 0;
const totalCards = cards.length; const totalCards = cards.length;
let stepActive = true;
function renderCurrentCard() { function renderCurrentCard() {
if (cardIdx >= totalCards) { if (cardIdx >= totalCards) {
// Finished step stepActive = false;
session.currentStepKeyHandler = null;
const score = Math.round((correctCount / totalCards) * 100); const score = Math.round((correctCount / totalCards) * 100);
session.stepResults.push({ session.stepResults.push({
type: 'vocab', type: 'vocab',
title: step.title, title: step.title,
score: score, score: score,
correct: correctCount, correct: correctCount,
total: totalCards total: totalCards,
mode: mode
}); });
container.innerHTML = ` container.innerHTML = `
@@ -663,46 +730,228 @@ async function runVocabStep(step, container) {
<div class="celebrate-badge">🎉</div> <div class="celebrate-badge">🎉</div>
<h3>Блок слов завершён!</h3> <h3>Блок слов завершён!</h3>
<p class="score-highlight">Результат: <strong>${correctCount} из ${totalCards}</strong> (${score}%)</p> <p class="score-highlight">Результат: <strong>${correctCount} из ${totalCards}</strong> (${score}%)</p>
<button class="btn btn-primary btn-lg mt-3" onclick="window.advanceLessonStep()"> <div class="step-summary-badges mt-2">
<span class="badge badge-success">✓ Верно: ${correctCount}</span>
${totalCards - correctCount > 0 ? `<span class="badge badge-danger">✗ Ошибок: ${totalCards - correctCount}</span>` : ''}
</div>
<button class="btn btn-primary btn-lg mt-4" id="vocab-finish-btn">
Перейти к следующему блоку ➔ Перейти к следующему блоку ➔
</button> </button>
</div> </div>
`; `;
document.getElementById('vocab-finish-btn')?.addEventListener('click', () => {
advanceStep();
});
return; return;
} }
const card = cards[cardIdx]; const card = cards[cardIdx];
if (mode === 'quiz') {
renderQuizCard(card);
} else if (mode === 'typing') {
renderTypingCard(card);
} else if (mode === 'dictation') {
renderDictationCard(card);
} else {
renderFlashcardCard(card);
}
}
// -----------------------------------------------------------------------
// Mode A: 4 Options Quiz
// -----------------------------------------------------------------------
function renderQuizCard(card) {
const correctText = card.translation;
const candidates = [
...cards.filter(c => c.id !== card.id && c.translation).map(c => c.translation),
...distractorPool.filter(t => t && t !== correctText)
];
const uniqueCandidates = [...new Set(candidates.filter(t => t !== correctText))];
shuffleArray(uniqueCandidates);
const options = [correctText, ...uniqueCandidates.slice(0, 3)];
const fallbackOptions = ['определять', 'проверять', 'исследовать', 'сохранять', 'показывать', 'понимать', 'находить', 'создавать'];
while (options.length < 4) {
const f = fallbackOptions.find(o => !options.includes(o)) || `Вариант ${options.length + 1}`;
options.push(f);
}
shuffleArray(options);
let answered = false;
container.innerHTML = ` container.innerHTML = `
<div class="step-card-box"> <div class="step-card-box lesson-quiz-card">
<div class="step-mini-header"> <div class="step-mini-header">
<span class="badge">Слово ${cardIdx + 1} из ${totalCards}</span> <div class="step-tags-wrap">
<button class="icon-btn" id="srs-tts-btn" title="Озвучить слово">🔊</button> <span class="badge badge-primary">Слово ${cardIdx + 1} из ${totalCards}</span>
<span class="badge badge-outline">🎯 4 Варианта</span>
</div>
<button class="icon-btn audio-play-btn" id="srs-tts-btn" title="Озвучить слово">🔊</button>
</div> </div>
<div class="step-term-display"> <div class="step-term-display mt-3">
<h2 class="display-term">${escapeHtml(card.term)}</h2> <h2 class="display-term">${escapeHtml(card.term)}</h2>
${card.phonetic ? `<span class="display-phonetic">${escapeHtml(card.phonetic)}</span>` : ''} ${card.phonetic ? `<span class="display-phonetic">${escapeHtml(card.phonetic)}</span>` : ''}
</div> </div>
${card.context_sentence ? ` ${card.context_sentence ? `
<div class="step-context-box"> <div class="step-context-box mt-3">
<em class="context-quote">"${escapeHtml(card.context_sentence)}"</em>
</div>
` : ''}
<div class="lesson-quiz-options-grid mt-4">
${options.map((opt, oIdx) => `
<button type="button" class="lesson-quiz-opt-btn" data-val="${escapeHtml(opt)}" data-num="${oIdx + 1}">
<span class="quiz-opt-num">${oIdx + 1}</span>
<span class="quiz-opt-text">${escapeHtml(opt)}</span>
</button>
`).join('')}
</div>
<div id="lesson-quiz-feedback" class="trainer-feedback-box hidden mt-3"></div>
<div id="lesson-quiz-next-bar" class="hidden mt-3">
<button id="lesson-quiz-next-btn" class="btn btn-primary btn-lg w-100">
Продолжить ➔ (Enter или Пробел)
</button>
</div>
</div>
`;
const playCardAudio = () => {
if (card.audio_url) playAudio(card.audio_url);
else playTextSpeech(card.term, 'en');
};
document.getElementById('srs-tts-btn')?.addEventListener('click', playCardAudio);
const optBtns = container.querySelectorAll('.lesson-quiz-opt-btn');
const handleOptionClick = (btn) => {
if (answered) return;
answered = true;
const selectedText = btn.dataset.val;
const isCorrect = selectedText === correctText;
optBtns.forEach(b => {
b.disabled = true;
if (b.dataset.val === correctText) {
b.classList.add('opt-correct');
} else if (b === btn && !isCorrect) {
b.classList.add('opt-wrong');
}
});
playCardAudio();
const fb = document.getElementById('lesson-quiz-feedback');
const nextBar = document.getElementById('lesson-quiz-next-bar');
const nextBtn = document.getElementById('lesson-quiz-next-btn');
if (isCorrect) {
correctCount++;
if (fb) {
fb.className = 'trainer-feedback-box feedback-correct';
fb.innerHTML = '🎉 <strong>Отлично! Правильный перевод.</strong>';
fb.classList.remove('hidden');
}
api.srs.submitReview(card.id, 4).catch(() => {});
} else {
session.errors.push({
type: 'vocab',
term: card.term,
translation: card.translation,
context: card.context_sentence || '',
user_answer: selectedText,
mode: 'quiz'
});
if (fb) {
fb.className = 'trainer-feedback-box feedback-wrong';
fb.innerHTML = `❌ <strong>Ошибка!</strong> Правильно: <em>${escapeHtml(correctText)}</em>`;
fb.classList.remove('hidden');
}
api.srs.submitReview(card.id, 1).catch(() => {});
}
if (nextBar) nextBar.classList.remove('hidden');
if (nextBtn) nextBtn.focus();
};
optBtns.forEach(btn => {
btn.addEventListener('click', () => handleOptionClick(btn));
});
const nextAction = () => {
cardIdx++;
renderCurrentCard();
};
document.getElementById('lesson-quiz-next-btn')?.addEventListener('click', nextAction);
session.currentStepKeyHandler = (e) => {
if (!stepActive) return;
if (!answered) {
if (['1', '2', '3', '4'].includes(e.key)) {
e.preventDefault();
const n = parseInt(e.key, 10);
if (optBtns[n - 1]) handleOptionClick(optBtns[n - 1]);
} else if (e.code === 'KeyS' || e.code === 'KeyA') {
e.preventDefault();
playCardAudio();
}
} else {
if (e.key === 'Enter' || e.code === 'Space') {
e.preventDefault();
nextAction();
}
}
};
}
// -----------------------------------------------------------------------
// Mode B: Traditional Flashcards
// -----------------------------------------------------------------------
function renderFlashcardCard(card) {
let isFlipped = false;
container.innerHTML = `
<div class="step-card-box lesson-flashcard-card">
<div class="step-mini-header">
<div class="step-tags-wrap">
<span class="badge badge-primary">Слово ${cardIdx + 1} из ${totalCards}</span>
<span class="badge badge-outline">📇 Карточки</span>
</div>
<button class="icon-btn audio-play-btn" id="srs-tts-btn" title="Озвучить слово">🔊</button>
</div>
<div class="step-term-display mt-3">
<h2 class="display-term">${escapeHtml(card.term)}</h2>
${card.phonetic ? `<span class="display-phonetic">${escapeHtml(card.phonetic)}</span>` : ''}
</div>
${card.context_sentence ? `
<div class="step-context-box mt-3">
<em class="context-quote">"${escapeHtml(card.context_sentence)}"</em> <em class="context-quote">"${escapeHtml(card.context_sentence)}"</em>
</div> </div>
` : ''} ` : ''}
<!-- Reveal / Options --> <!-- Reveal / Options -->
<div id="vocab-answer-box" class="mt-4"> <div id="vocab-answer-box" class="mt-4">
<button id="vocab-show-btn" class="btn btn-outline btn-lg w-100">Показать перевод (Пробел)</button> <button id="vocab-show-btn" class="btn btn-outline btn-lg w-100">
👁️ Показать перевод (Пробел)
</button>
</div> </div>
<div id="vocab-rating-box" class="srs-actions-grid hidden mt-4"> <div id="vocab-rating-box" class="srs-actions-grid hidden mt-4">
<div class="translation-reveal-banner mb-3"> <div class="translation-reveal-banner mb-3">
<strong>${escapeHtml(card.translation)}</strong> <div class="tr-label text-muted text-sm">Перевод:</div>
<strong class="tr-text">${escapeHtml(card.translation)}</strong>
${card.definition_en ? `<div class="tr-definition text-muted mt-1">${escapeHtml(card.definition_en)}</div>` : ''}
</div> </div>
<div class="rating-buttons-row"> <div class="rating-buttons-row">
<button class="btn btn-danger srs-btn-rating" data-rating="1"> <button class="btn btn-danger srs-btn-rating" data-rating="1">
<span class="rating-key">1</span> <span class="rating-key">1</span>
<span class="rating-label">Снова (Забыл)</span> <span class="rating-label">Снова</span>
</button> </button>
<button class="btn btn-warning srs-btn-rating" data-rating="2"> <button class="btn btn-warning srs-btn-rating" data-rating="2">
<span class="rating-key">2</span> <span class="rating-key">2</span>
@@ -721,45 +970,306 @@ async function runVocabStep(step, container) {
</div> </div>
`; `;
// TTS button const playCardAudio = () => {
document.getElementById('srs-tts-btn')?.addEventListener('click', () => { if (card.audio_url) playAudio(card.audio_url);
if (card.audio_url) new Audio(card.audio_url).play().catch(() => {}); else playTextSpeech(card.term, 'en');
}); };
document.getElementById('srs-tts-btn')?.addEventListener('click', playCardAudio);
// Show button const flipCard = () => {
document.getElementById('vocab-show-btn')?.addEventListener('click', () => { if (isFlipped) return;
isFlipped = true;
document.getElementById('vocab-answer-box')?.classList.add('hidden'); document.getElementById('vocab-answer-box')?.classList.add('hidden');
document.getElementById('vocab-rating-box')?.classList.remove('hidden'); document.getElementById('vocab-rating-box')?.classList.remove('hidden');
}); playCardAudio();
};
// Rating buttons document.getElementById('vocab-show-btn')?.addEventListener('click', flipCard);
container.querySelectorAll('.srs-btn-rating').forEach(btn => {
btn.addEventListener('click', async () => { const handleRating = async (rating) => {
const rating = parseInt(btn.dataset.rating, 10);
if (rating >= 3) { if (rating >= 3) {
correctCount++; correctCount++;
} else { } else {
// Record error
session.errors.push({ session.errors.push({
type: 'vocab', type: 'vocab',
term: card.term, term: card.term,
translation: card.translation, translation: card.translation,
context: card.context_sentence || '', context: card.context_sentence || '',
rating: rating, rating: rating,
mode: 'flashcards'
}); });
} }
// Submit to SRS engine api.srs.submitReview(card.id, rating).catch(() => {});
try {
await api.srs.submitReview({ card_id: card.id, rating: rating });
} catch (e) {
console.warn('SRS review log deferred:', e);
}
cardIdx++; cardIdx++;
renderCurrentCard(); renderCurrentCard();
};
container.querySelectorAll('.srs-btn-rating').forEach(btn => {
btn.addEventListener('click', () => {
const r = parseInt(btn.dataset.rating, 10);
handleRating(r);
}); });
}); });
session.currentStepKeyHandler = (e) => {
if (!stepActive) return;
if (!isFlipped) {
if (e.code === 'Space' || e.key === 'Enter') {
e.preventDefault();
flipCard();
} else if (e.code === 'KeyS' || e.code === 'KeyA') {
e.preventDefault();
playCardAudio();
}
} else {
if (['1', '2', '3', '4'].includes(e.key)) {
e.preventDefault();
handleRating(parseInt(e.key, 10));
}
}
};
}
// -----------------------------------------------------------------------
// Mode C: Active Typing
// -----------------------------------------------------------------------
function renderTypingCard(card) {
let answered = false;
container.innerHTML = `
<div class="step-card-box lesson-typing-card">
<div class="step-mini-header">
<div class="step-tags-wrap">
<span class="badge badge-primary">Слово ${cardIdx + 1} из ${totalCards}</span>
<span class="badge badge-outline">✍️ Набор слова</span>
</div>
<button class="icon-btn audio-play-btn" id="srs-tts-btn" title="Озвучить слово">🔊</button>
</div>
<div class="step-prompt-display text-center mt-3">
<span class="text-muted text-sm">Переведите на английский:</span>
<h2 class="display-term display-prompt">${escapeHtml(card.translation)}</h2>
</div>
${card.context_sentence ? `
<div class="step-context-box mt-3 text-center">
<em class="context-quote">"${escapeHtml(card.context_sentence.replace(new RegExp(escapeRegex(card.term), 'gi'), '_____'))}"</em>
</div>
` : ''}
<form id="lesson-typing-form" class="mt-4">
<div class="typing-input-wrap">
<input type="text" id="lesson-typing-input" class="input-styled input-lg typing-main-input"
placeholder="Введите английское слово..." autocomplete="off" autocapitalize="off" spellcheck="false" autofocus>
<button type="submit" id="lesson-typing-submit-btn" class="btn btn-primary btn-lg">
Проверить (Enter)
</button>
</div>
</form>
<div id="lesson-typing-feedback" class="trainer-feedback-box hidden mt-3"></div>
<div id="lesson-typing-next-bar" class="hidden mt-3">
<button id="lesson-typing-next-btn" class="btn btn-primary btn-lg w-100">
Продолжить ➔ (Enter или Пробел)
</button>
</div>
</div>
`;
const inputEl = document.getElementById('lesson-typing-input');
inputEl?.focus();
const playCardAudio = () => {
if (card.audio_url) playAudio(card.audio_url);
else playTextSpeech(card.term, 'en');
};
document.getElementById('srs-tts-btn')?.addEventListener('click', playCardAudio);
const checkAnswer = () => {
if (answered) return;
answered = true;
const userVal = inputEl?.value.trim().toLowerCase() || '';
const targetVal = card.term.trim().toLowerCase();
const isMatch = userVal === targetVal;
if (inputEl) inputEl.disabled = true;
playCardAudio();
const fb = document.getElementById('lesson-typing-feedback');
const nextBar = document.getElementById('lesson-typing-next-bar');
const nextBtn = document.getElementById('lesson-typing-next-btn');
if (isMatch) {
correctCount++;
if (fb) {
fb.className = 'trainer-feedback-box feedback-correct';
fb.innerHTML = `✅ <strong>Верно!</strong> <em>${escapeHtml(card.term)}</em> ${card.phonetic ? `<code>${escapeHtml(card.phonetic)}</code>` : ''}`;
fb.classList.remove('hidden');
}
api.srs.submitReview(card.id, 4).catch(() => {});
} else {
session.errors.push({
type: 'vocab',
term: card.term,
translation: card.translation,
user_answer: userVal,
mode: 'typing'
});
if (fb) {
fb.className = 'trainer-feedback-box feedback-wrong';
fb.innerHTML = `❌ <strong>Ошибка:</strong> правильное написание — <strong>${escapeHtml(card.term)}</strong> ${card.phonetic ? `<code>${escapeHtml(card.phonetic)}</code>` : ''}`;
fb.classList.remove('hidden');
}
api.srs.submitReview(card.id, 1).catch(() => {});
}
if (nextBar) nextBar.classList.remove('hidden');
if (nextBtn) nextBtn.focus();
};
document.getElementById('lesson-typing-form')?.addEventListener('submit', (e) => {
e.preventDefault();
checkAnswer();
});
const nextAction = () => {
cardIdx++;
renderCurrentCard();
};
document.getElementById('lesson-typing-next-btn')?.addEventListener('click', nextAction);
session.currentStepKeyHandler = (e) => {
if (!stepActive) return;
if (answered && (e.key === 'Enter' || e.code === 'Space')) {
e.preventDefault();
nextAction();
}
};
}
// -----------------------------------------------------------------------
// Mode D: Dictation
// -----------------------------------------------------------------------
function renderDictationCard(card) {
let answered = false;
container.innerHTML = `
<div class="step-card-box lesson-dictation-card">
<div class="step-mini-header">
<div class="step-tags-wrap">
<span class="badge badge-primary">Слово ${cardIdx + 1} из ${totalCards}</span>
<span class="badge badge-outline">🎧 Диктант</span>
</div>
</div>
<div class="dictation-hero-play text-center mt-3">
<button id="dictation-big-audio-btn" class="btn btn-secondary btn-lg dictation-play-btn" type="button">
🔊 Прослушать слово (Пробел)
</button>
${card.translation ? `<p class="dictation-hint text-muted mt-2">Подсказка перевода: ${escapeHtml(card.translation)}</p>` : ''}
</div>
<form id="lesson-dictation-form" class="mt-4">
<div class="typing-input-wrap">
<input type="text" id="lesson-dictation-input" class="input-styled input-lg typing-main-input"
placeholder="Запишите услышанное английское слово..." autocomplete="off" autocapitalize="off" spellcheck="false" autofocus>
<button type="submit" class="btn btn-primary btn-lg">
Проверить (Enter)
</button>
</div>
</form>
<div id="lesson-dictation-feedback" class="trainer-feedback-box hidden mt-3"></div>
<div id="lesson-dictation-next-bar" class="hidden mt-3">
<button id="lesson-dictation-next-btn" class="btn btn-primary btn-lg w-100">
Продолжить ➔ (Enter или Пробел)
</button>
</div>
</div>
`;
const playCardAudio = () => {
if (card.audio_url) playAudio(card.audio_url);
else playTextSpeech(card.term, 'en');
};
setTimeout(playCardAudio, 200);
document.getElementById('dictation-big-audio-btn')?.addEventListener('click', playCardAudio);
const inputEl = document.getElementById('lesson-dictation-input');
inputEl?.focus();
const checkAnswer = () => {
if (answered) return;
answered = true;
const userVal = inputEl?.value.trim().toLowerCase() || '';
const targetVal = card.term.trim().toLowerCase();
const isMatch = userVal === targetVal;
if (inputEl) inputEl.disabled = true;
playCardAudio();
const fb = document.getElementById('lesson-dictation-feedback');
const nextBar = document.getElementById('lesson-dictation-next-bar');
const nextBtn = document.getElementById('lesson-dictation-next-btn');
if (isMatch) {
correctCount++;
if (fb) {
fb.className = 'trainer-feedback-box feedback-correct';
fb.innerHTML = `✅ <strong>Верно на слух!</strong> <em>${escapeHtml(card.term)}</em> ${card.phonetic ? `<code>${escapeHtml(card.phonetic)}</code>` : ''}`;
fb.classList.remove('hidden');
}
api.srs.submitReview(card.id, 4).catch(() => {});
} else {
session.errors.push({
type: 'vocab',
term: card.term,
translation: card.translation,
user_answer: userVal,
mode: 'dictation'
});
if (fb) {
fb.className = 'trainer-feedback-box feedback-wrong';
fb.innerHTML = `❌ <strong>Ошибка:</strong> правильное слово — <strong>${escapeHtml(card.term)}</strong> (${escapeHtml(card.translation)})`;
fb.classList.remove('hidden');
}
api.srs.submitReview(card.id, 1).catch(() => {});
}
if (nextBar) nextBar.classList.remove('hidden');
if (nextBtn) nextBtn.focus();
};
document.getElementById('lesson-dictation-form')?.addEventListener('submit', (e) => {
e.preventDefault();
checkAnswer();
});
const nextAction = () => {
cardIdx++;
renderCurrentCard();
};
document.getElementById('lesson-dictation-next-btn')?.addEventListener('click', nextAction);
session.currentStepKeyHandler = (e) => {
if (!stepActive) return;
if (answered && (e.key === 'Enter' || e.code === 'Space')) {
e.preventDefault();
nextAction();
} else if (!answered && e.code === 'Space' && document.activeElement !== inputEl) {
e.preventDefault();
playCardAudio();
}
};
} }
renderCurrentCard(); renderCurrentCard();
+10
View File
@@ -182,11 +182,21 @@ export function initSrs() {
startSrsTrainingSession(false); 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 => { srsTabs.forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
srsTabs.forEach(t => t.classList.remove('active')); srsTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active'); tab.classList.add('active');
srsCurrentMode = tab.dataset.mode; srsCurrentMode = tab.dataset.mode;
localStorage.setItem('srs_active_mode', srsCurrentMode);
renderSrsCurrentCard(); renderSrsCurrentCard();
}); });
}); });