From 640da1f27c7dc43596bef9837d90b27d72beaa52 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Wed, 7 Oct 2026 13:13:45 +0300 Subject: [PATCH] fix(frontend): export ui object, harmonize API signatures, and clean view switcher --- app/static/js/app.js | 132 ++++---------------------------- app/static/js/modules/api.js | 54 ++++++++++--- app/static/js/modules/lesson.js | 11 +-- app/static/js/modules/ui.js | 88 ++++++++++++++++++--- 4 files changed, 138 insertions(+), 147 deletions(-) diff --git a/app/static/js/app.js b/app/static/js/app.js index 9cd90f2..86e1d2e 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -4,7 +4,7 @@ // ======================================================================= import { api } from './modules/api.js'; -import { initTheme, openModal, closeModal, showToast, playTextSpeech } from './modules/ui.js'; +import { initTheme, openModal, closeModal, showToast, playTextSpeech, switchView } from './modules/ui.js'; import { initDictionary, loadInitialData } from './modules/dictionary.js'; import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js'; import { initReader, loadBookshelf, openBook } from './modules/reader.js'; @@ -81,25 +81,12 @@ export function applyInitialViewPreflight() { } catch (e) {} } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); - const lessonView = document.getElementById('lesson-view'); - const lessonRunnerView = document.getElementById('lesson-runner-view'); const bookshelfContainer = document.getElementById('bookshelf-container'); const readerInterface = document.getElementById('reader-interface'); const readerBookTitle = document.getElementById('reader-book-title'); if (hash.startsWith('#reader') || hash === '#bookshelf') { - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (lessonView) lessonView.classList.add('hidden'); - if (lessonRunnerView) lessonRunnerView.classList.add('hidden'); - if (readerView) readerView.classList.remove('hidden'); + switchView('reader-view'); updateActiveNavState('reader'); if (hash.includes('/book/')) { if (bookshelfContainer) bookshelfContainer.classList.add('hidden'); @@ -107,40 +94,16 @@ export function applyInitialViewPreflight() { if (readerBookTitle) readerBookTitle.textContent = 'Загрузка книги...'; } } else if (hash.startsWith('#srs')) { - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (lessonView) lessonView.classList.add('hidden'); - if (lessonRunnerView) lessonRunnerView.classList.add('hidden'); - if (srsView) srsView.classList.remove('hidden'); + switchView('srs-view'); updateActiveNavState('srs'); } else if (hash.startsWith('#cefr')) { - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (lessonView) lessonView.classList.add('hidden'); - if (lessonRunnerView) lessonRunnerView.classList.add('hidden'); - if (cefrView) cefrView.classList.remove('hidden'); + switchView('cefr-view'); updateActiveNavState('cefr'); } else if (hash.startsWith('#grammar')) { - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (lessonView) lessonView.classList.add('hidden'); - if (lessonRunnerView) lessonRunnerView.classList.add('hidden'); - if (grammarView) grammarView.classList.remove('hidden'); + switchView('grammar-view'); updateActiveNavState('grammar'); } else if (hash.startsWith('#lesson')) { - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (lessonRunnerView) lessonRunnerView.classList.add('hidden'); - if (lessonView) lessonView.classList.remove('hidden'); + switchView('lesson-view'); updateActiveNavState('lesson'); } } @@ -153,17 +116,7 @@ export function openDictionaryView(updateHash = true) { window.location.hash = '#dictionary'; return; } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); - - if (srsView) srsView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (dictionaryView) dictionaryView.classList.remove('hidden'); + switchView('dictionary-view'); updateActiveNavState('dict'); try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {} loadInitialData(); @@ -175,17 +128,7 @@ export function openSrsView(tagFilter = null, updateHash = true) { window.location.hash = targetHash; return; } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); - - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (srsView) srsView.classList.remove('hidden'); + switchView('srs-view'); updateActiveNavState('srs'); try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {} startSrsTrainingSession(false, tagFilter); @@ -200,19 +143,9 @@ export function openBookshelfView(updateHash = true) { window.location.hash = '#reader'; return; } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); + switchView('reader-view'); const bookshelfContainer = document.getElementById('bookshelf-container'); const readerInterface = document.getElementById('reader-interface'); - - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (readerView) readerView.classList.remove('hidden'); if (bookshelfContainer) bookshelfContainer.classList.remove('hidden'); if (readerInterface) readerInterface.classList.add('hidden'); updateActiveNavState('reader'); @@ -242,18 +175,7 @@ export function openCefrView(level = null, mode = 'all', updateHash = true) { return; } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); - - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (cefrView) cefrView.classList.remove('hidden'); - + switchView('cefr-view'); updateActiveNavState('cefr'); try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {} @@ -272,18 +194,7 @@ export function openGrammarView(unitId = null, updateHash = true) { return; } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); - - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.remove('hidden'); - + switchView('grammar-view'); updateActiveNavState('grammar'); try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {} @@ -301,22 +212,7 @@ export function openLessonView(presetId = null, updateHash = true) { return; } - const dictionaryView = document.getElementById('dictionary-view'); - const srsView = document.getElementById('srs-view'); - const readerView = document.getElementById('reader-view'); - const cefrView = document.getElementById('cefr-view'); - const grammarView = document.getElementById('grammar-view'); - const lessonView = document.getElementById('lesson-view'); - const lessonRunnerView = document.getElementById('lesson-runner-view'); - - if (dictionaryView) dictionaryView.classList.add('hidden'); - if (srsView) srsView.classList.add('hidden'); - if (readerView) readerView.classList.add('hidden'); - if (cefrView) cefrView.classList.add('hidden'); - if (grammarView) grammarView.classList.add('hidden'); - if (lessonRunnerView) lessonRunnerView.classList.add('hidden'); - if (lessonView) lessonView.classList.remove('hidden'); - + switchView('lesson-view'); updateActiveNavState('lesson'); try { localStorage.setItem('notes_active_view', 'lesson-view'); } catch (e) {} @@ -352,6 +248,10 @@ export async function handleRouting() { hash = '#grammar'; window.location.hash = hash; return; + } else if (lastView === 'lesson-view') { + hash = '#lesson'; + window.location.hash = hash; + return; } else { hash = '#dictionary'; } diff --git a/app/static/js/modules/api.js b/app/static/js/modules/api.js index ccda482..424042c 100644 --- a/app/static/js/modules/api.js +++ b/app/static/js/modules/api.js @@ -167,18 +167,34 @@ export const api = { // SRS Spaced Repetition srs: { - async getQueue(forceAll = false, tagFilter = null) { - let url = '/api/srs/queue?limit=50'; + async getQueue(forceAllOrOptions = false, tagFilter = null) { + let forceAll = false; + let tag = tagFilter; + let limit = 50; + if (typeof forceAllOrOptions === 'object' && forceAllOrOptions !== null) { + forceAll = !!forceAllOrOptions.forceAll || !!forceAllOrOptions.force_all; + tag = forceAllOrOptions.tagFilter || forceAllOrOptions.tag || tag; + if (forceAllOrOptions.limit) limit = forceAllOrOptions.limit; + } else { + forceAll = !!forceAllOrOptions; + } + let url = `/api/srs/queue?limit=${limit}`; if (forceAll) url += '&force_all=true'; - if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; + if (tag) url += `&tag=${encodeURIComponent(tag)}`; const resp = await fetch(url); - return resp.ok ? await resp.json() : []; + return resp.ok ? await resp.json() : { cards: [], count: 0 }; }, - async submitReview(cardId, rating) { + async submitReview(cardIdOrPayload, rating) { + let cardId = cardIdOrPayload; + let r = rating; + if (typeof cardIdOrPayload === 'object' && cardIdOrPayload !== null) { + cardId = cardIdOrPayload.card_id ?? cardIdOrPayload.id; + r = cardIdOrPayload.rating; + } return await fetch('/api/srs/review', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ card_id: cardId, rating }) + body: JSON.stringify({ card_id: cardId, rating: r }) }); } }, @@ -194,15 +210,25 @@ export const api = { if (resp.ok) return await resp.json(); throw new Error(`Failed to load listening exercise: ${resp.status}`); }, - async checkAnswer(scenarioId, userAnswer, story = '', idealAnswer = '') { + async checkAnswer(scenarioIdOrPayload, userAnswer = '', story = '', idealAnswer = '') { + let sId = scenarioIdOrPayload; + let uAns = userAnswer; + let st = story; + let idAns = idealAnswer; + if (typeof scenarioIdOrPayload === 'object' && scenarioIdOrPayload !== null) { + sId = scenarioIdOrPayload.scenario_id ?? scenarioIdOrPayload.id; + uAns = scenarioIdOrPayload.user_answer ?? scenarioIdOrPayload.answer ?? ''; + st = scenarioIdOrPayload.story ?? ''; + idAns = scenarioIdOrPayload.ideal_answer ?? scenarioIdOrPayload.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 + scenario_id: sId, + user_answer: uAns, + story: st, + ideal_answer: idAns }) }); if (resp.ok) return await resp.json(); @@ -374,6 +400,9 @@ export const api = { if (resp.ok) return await resp.json(); throw new Error(`Failed to complete unit: ${resp.status}`); }, + async recordCompletion(unitId, score, handwrittenLogged = false) { + return this.completeUnit(unitId, score, handwrittenLogged); + }, async toggleHandwritten(unitId, handwritten) { const resp = await fetch('/api/grammar/handwritten', { method: 'POST', @@ -385,6 +414,9 @@ export const api = { }); if (resp.ok) return await resp.json(); throw new Error(`Failed to update handwritten status: ${resp.status}`); + }, + async updateHandwritten(unitId, handwritten) { + return this.toggleHandwritten(unitId, handwritten); } }, diff --git a/app/static/js/modules/lesson.js b/app/static/js/modules/lesson.js index 68b7baa..55caffe 100644 --- a/app/static/js/modules/lesson.js +++ b/app/static/js/modules/lesson.js @@ -4,7 +4,7 @@ // ======================================================================= import { api } from './api.js'; -import { ui } from './ui.js'; +import { ui, escapeHtml, showToast, switchView } from './ui.js'; let presets = []; let currentPreset = null; @@ -1630,12 +1630,3 @@ async function openHistoryModal() { } } -function escapeHtml(str) { - if (!str) return ''; - return String(str) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/app/static/js/modules/ui.js b/app/static/js/modules/ui.js index dd90da7..a9715c9 100644 --- a/app/static/js/modules/ui.js +++ b/app/static/js/modules/ui.js @@ -3,7 +3,7 @@ // ======================================================================= export function escapeHtml(str) { - if (!str) return ''; + if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(/ 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -37,6 +39,7 @@ export function shuffleArray(array) { // Toast Notifications // ----------------------------------------------------------------------- export function showToast(message, type = 'info') { + if (!message) return; let toastContainer = document.getElementById('notes-toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); @@ -51,7 +54,11 @@ export function showToast(message, type = 'info') { gap: 8px; pointer-events: none; `; - document.body.appendChild(toastContainer); + if (document.body) { + document.body.appendChild(toastContainer); + } else { + return; + } } const toast = document.createElement('div'); @@ -98,14 +105,19 @@ export function showToast(message, type = 'info') { export function initTheme() { const themeToggle = document.getElementById('theme-toggle'); const mobileThemeBtn = document.getElementById('mobile-theme-btn'); - const savedTheme = localStorage.getItem('notes_theme') || 'dark'; + let savedTheme = 'dark'; + try { + savedTheme = localStorage.getItem('notes_theme') || 'dark'; + } catch (e) {} document.documentElement.setAttribute('data-theme', savedTheme); function toggle() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); - localStorage.setItem('notes_theme', next); + try { + localStorage.setItem('notes_theme', next); + } catch (e) {} } if (themeToggle) { @@ -120,16 +132,47 @@ export function initTheme() { } // ----------------------------------------------------------------------- -// Modal Open / Close Helpers +// Modal Open / Close Helpers (Accepts HTMLElement or string selector/ID) // ----------------------------------------------------------------------- export function openModal(modalElem) { if (!modalElem) return; - modalElem.classList.remove('hidden'); + const el = typeof modalElem === 'string' + ? (document.getElementById(modalElem) || document.querySelector(modalElem)) + : modalElem; + if (el) el.classList.remove('hidden'); } export function closeModal(modalElem) { if (!modalElem) return; - modalElem.classList.add('hidden'); + const el = typeof modalElem === 'string' + ? (document.getElementById(modalElem) || document.querySelector(modalElem)) + : modalElem; + if (el) el.classList.add('hidden'); +} + +// ----------------------------------------------------------------------- +// View Switcher Helper +// ----------------------------------------------------------------------- +export function switchView(targetViewId) { + const allViews = [ + 'dictionary-view', + 'srs-view', + 'reader-view', + 'cefr-view', + 'grammar-view', + 'lesson-view', + 'lesson-runner-view' + ]; + allViews.forEach(id => { + const el = document.getElementById(id); + if (el) { + if (id === targetViewId) { + el.classList.remove('hidden'); + } else { + el.classList.add('hidden'); + } + } + }); } // ----------------------------------------------------------------------- @@ -197,11 +240,36 @@ export function playAudio(url, speed = 1.0) { export function playTextSpeech(text, lang = 'en', speed = 1.0, voice = null) { if (!text || !text.trim()) return null; const isRu = lang && lang.toLowerCase().startsWith('ru'); - const savedVoice = isRu ? localStorage.getItem('notes_tts_voice_ru') : localStorage.getItem('notes_tts_voice_en'); + let savedVoice = null; + try { + savedVoice = isRu + ? localStorage.getItem('notes_tts_voice_ru') + : localStorage.getItem('notes_tts_voice_en'); + } catch (e) {} const activeVoice = voice || savedVoice; - let url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang}&speed=${speed}`; + let url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang || 'en'}&speed=${speed}`; if (activeVoice) { url += `&voice=${encodeURIComponent(activeVoice)}`; } return playAudio(url, speed); } + +// ----------------------------------------------------------------------- +// Unified UI Object Export (for modules importing `{ ui }` or `ui`) +// ----------------------------------------------------------------------- +export const ui = { + escapeHtml, + escapeRegex, + slugify, + shuffleArray, + showToast, + initTheme, + openModal, + closeModal, + switchView, + updateHeaderStats, + playAudio, + playTextSpeech, +}; + +export default ui;