fix: resolve bugs 01 through 08 and set Ollama TTL to 1h
This commit is contained in:
1 parent
a01bf64445
commit
9bbc9473de
14 files changed
+366
-108
No files matched your search
@@ -492,13 +492,22 @@ export const api = {
|
||||
throw new Error(`Failed to get articles: ${resp.status}`);
|
||||
},
|
||||
async submitDebrief(payload) {
|
||||
const resp = await fetch('/api/lessons/debrief', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (resp.ok) return await resp.json();
|
||||
throw new Error(`Failed to submit debrief: ${resp.status}`);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
||||
try {
|
||||
const resp = await fetch('/api/lessons/debrief', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
if (resp.ok) return await resp.json();
|
||||
throw new Error(`Failed to submit debrief: ${resp.status}`);
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
async getHistory(limit = 15) {
|
||||
const resp = await fetch(`/api/lessons/history?limit=${limit}`);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './api.js';
|
||||
import { escapeHtml, showToast, openModal, closeModal, playAudio } from './ui.js';
|
||||
import { escapeHtml, showToast, openModal, closeModal, playAudio, playTextSpeech, formatTranslationWithSpoiler } from './ui.js';
|
||||
|
||||
export let allCards = [];
|
||||
export let collectionsList = ['General', 'Разговорный', 'Работа & IT', 'Книги & Сериалы', 'Идиомы'];
|
||||
@@ -67,7 +67,7 @@ export function initDictionary() {
|
||||
const batchCancelBtn = document.getElementById('batch-cancel-btn');
|
||||
|
||||
// Elements - Modals
|
||||
const editModal = document.getElementById('edit-card-modal');
|
||||
const editModal = document.getElementById('edit-modal') || document.getElementById('edit-card-modal');
|
||||
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||
const modalCancelBtn = document.getElementById('modal-cancel-btn');
|
||||
const modalSaveBtn = document.getElementById('modal-save-btn');
|
||||
@@ -165,8 +165,9 @@ export function initDictionary() {
|
||||
|
||||
if (audioPlayBtn) {
|
||||
audioPlayBtn.addEventListener('click', () => {
|
||||
if (!currentLookupData || !currentLookupData.term) return;
|
||||
const url = currentLookupData.audio_url || `/api/tts?text=${encodeURIComponent(currentLookupData.term)}&lang=en`;
|
||||
const term = currentLookupData?.term || currentLookupData?.english_term;
|
||||
if (!term) return;
|
||||
const url = currentLookupData.audio_url || `/api/tts?text=${encodeURIComponent(term)}&lang=en`;
|
||||
playAudio(url);
|
||||
});
|
||||
}
|
||||
@@ -192,7 +193,7 @@ export function initDictionary() {
|
||||
}
|
||||
|
||||
function displayLookupResult(data) {
|
||||
resTerm.textContent = data.term;
|
||||
resTerm.textContent = data.term || data.english_term || data.query || '';
|
||||
resPhonetic.textContent = data.phonetic || '';
|
||||
resPhonetic.style.display = data.phonetic ? 'inline-block' : 'none';
|
||||
resPos.textContent = data.part_of_speech || 'Phrase';
|
||||
@@ -210,6 +211,8 @@ export function initDictionary() {
|
||||
resInLibrary.classList.toggle('hidden', !inLib);
|
||||
}
|
||||
|
||||
const targetTrans = (data.is_reverse ? (data.russian_query || data.translation) : data.translation) || '';
|
||||
|
||||
if (inLib) {
|
||||
saveCardBtn.innerHTML = '⭐ Обновить в словаре <span class="btn-subtext">(Ctrl + Enter)</span>';
|
||||
const lib = data.library_card || foundDuplicateCard;
|
||||
@@ -220,11 +223,11 @@ export function initDictionary() {
|
||||
if (lib.tags) editTags.value = lib.tags;
|
||||
if (lib.notes) editNotes.value = lib.notes;
|
||||
} else {
|
||||
editTranslation.value = data.translation || '';
|
||||
editTranslation.value = targetTrans;
|
||||
}
|
||||
} else {
|
||||
saveCardBtn.innerHTML = '💾 Сохранить в словарь <span class="btn-subtext">(Ctrl + Enter)</span>';
|
||||
editTranslation.value = data.translation || '';
|
||||
editTranslation.value = targetTrans;
|
||||
editContext.value = '';
|
||||
editNotes.value = '';
|
||||
editCollection.value = selectedCollection || 'General';
|
||||
@@ -247,7 +250,7 @@ export function initDictionary() {
|
||||
|
||||
async function saveNewCard() {
|
||||
if (!currentLookupData) return;
|
||||
const term = currentLookupData.term.trim();
|
||||
const term = (currentLookupData.term || currentLookupData.english_term || currentLookupData.query || '').trim();
|
||||
const translation = editTranslation.value.trim();
|
||||
if (!translation) {
|
||||
alert('Пожалуйста, укажите русский перевод');
|
||||
@@ -388,14 +391,19 @@ export function initDictionary() {
|
||||
<span class="card-term">${escapeHtml(c.term)}</span>
|
||||
${c.phonetic ? `<span class="card-phonetic">${escapeHtml(c.phonetic)}</span>` : ''}
|
||||
${c.part_of_speech ? `<span class="card-pos">${escapeHtml(c.part_of_speech)}</span>` : ''}
|
||||
<button class="icon-btn audio-play-btn play-term-audio-btn" data-id="${c.id}" title="Озвучить слово">🔊</button>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="icon-btn edit-card-btn" data-id="${c.id}" title="Редактировать">✏️</button>
|
||||
<button class="icon-btn delete-card-btn" data-id="${c.id}" title="Удалить">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-translation">${escapeHtml(c.translation)}</div>
|
||||
${c.context_sentence ? `<div class="card-context">“${escapeHtml(c.context_sentence)}”</div>` : ''}
|
||||
<div class="card-translation">${formatTranslationWithSpoiler(c.translation)}</div>
|
||||
${c.context_sentence ? `
|
||||
<div class="card-context">
|
||||
“${escapeHtml(c.context_sentence)}”
|
||||
<button class="icon-btn audio-play-btn play-context-audio-btn" data-id="${c.id}" title="Озвучить пример">🔊</button>
|
||||
</div>` : ''}
|
||||
<div class="card-bottom">
|
||||
<div class="card-tags">
|
||||
<span class="card-tag">📁 ${escapeHtml(c.collection || 'General')}</span>
|
||||
@@ -405,6 +413,18 @@ export function initDictionary() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Audio buttons
|
||||
cardEl.querySelector('.play-term-audio-btn')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (c.audio_url) playAudio(c.audio_url);
|
||||
else playTextSpeech(c.term, 'en');
|
||||
});
|
||||
|
||||
cardEl.querySelector('.play-context-audio-btn')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
playTextSpeech(c.context_sentence, 'en');
|
||||
});
|
||||
|
||||
// Edit button
|
||||
cardEl.querySelector('.edit-card-btn').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './api.js';
|
||||
import { ui, escapeHtml, showToast, switchView, shuffleArray, escapeRegex, playAudio, playTextSpeech } from './ui.js';
|
||||
import { ui, escapeHtml, showToast, switchView, shuffleArray, escapeRegex, playAudio, playTextSpeech, cleanPromptTranslation } from './ui.js';
|
||||
|
||||
let presets = [];
|
||||
let currentPreset = null;
|
||||
@@ -762,20 +762,20 @@ async function runVocabStep(step, container) {
|
||||
// Mode A: 4 Options Quiz
|
||||
// -----------------------------------------------------------------------
|
||||
function renderQuizCard(card) {
|
||||
const correctText = card.translation;
|
||||
const correctText = cleanPromptTranslation(card.translation);
|
||||
const candidates = [
|
||||
...cards.filter(c => c.id !== card.id && c.translation).map(c => c.translation),
|
||||
...distractorPool.filter(t => t && t !== correctText)
|
||||
...cards.filter(c => c.id !== card.id && c.translation).map(c => cleanPromptTranslation(c.translation)),
|
||||
...distractorPool.filter(t => t && cleanPromptTranslation(t) !== correctText).map(t => cleanPromptTranslation(t))
|
||||
];
|
||||
const uniqueCandidates = [...new Set(candidates.filter(t => t !== correctText))];
|
||||
shuffleArray(uniqueCandidates);
|
||||
const options = [correctText, ...uniqueCandidates.slice(0, 3)];
|
||||
let 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);
|
||||
options = shuffleArray(options);
|
||||
|
||||
let answered = false;
|
||||
|
||||
@@ -1050,7 +1050,7 @@ async function runVocabStep(step, container) {
|
||||
|
||||
<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>
|
||||
<h2 class="display-term display-prompt">${escapeHtml(cleanPromptTranslation(card.translation))}</h2>
|
||||
</div>
|
||||
|
||||
${card.context_sentence ? `
|
||||
@@ -1171,7 +1171,7 @@ async function runVocabStep(step, container) {
|
||||
<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>` : ''}
|
||||
${card.translation ? `<p class="dictation-hint text-muted mt-2">Подсказка перевода: ${escapeHtml(cleanPromptTranslation(card.translation))}</p>` : ''}
|
||||
</div>
|
||||
|
||||
<form id="lesson-dictation-form" class="mt-4">
|
||||
@@ -1656,7 +1656,7 @@ async function runListeningStep(step, container) {
|
||||
}
|
||||
|
||||
const data = await api.listening.getExercise(step.topic || 'all');
|
||||
const scenario = data.scenario;
|
||||
const scenario = (data && (data.scenario || data)) || {};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="step-card-box">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// =======================================================================
|
||||
|
||||
import { api } from './api.js';
|
||||
import { escapeHtml, escapeRegex, shuffleArray, showToast, openModal, closeModal, playAudio } from './ui.js';
|
||||
import { escapeHtml, escapeRegex, shuffleArray, showToast, openModal, closeModal, playAudio, cleanPromptTranslation } from './ui.js';
|
||||
import { getAllCards } from './dictionary.js';
|
||||
|
||||
let srsQueue = [];
|
||||
@@ -330,7 +330,7 @@ export function initSrs() {
|
||||
fcBackPos.style.display = card.part_of_speech ? 'inline-block' : 'none';
|
||||
} else {
|
||||
fcFrontDirBadge.textContent = 'RU ➔ EN';
|
||||
fcFrontMain.textContent = card.translation;
|
||||
fcFrontMain.textContent = cleanPromptTranslation(card.translation);
|
||||
fcFrontPos.textContent = card.part_of_speech || '';
|
||||
fcFrontPhoneticWrap.style.display = 'none';
|
||||
fcHintBtn.style.display = 'none';
|
||||
@@ -447,18 +447,18 @@ export function initSrs() {
|
||||
|
||||
if (quizAudioBtn) quizAudioBtn.classList.remove('hidden');
|
||||
|
||||
const correctText = card.translation;
|
||||
const correctText = cleanPromptTranslation(card.translation);
|
||||
const all = getAllCards();
|
||||
const candidates = all
|
||||
.filter(c => c.id !== card.id && c.translation && c.translation !== correctText)
|
||||
.map(c => c.translation);
|
||||
.filter(c => c.id !== card.id && c.translation && cleanPromptTranslation(c.translation) !== correctText)
|
||||
.map(c => cleanPromptTranslation(c.translation));
|
||||
|
||||
shuffleArray(candidates);
|
||||
const options = [correctText, ...candidates.slice(0, 3)];
|
||||
let options = [correctText, ...candidates.slice(0, 3)];
|
||||
while (options.length < 4) {
|
||||
options.push(`Вариант ${options.length + 1}`);
|
||||
}
|
||||
shuffleArray(options);
|
||||
options = shuffleArray(options);
|
||||
quizCurrentOptions = options;
|
||||
|
||||
quizOptionsGrid.innerHTML = '';
|
||||
@@ -543,7 +543,7 @@ export function initSrs() {
|
||||
typingRatingBar.classList.add('hidden');
|
||||
|
||||
typingPos.textContent = card.part_of_speech || '';
|
||||
typingPromptRu.textContent = card.translation;
|
||||
typingPromptRu.textContent = cleanPromptTranslation(card.translation);
|
||||
if (card.context_sentence) {
|
||||
typingContextHint.textContent = `Контекст: “${card.context_sentence}”`;
|
||||
typingContextHint.classList.remove('hidden');
|
||||
@@ -590,7 +590,7 @@ export function initSrs() {
|
||||
clozeRatingBar.classList.add('hidden');
|
||||
|
||||
clozePos.textContent = card.part_of_speech || '';
|
||||
clozeTargetTranslation.textContent = card.translation;
|
||||
clozeTargetTranslation.textContent = cleanPromptTranslation(card.translation);
|
||||
|
||||
const context = card.context_sentence || `The word is ${card.term}.`;
|
||||
const re = new RegExp(escapeRegex(card.term), 'gi');
|
||||
@@ -635,7 +635,7 @@ export function initSrs() {
|
||||
dictationDetailsBox.classList.add('hidden');
|
||||
dictationRatingBar.classList.add('hidden');
|
||||
dictationHintText.classList.add('hidden');
|
||||
dictationHintText.textContent = card.translation;
|
||||
dictationHintText.textContent = cleanPromptTranslation(card.translation);
|
||||
|
||||
dictationInput.value = '';
|
||||
dictationInput.disabled = false;
|
||||
|
||||
@@ -27,12 +27,26 @@ export function slugify(text) {
|
||||
|
||||
export function shuffleArray(array) {
|
||||
if (!array || !Array.isArray(array)) return [];
|
||||
const arr = [...array];
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return arr;
|
||||
return array;
|
||||
}
|
||||
|
||||
export function cleanPromptTranslation(text) {
|
||||
if (!text || typeof text !== 'string') return '';
|
||||
// Strip grammatical inflection notes that contain Latin words to prevent spoilers in tests
|
||||
return text.replace(/\s*\([а-яА-ЯёЁ0-9\s.,-]*\bот\s+[a-zA-Z\x27-]+\)/gi, '').trim();
|
||||
}
|
||||
|
||||
export function formatTranslationWithSpoiler(text) {
|
||||
if (!text || typeof text !== 'string') return '';
|
||||
const noteMatch = text.match(/\s*\(([а-яА-ЯёЁ0-9\s.,-]*\bот\s+[a-zA-Z\x27-]+)\)/i);
|
||||
if (!noteMatch) return escapeHtml(text);
|
||||
const cleanText = text.replace(noteMatch[0], '').trim();
|
||||
const noteText = noteMatch[1];
|
||||
return `${escapeHtml(cleanText)} <span class="grammar-hint-badge" title="Нажмите, чтобы показать форму" onclick="this.classList.toggle('active')"><span class="badge-label">ℹ️ форма</span><span class="badge-body">${escapeHtml(noteText)}</span></span>`;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -270,6 +284,8 @@ export const ui = {
|
||||
updateHeaderStats,
|
||||
playAudio,
|
||||
playTextSpeech,
|
||||
cleanPromptTranslation,
|
||||
formatTranslationWithSpoiler,
|
||||
};
|
||||
|
||||
export default ui;
|
||||
Reference in new issue
Block a user