fix & feat: resolve user reports #1 through #8 (TTS voice & speed, modals, scrollbar, CEFR hint, SRS grammar exclusion, global word lookup)
This commit is contained in:
@@ -4,19 +4,22 @@
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './api.js';
|
||||
import { showToast, escapeHtml } from './ui.js';
|
||||
import { showToast, escapeHtml, playTextSpeech } from './ui.js';
|
||||
|
||||
let currentLevel = 'A1';
|
||||
let currentMode = 'all';
|
||||
let currentQuiz = [];
|
||||
let currentQuestionIdx = 0;
|
||||
let isAnswered = false;
|
||||
let hintUsedThisQuestion = false;
|
||||
|
||||
const sessionStats = {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
mistakes: 0,
|
||||
ingested: 0,
|
||||
score: 0,
|
||||
hintsUsed: 0,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -391,6 +394,8 @@ export async function startCefrQuiz(level = 'A1', mode = 'all') {
|
||||
sessionStats.correct = 0;
|
||||
sessionStats.mistakes = 0;
|
||||
sessionStats.ingested = 0;
|
||||
sessionStats.score = 0;
|
||||
sessionStats.hintsUsed = 0;
|
||||
|
||||
showCefrScreen('quiz');
|
||||
|
||||
@@ -459,6 +464,8 @@ function renderCurrentQuestion() {
|
||||
const cardContainer = document.getElementById('cefr-cloze-card');
|
||||
if (!cardContainer) return;
|
||||
|
||||
hintUsedThisQuestion = false;
|
||||
|
||||
// Stylize the sentence blank: replace ___ with a visible blank node
|
||||
const formattedSentence = escapeHtml(q.sentence).replace(/___/g, '<span class="cefr-blank" id="cefr-blank-target">_____</span>');
|
||||
|
||||
@@ -473,10 +480,15 @@ function renderCurrentQuestion() {
|
||||
<p class="cefr-sentence-text">${formattedSentence}</p>
|
||||
</div>
|
||||
|
||||
<div class="cefr-clue-box">
|
||||
<span class="clue-icon">💡</span>
|
||||
<span class="clue-label">Значение:</span>
|
||||
<strong class="clue-term">${escapeHtml(q.clue)}</strong>
|
||||
<div class="cefr-clue-box" id="cefr-clue-box">
|
||||
<button type="button" class="btn btn-xs btn-outline cefr-hint-btn" id="cefr-hint-btn" title="Показать подсказку с переводом слова (-0.2 балла)">
|
||||
💡 Подсказка (-0.2 балла)
|
||||
</button>
|
||||
<div id="cefr-hint-content" class="cefr-hint-content hidden">
|
||||
<span class="clue-icon">💡</span>
|
||||
<span class="clue-label">Значение:</span>
|
||||
<strong class="clue-term">${escapeHtml(q.clue)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cefr-options-grid" id="cefr-options-grid">
|
||||
@@ -491,6 +503,17 @@ function renderCurrentQuestion() {
|
||||
<div id="cefr-feedback-box" class="cefr-feedback-box hidden"></div>
|
||||
`;
|
||||
|
||||
// Hook up hint toggle button
|
||||
const hintBtn = cardContainer.querySelector('#cefr-hint-btn');
|
||||
const hintContent = cardContainer.querySelector('#cefr-hint-content');
|
||||
if (hintBtn && hintContent) {
|
||||
hintBtn.addEventListener('click', () => {
|
||||
hintUsedThisQuestion = true;
|
||||
hintBtn.classList.add('hidden');
|
||||
hintContent.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Attach click listeners to option buttons
|
||||
cardContainer.querySelectorAll('.cefr-opt-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -524,6 +547,10 @@ async function handleOptionTap(questionId, selectedOption, clickedBtn) {
|
||||
|
||||
if (res.correct) {
|
||||
sessionStats.correct++;
|
||||
const pts = hintUsedThisQuestion ? 0.8 : 1.0;
|
||||
sessionStats.score += pts;
|
||||
if (hintUsedThisQuestion) sessionStats.hintsUsed++;
|
||||
|
||||
clickedBtn.classList.add('opt-correct');
|
||||
if (blankTarget) {
|
||||
blankTarget.textContent = res.correct_option;
|
||||
@@ -533,16 +560,28 @@ async function handleOptionTap(questionId, selectedOption, clickedBtn) {
|
||||
if (feedbackBox) {
|
||||
feedbackBox.className = 'cefr-feedback-box feedback-correct';
|
||||
feedbackBox.innerHTML = `
|
||||
<div class="fb-title">✓ Верно! Отличный выбор.</div>
|
||||
<div class="fb-meta">
|
||||
<div class="fb-title">✓ Верно! ${pts === 0.8 ? 'Получено: <strong>+0.8 балла</strong> (с подсказкой)' : 'Получено: <strong>+1.0 балл</strong>'}</div>
|
||||
<div class="fb-meta" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||
<strong class="fb-term">${escapeHtml(res.correct_option)}</strong>
|
||||
${res.phonetic ? `<span class="res-phonetic">${escapeHtml(res.phonetic)}</span>` : ''}
|
||||
<span class="audio-ctrl-group">
|
||||
<button type="button" class="icon-btn audio-xs-btn play-cefr-word-audio-btn" title="Озвучить слово (1.0x)">🔊</button>
|
||||
<button type="button" class="icon-btn audio-xs-btn audio-slow-btn play-cefr-word-slow-btn" title="Озвучить медленно (0.7x)">🐢 0.7x</button>
|
||||
</span>
|
||||
</div>
|
||||
${res.source_book_title ? `<div class="fb-source-citation">📖 Пример из книги «${escapeHtml(res.source_book_title)}»${res.source_chapter_title ? ` (${escapeHtml(res.source_chapter_title)})` : ''}</div>` : ''}
|
||||
${res.explanation ? `<p class="fb-explanation">${escapeHtml(res.explanation)}</p>` : ''}
|
||||
${renderOptionsBreakdown(res.options_breakdown)}
|
||||
`;
|
||||
feedbackBox.classList.remove('hidden');
|
||||
feedbackBox.querySelector('.play-cefr-word-audio-btn')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
playTextSpeech(res.correct_option, 'en', 1.0);
|
||||
});
|
||||
feedbackBox.querySelector('.play-cefr-word-slow-btn')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
playTextSpeech(res.correct_option, 'en', 0.75);
|
||||
});
|
||||
attachBreakdownListeners(feedbackBox, sentence);
|
||||
}
|
||||
} else {
|
||||
@@ -550,6 +589,12 @@ async function handleOptionTap(questionId, selectedOption, clickedBtn) {
|
||||
// Two words were ingested: the correct target word and the clicked distractor
|
||||
sessionStats.ingested += 2;
|
||||
|
||||
// Automatically reveal clue on mistake so user sees the meaning
|
||||
const hBtn = document.getElementById('cefr-hint-btn');
|
||||
const hCnt = document.getElementById('cefr-hint-content');
|
||||
if (hBtn) hBtn.classList.add('hidden');
|
||||
if (hCnt) hCnt.classList.remove('hidden');
|
||||
|
||||
clickedBtn.classList.add('opt-wrong');
|
||||
|
||||
// Highlight the true correct option
|
||||
@@ -623,7 +668,8 @@ function renderQuizSummary() {
|
||||
const correct = sessionStats.correct;
|
||||
const mistakes = sessionStats.mistakes;
|
||||
const ingested = sessionStats.ingested;
|
||||
const pct = Math.round((correct / total) * 100);
|
||||
const earnedScore = Math.round(sessionStats.score * 10) / 10;
|
||||
const pct = Math.round((earnedScore / total) * 100);
|
||||
|
||||
const sumScore = document.getElementById('cefr-sum-score');
|
||||
const sumLead = document.getElementById('cefr-sum-lead');
|
||||
@@ -633,8 +679,8 @@ function renderQuizSummary() {
|
||||
const sumSrsBtn = document.getElementById('cefr-sum-srs-btn');
|
||||
|
||||
if (sumScore) sumScore.textContent = `${pct}%`;
|
||||
if (sumCorrect) sumCorrect.textContent = `${correct} / ${total}`;
|
||||
if (sumMistakes) sumMistakes.textContent = mistakes;
|
||||
if (sumCorrect) sumCorrect.textContent = `${earnedScore} / ${total} баллов (${correct} верных)`;
|
||||
if (sumMistakes) sumMistakes.innerHTML = `${mistakes} ${sessionStats.hintsUsed > 0 ? `<span style="font-size:12px;opacity:0.8;">(подсказок: ${sessionStats.hintsUsed})</span>` : ''}`;
|
||||
if (sumIngested) sumIngested.textContent = ingested;
|
||||
|
||||
if (sumLead) {
|
||||
|
||||
Reference in New Issue
Block a user