fix(frontend): export ui object, harmonize API signatures, and clean view switcher

This commit is contained in:
Antigravity committed 2026-10-07 13:13:45 +03:00
1 parent 718e9797eb
commit 640da1f27c
4 files changed
+138 -147

No files matched your search

+16 -116
View File
@@ -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';
}
+43 -11
View File
@@ -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);
}
},
+1 -10
View File
@@ -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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
+78 -10
View File
@@ -3,7 +3,7 @@
// =======================================================================
export function escapeHtml(str) {
if (!str) return '';
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
@@ -13,7 +13,8 @@ export function escapeHtml(str) {
}
export function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (!string) return '';
return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function slugify(text) {
@@ -25,6 +26,7 @@ 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--) {
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;