// ======================================================================= // Notes Application - API Client Module (Native ES Module) // ======================================================================= // Persistent session: auto-inject Authorization header if token exists in localStorage const originalFetch = window.fetch; window.fetch = function(url, options = {}) { const token = localStorage.getItem('notes_token'); if (token && typeof url === 'string' && url.startsWith('/api/')) { options.headers = options.headers || {}; if (options.headers instanceof Headers) { if (!options.headers.has('Authorization')) { options.headers.set('Authorization', `Bearer ${token}`); } } else if (Array.isArray(options.headers)) { if (!options.headers.some(([k]) => k.toLowerCase() === 'authorization')) { options.headers.push(['Authorization', `Bearer ${token}`]); } } else { if (!options.headers['Authorization']) { options.headers['Authorization'] = `Bearer ${token}`; } } } return originalFetch.call(this, url, options); }; export const api = { // Auth endpoints auth: { async me() { return await fetch('/api/me'); }, async check() { const resp = await this.me(); if (resp.ok) { return await resp.json(); } return null; }, async login(username, password) { return await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); }, async logout() { return await fetch('/api/logout', { method: 'POST' }); }, async changePassword(currentPassword, newPassword) { return await fetch('/api/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ old_password: currentPassword, new_password: newPassword }) }); } }, // Cards & Vocabulary cards: { async getAll(params = {}) { const qs = new URLSearchParams(params).toString(); const url = qs ? `/api/cards?${qs}` : '/api/cards'; const resp = await fetch(url); return resp.ok ? await resp.json() : []; }, async lookup(term) { return await api.lookup.search(term); }, async create(payload) { return await fetch('/api/cards', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, async update(id, payload) { return await fetch(`/api/cards/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, async delete(id) { return await fetch(`/api/cards/${id}`, { method: 'DELETE' }); }, async batch(payload) { return await fetch('/api/cards/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } }, // Dictionary Lookup & AI Sentence Translation lookup: { async search(term, context = '') { let url = `/api/lookup?term=${encodeURIComponent(term)}`; if (context) { url += `&context=${encodeURIComponent(context)}`; } const resp = await fetch(url); if (resp.ok) { return await resp.json(); } throw new Error(`Lookup failed: ${resp.status}`); }, async detectPhrasalVerb(sentence, word) { const resp = await fetch(`/api/detect-phrasal-verb?sentence=${encodeURIComponent(sentence)}&word=${encodeURIComponent(word)}`); return resp.ok ? await resp.json() : { detected: false, phrasal_verb: null }; }, async translateSentence(sentence, targetWord = '', context = '') { const resp = await fetch('/api/translate-sentence', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sentence: sentence, target_word: targetWord, context: context }) }); if (resp.ok) { return await resp.json(); } throw new Error(`Sentence translation failed: ${resp.status}`); }, async getAiStatus() { const resp = await fetch('/api/ai/status'); return resp.ok ? await resp.json() : { online: false, models: [] }; } }, // Collections & Tags collections: { async getAll() { const resp = await fetch('/api/collections'); return resp.ok ? await resp.json() : []; } }, tags: { async getAll() { const resp = await fetch('/api/tags'); return resp.ok ? await resp.json() : []; } }, // Statistics & Analytics stats: { async getStats() { const resp = await fetch('/api/stats'); return resp.ok ? await resp.json() : null; }, async getSrsStats() { const resp = await fetch('/api/srs/stats'); return resp.ok ? await resp.json() : null; }, async getHeatmap(days = 180) { const resp = await fetch(`/api/srs/activity-heatmap?days=${days}`); return resp.ok ? await resp.json() : { days: [] }; } }, // SRS Spaced Repetition srs: { 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 (tag) url += `&tag=${encodeURIComponent(tag)}`; const resp = await fetch(url); return resp.ok ? await resp.json() : { cards: [], count: 0 }; }, 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: r }) }); } }, // Listening Comprehension Trainer listening: { async getExercise(topic = 'all', level = 'B1', voice = null) { let url = `/api/listening/exercise?topic=${encodeURIComponent(topic)}&level=${encodeURIComponent(level)}`; if (voice) { url += `&voice=${encodeURIComponent(voice)}`; } const resp = await fetch(url); if (resp.ok) return await resp.json(); throw new Error(`Failed to load listening exercise: ${resp.status}`); }, 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: sId, user_answer: uAns, story: st, ideal_answer: idAns }) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to check listening answer: ${resp.status}`); } }, // Neural Speech Synthesis (TTS) tts: { async getStatus() { const resp = await fetch('/api/tts/status'); return resp.ok ? await resp.json() : null; } }, // Bookshelf & Reader books: { async getAll() { const resp = await fetch('/api/books'); return resp.ok ? await resp.json() : []; }, async get(id) { const resp = await fetch(`/api/books/${id}`); if (resp.ok) return await resp.json(); throw new Error(`Failed to load book ${id}: ${resp.status}`); }, async uploadFile(formData) { return await fetch('/api/books', { method: 'POST', body: formData }); }, async createManual(payload) { return await fetch('/api/books', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, async delete(id) { return await fetch(`/api/books/${id}`, { method: 'DELETE' }); }, async saveProgress(bookId, payload) { return await fetch(`/api/books/${bookId}/progress`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, async fetchRfc(query, force = false) { const resp = await fetch('/api/books/fetch-rfc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rfc: query, force: force }) }); if (resp.ok) return await resp.json(); let detail = 'Не удалось загрузить RFC'; try { const errData = await resp.json(); detail = errData.detail || detail; } catch (_) {} throw new Error(detail); }, async fetchWikipedia({ query = '', lang = 'simple', time_budget = '10-15', topic = 'random', force = false } = {}) { const resp = await fetch('/api/books/fetch-wikipedia', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query, lang: lang, time_budget: time_budget, topic: topic, force: force }) }); if (resp.ok) return await resp.json(); let detail = 'Не удалось загрузить статью из Wikipedia'; try { const errData = await resp.json(); detail = errData.detail || detail; } catch (_) {} throw new Error(detail); } }, // CEFR Level Tests & Gap Ingestion (Task 15) cefr: { async getLevels() { const resp = await fetch('/api/cefr/levels'); if (resp.ok) return await resp.json(); throw new Error(`Failed to load CEFR levels: ${resp.status}`); }, async getQuiz(level = 'A1', count = 10, mode = 'all') { const resp = await fetch(`/api/cefr/quiz?level=${encodeURIComponent(level)}&count=${count}&mode=${encodeURIComponent(mode)}`); if (resp.ok) return await resp.json(); throw new Error(`Failed to load CEFR quiz: ${resp.status}`); }, async submitAnswer(questionId, selectedOption) { const resp = await fetch('/api/cefr/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question_id: questionId, selected_option: selectedOption }) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to submit answer: ${resp.status}`); }, async resetLevel(level) { const resp = await fetch('/api/cefr/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ level }) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to reset level: ${resp.status}`); }, async addWord(level, term, contextSentence = '') { const resp = await fetch('/api/cefr/add-word', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ level: level, term: term, context_sentence: contextSentence }) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to add word: ${resp.status}`); } }, // Murphy Grammar Engine & Handwritten Journaling (Task 16) grammar: { async getUnits() { const resp = await fetch('/api/grammar/units'); if (resp.ok) return await resp.json(); throw new Error(`Failed to load grammar units: ${resp.status}`); }, async getUnit(id) { const resp = await fetch(`/api/grammar/units/${id}`); if (resp.ok) return await resp.json(); throw new Error(`Failed to load grammar unit ${id}: ${resp.status}`); }, async submitAnswer(unitId, exerciseIdx, selectedOption) { const resp = await fetch('/api/grammar/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ unit_id: unitId, exercise_idx: exerciseIdx, selected_option: selectedOption }) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to submit grammar answer: ${resp.status}`); }, async completeUnit(unitId, score, handwrittenLogged = false) { const resp = await fetch('/api/grammar/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ unit_id: unitId, score: score, handwritten_logged: handwrittenLogged }) }); 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', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ unit_id: unitId, handwritten: handwritten }) }); 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); } }, // Background Tasks & Sentence Mining tasks: { async mineBooks(bookId = null, level = 'A1', count = 15) { const resp = await fetch('/api/tasks/mine-books', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ book_id: bookId, level: level, count: count }) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to start mining task: ${resp.status}`); }, async getActive(taskType = null) { let url = '/api/tasks/active'; if (taskType) url += `?task_type=${encodeURIComponent(taskType)}`; const resp = await fetch(url); if (resp.ok) return await resp.json(); throw new Error(`Failed to get active task: ${resp.status}`); }, async getStatus(taskId) { const resp = await fetch(`/api/tasks/${taskId}`); if (resp.ok) return await resp.json(); throw new Error(`Failed to get task status: ${resp.status}`); }, async cancel(taskId) { const resp = await fetch(`/api/tasks/${taskId}/cancel`, { method: 'POST' }); if (resp.ok) return await resp.json(); throw new Error(`Failed to cancel task: ${resp.status}`); } }, // Comprehensive Lessons Configurator & Runner (Beta) lessons: { async getPresets() { const resp = await fetch('/api/lessons/presets'); if (resp.ok) return await resp.json(); throw new Error(`Failed to get presets: ${resp.status}`); }, async savePreset(payload) { const resp = await fetch('/api/lessons/presets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (resp.ok) return await resp.json(); throw new Error(`Failed to save preset: ${resp.status}`); }, async deletePreset(presetId) { const resp = await fetch(`/api/lessons/presets/${presetId}`, { method: 'DELETE' }); if (resp.ok) return await resp.json(); throw new Error(`Failed to delete preset: ${resp.status}`); }, async getGrammarRecommendations() { const resp = await fetch('/api/lessons/grammar-recommendations'); if (resp.ok) return await resp.json(); throw new Error(`Failed to get grammar recommendations: ${resp.status}`); }, async getArticles(topic = null) { let url = '/api/lessons/articles'; if (topic) url += `?topic=${encodeURIComponent(topic)}`; const resp = await fetch(url); if (resp.ok) return await resp.json(); 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}`); }, async getHistory(limit = 15) { const resp = await fetch(`/api/lessons/history?limit=${limit}`); if (resp.ok) return await resp.json(); throw new Error(`Failed to get lesson history: ${resp.status}`); } } };