Files
notes-qpid-pro/app/static/js/modules/api.js
T

479 lines
18 KiB
JavaScript

// =======================================================================
// 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(forceAll = false, tagFilter = null) {
let url = '/api/srs/queue?limit=50';
if (forceAll) url += '&force_all=true';
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
const resp = await fetch(url);
return resp.ok ? await resp.json() : [];
},
async submitReview(cardId, rating) {
return await fetch('/api/srs/review', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ card_id: cardId, rating })
});
}
},
// 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(scenarioId, userAnswer, story = '', 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
})
});
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 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}`);
}
},
// 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}`);
}
}
};