diff --git a/CLAUDE.md b/CLAUDE.md index 4a7378b..6e1984d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -824,4 +824,253 @@ Next content requires these words: refrigerator, elevator, closet. Learning voca --- +## πŸ“Š ROBUST PROGRESS SYSTEM - Ultra-Strict Architecture + +### 🎯 Core Philosophy + +**FUNDAMENTAL RULE**: Every piece of content is a trackable progress item with strict validation and type safety. + +### πŸ—οΈ Pedagogical Flow (NON-NEGOTIABLE) + +``` +1. DISCOVERY β†’ 2. MASTERY β†’ 3. APPLICATION + (passive) (active) (context) +``` + +**Flow Rules:** +- ❌ **NO Flashcards on undiscovered words** - Must discover first +- ❌ **NO Text exercises on unmastered vocabulary** - Must master first +- βœ… **Always check prerequisites before ANY exercise** +- βœ… **Form vocabulary lists on-the-fly** from next exercise content + +### πŸ“¦ Progress Item System + +#### **Item Types & Weights** + +Each content piece = 1 or more progress items with defined weights: + +| Type | Weight | Description | Prerequisites | +|------|--------|-------------|---------------| +| **vocabulary-discovery** | 1 | Passive exposure to new word | None | +| **vocabulary-mastery** | 1 | Active flashcard practice | Must be discovered | +| **phrase** | 6 | Phrase comprehension (3x vocab) | Vocabulary mastered | +| **dialog** | 12 | Dialog comprehension (6x vocab, complex) | Vocabulary mastered | +| **text** | 15 | Full text analysis (7.5x vocab, most complex) | Vocabulary mastered | +| **audio** | 12 | Audio comprehension (6x vocab) | Vocabulary mastered | +| **image** | 6 | Image description (3x vocab) | Vocabulary discovered | +| **grammar** | 6 | Grammar rules (3x vocab) | Vocabulary discovered | + +**Total for 1 vocabulary word** = 2 points (1 discovery + 1 mastery) + +#### **Strict Interface Contract** + +```javascript +// ALL progress items MUST implement this interface +class ProgressItemInterface { + validate() // ⚠️ REQUIRED - Validate item data + serialize() // ⚠️ REQUIRED - Convert to JSON + getWeight() // ⚠️ REQUIRED - Return item weight + canComplete(state) // ⚠️ REQUIRED - Check prerequisites +} +``` + +**Missing implementation = FATAL ERROR** (red screen, app refuses to start) + +### πŸ”₯ Ultra-Strict Validation System + +#### **Runtime Checks** + +**At Application Startup:** +1. Validate ALL item implementations +2. Check ALL methods are implemented +3. Verify weight calculations +4. Test prerequisite logic + +**If ANY validation fails:** +- πŸ”΄ **Full-screen red error overlay** +- πŸ”Š **Alert sound** (dev mode) +- πŸ“³ **Screen shake animation** +- 🚨 **Giant error message with:** + - Class name + - Missing method name + - Stack trace +- ❌ **Application REFUSES to start** + +#### **Error Display Example** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ πŸ”₯ FATAL ERROR πŸ”₯ β”‚ +β”‚ β”‚ +β”‚ Implementation Missing β”‚ +β”‚ β”‚ +β”‚ Class: VocabularyMasteryItem β”‚ +β”‚ Missing Method: canComplete() β”‚ +β”‚ β”‚ +β”‚ ❌ MUST implement all interface methods β”‚ +β”‚ β”‚ +β”‚ [ Dismiss (Fix Required!) ] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Impossible to ignore. Impossible to skip. Forces correct implementation.** + +### πŸ“ˆ Progress Calculation + +#### **Chapter Analysis** + +When loading a chapter, the system: + +1. **Scans ALL content** (vocabulary, phrases, dialogs, texts, etc.) +2. **Creates progress items** for each piece +3. **Calculates total weight** (sum of all item weights) +4. **Stores item registry** for tracking + +**Example Chapter:** +- 171 vocabulary words β†’ 342 points (171Γ—2: discovery + mastery) +- 75 phrases β†’ 450 points (75Γ—6) +- 6 dialogs β†’ 72 points (6Γ—12) +- 3 lessons β†’ 45 points (3Γ—15) +- **TOTAL: 909 points** + +#### **Progress Calculation** + +```javascript +percentage = (completedWeight / totalWeight) Γ— 100 + +Example: +- Discovered 50 words = 50 points +- Mastered 20 words = 20 points +- Completed 3 phrases = 18 points (3Γ—6) +- Completed 1 dialog = 12 points +- Total completed = 100 points +- Progress = (100 / 909) Γ— 100 = 11% +``` + +#### **Breakdown Display** + +```javascript +{ + percentage: 11, + completedWeight: 100, + totalWeight: 909, + breakdown: { + 'vocabulary-discovery': { count: 50, weight: 50 }, + 'vocabulary-mastery': { count: 20, weight: 20 }, + 'phrase': { count: 3, weight: 18 }, + 'dialog': { count: 1, weight: 12 } + } +} +``` + +### 🎯 On-The-Fly Vocabulary Formation + +**OLD (Wrong):** Pre-load all 171 words for flashcards + +**NEW (Correct):** +1. **Analyze next exercise** (e.g., Dialog #3) +2. **Extract words used** in that specific dialog +3. **Check user's status** for those words +4. **Form targeted list** of only needed words +5. **Force discovery/mastery** for just those words + +**Example:** +```javascript +// Next exercise: Dialog "Academic Conference" +// Words in dialog: methodology, hypothesis, analysis, paradigm, framework + +// User status check: +// - methodology: never seen β†’ Discovery needed +// - hypothesis: discovered, not mastered β†’ Mastery needed +// - analysis: mastered β†’ Skip +// - paradigm: never seen β†’ Discovery needed +// - framework: discovered, not mastered β†’ Mastery needed + +// Smart system creates: +// 1. Discovery module: [methodology, paradigm] (2 words) +// 2. Mastery module: [hypothesis, framework] (2 words) +// 3. Then allow dialog exercise +``` + +### πŸ”§ Implementation Components + +#### **Required Classes** + +1. **ProgressItemInterface** - Abstract base with strict validation +2. **StrictInterface** - Enforcement mechanism with visual errors +3. **ContentProgressAnalyzer** - Scans content, creates items, calculates total +4. **ProgressTracker** - Manages state, marks completion, saves progress +5. **ImplementationValidator** - Runtime validation at startup + +#### **Concrete Item Implementations** + +- VocabularyDiscoveryItem +- VocabularyMasteryItem +- PhraseItem +- DialogItem +- TextItem +- AudioItem +- ImageItem +- GrammarItem + +**Each MUST implement all 4 interface methods or app fails to start** + +### βœ… Validation Checklist + +**Before ANY exercise can run:** +- [ ] Prerequisites analyzed for next specific content +- [ ] Missing words identified +- [ ] Discovery forced for never-seen words +- [ ] Mastery forced for seen-but-not-mastered words +- [ ] Progress item created with correct weight +- [ ] Completion properly tracked and saved +- [ ] Total progress recalculated + +**If ANY step fails β†’ Clear error message, app stops gracefully** + +### 🎨 UI Integration + +**Progress Display:** +``` +Chapter Progress: 11% (100/909 points) + +βœ… Vocabulary Discovery: 50/171 words (50pts) +βœ… Vocabulary Mastery: 20/171 words (20pts) +βœ… Phrases: 3/75 (18pts) +βœ… Dialogs: 1/6 (12pts) +⬜ Texts: 0/3 (0/45pts) +``` + +**Smart Guide Updates:** +``` +πŸ” Analyzing next exercise: Dialog "Academic Conference" +πŸ“š 4 words needed (2 discovery, 2 mastery) +🎯 Starting Vocabulary Discovery for: methodology, paradigm +``` + +### 🚨 Error Prevention + +**Compile-Time (Startup):** +- Interface validation +- Method implementation checks +- Weight configuration validation + +**Runtime:** +- Prerequisite enforcement +- State consistency checks +- Progress calculation validation + +**Visual Feedback:** +- Red screen for missing implementations +- Clear prerequisite errors +- Progress breakdown always visible + +--- + +**Status**: πŸ“ **DOCUMENTED - READY FOR IMPLEMENTATION** + +--- + **This is a high-quality, maintainable system built for educational software that will scale.** \ No newline at end of file diff --git a/index.html b/index.html index b6b33e1..a74efc7 100644 --- a/index.html +++ b/index.html @@ -78,6 +78,7 @@ import app from './src/Application.js'; import ContentLoader from './src/utils/ContentLoader.js'; import SmartPreviewOrchestrator from './src/DRS/SmartPreviewOrchestrator.js'; + import apiService from './src/services/APIService.js'; // Global navigation state let currentBookId = null; @@ -198,8 +199,7 @@ eventBus.on('navigation:books', async () => { try { - const response = await fetch('/api/books'); - const books = await response.json(); + const books = await apiService.getBooks(); const languages = [...new Set(books.map(book => book.language))]; const languageOptions = languages.map(lang => @@ -905,6 +905,7 @@ window.onChapterSelected = async function(chapterId) { const startBtn = document.getElementById('start-revision-btn'); const smartGuideBtn = document.getElementById('smart-guide-btn'); + const bookSelect = document.getElementById('book-select'); if (!chapterId) { if (startBtn) { @@ -918,6 +919,11 @@ return; } + // Save selection immediately when chapter is selected + if (bookSelect && bookSelect.value) { + saveLastUsedSelection(bookSelect.value, chapterId); + } + // Smart Guide is the primary button if (smartGuideBtn) { smartGuideBtn.disabled = false; @@ -930,8 +936,8 @@ } try { - // Load chapter content using ContentLoader - const chapterData = await contentLoader.loadContent(chapterId); + // Load chapter content directly from APIService + const chapterData = await apiService.loadChapter(chapterId); const stats = chapterData.statistics || {}; // Load progress information @@ -1333,24 +1339,13 @@ // DRS Progress Management Functions (Server-side storage) window.saveChapterProgress = async function(bookId, chapterId, progressData) { try { - const response = await fetch('/api/progress/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - system: 'drs', - bookId, - chapterId, - progressData - }) + const result = await apiService.saveProgress({ + system: 'drs', + bookId, + chapterId, + progressData }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const result = await response.json(); console.log(`πŸ’Ύ Saved progress to file: ${result.filename}`, progressData); return progressData; @@ -1362,13 +1357,7 @@ window.getChapterProgress = async function(bookId, chapterId) { try { - const response = await fetch(`/api/progress/load/drs/${bookId}/${chapterId}`); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const progress = await response.json(); + const progress = await apiService.loadDRSProgress(bookId, chapterId); console.log(`πŸ“ Loaded progress from file: ${bookId}/${chapterId}`, progress); return progress; diff --git a/src/Application.js b/src/Application.js index bb5986d..2141d3e 100644 --- a/src/Application.js +++ b/src/Application.js @@ -52,6 +52,15 @@ class Application { console.log('πŸš€ Starting Class Generator Application...'); + // Validate all progress item implementations (STRICT MODE) + console.log('πŸ” Validating progress item implementations...'); + const { default: ImplementationValidator } = await import('./DRS/services/ImplementationValidator.js'); + const isValid = await ImplementationValidator.validateAll(); + + if (!isValid) { + throw new Error('❌ Implementation validation failed - check console for details'); + } + // Initialize core systems await this._initializeCore(); diff --git a/src/DRS/UnifiedDRS.js b/src/DRS/UnifiedDRS.js index e1e19fa..d9d6ffb 100644 --- a/src/DRS/UnifiedDRS.js +++ b/src/DRS/UnifiedDRS.js @@ -175,15 +175,16 @@ class UnifiedDRS extends Module { const exerciseType = exerciseConfig.type || this._config.exerciseTypes[0]; - // Check if we need word discovery first + // Priority 1: Check if we need word discovery first (first-time exposure) if (this._shouldUseWordDiscovery(exerciseType, exerciseConfig)) { - console.log(`πŸ“– Using Word Discovery for ${exerciseType}`); + console.log(`πŸ“– Using Word Discovery for ${exerciseType} (first-time word exposure)`); await this._loadWordDiscoveryModule(exerciseType, exerciseConfig); return; } + // Priority 2: Check if we need intelligent vocabulary learning (review/mastery - Smart System) if (await this._shouldUseVocabularyModule(exerciseType, exerciseConfig)) { - console.log(`πŸ“š Using DRS VocabularyModule for ${exerciseType}`); + console.log(`πŸ“š Using Smart VocabularyModule for ${exerciseType} (vocabulary review/mastery)`); await this._loadVocabularyModule(exerciseType, exerciseConfig); return; } @@ -921,9 +922,9 @@ class UnifiedDRS extends Module { try { console.log('πŸ” Analyzing content dependencies for intelligent vocabulary override...'); - // 1. Load the real chapter content first - const chapterPath = `${config.bookId}/${config.chapterId}`; - const chapterContent = await this._contentLoader.loadContent(chapterPath); + // 1. Load the real chapter content first using apiService + const { default: apiService } = await import('../services/APIService.js'); + const chapterContent = await apiService.loadChapter(config.chapterId); if (!chapterContent) { console.log('⚠️ No chapter content available for analysis'); @@ -1046,9 +1047,9 @@ class UnifiedDRS extends Module { const progressData = await progressManager.loadProgress(config.bookId, config.chapterId); const masteredWords = Object.keys(progressData.mastered || {}); - // Load chapter content to get total vocabulary count - const chapterPath = `${config.bookId}/${config.chapterId}`; - const chapterContent = await this._contentLoader.loadContent(chapterPath); + // Load chapter content to get total vocabulary count using apiService + const { default: apiService } = await import('../services/APIService.js'); + const chapterContent = await apiService.loadChapter(config.chapterId); const totalVocabWords = chapterContent?.vocabulary ? Object.keys(chapterContent.vocabulary).length : 1; const vocabularyMastery = totalVocabWords > 0 ? Math.round((masteredWords.length / totalVocabWords) * 100) : 0; diff --git a/src/DRS/interfaces/ProgressItemInterface.js b/src/DRS/interfaces/ProgressItemInterface.js new file mode 100644 index 0000000..df61189 --- /dev/null +++ b/src/DRS/interfaces/ProgressItemInterface.js @@ -0,0 +1,122 @@ +/** + * ProgressItemInterface - Strict interface for all progress items + * ALL concrete items MUST implement these methods or app refuses to start + */ + +import StrictInterface from './StrictInterface.js'; + +class ProgressItemInterface extends StrictInterface { + // Item type constants + static TYPES = { + VOCABULARY_DISCOVERY: 'vocabulary-discovery', + VOCABULARY_MASTERY: 'vocabulary-mastery', + PHRASE: 'phrase', + DIALOG: 'dialog', + TEXT: 'text', + AUDIO: 'audio', + IMAGE: 'image', + GRAMMAR: 'grammar' + }; + + // Weight constants (as defined in CLAUDE.md) + // Note: 1 vocab = 2 points total (discovery + mastery) + // Other items are 3x+ more valuable to show realistic progress + static WEIGHTS = { + 'vocabulary-discovery': 1, + 'vocabulary-mastery': 1, + 'phrase': 6, // 3x vocab + 'dialog': 12, // 6x vocab (complex) + 'text': 15, // 7.5x vocab (most complex) + 'audio': 12, // 6x vocab + 'image': 6, // 3x vocab + 'grammar': 6 // 3x vocab + }; + + constructor(type, id, metadata = {}) { + super(`${type}Item`); + + // Validate type + const validTypes = Object.values(ProgressItemInterface.TYPES); + if (!validTypes.includes(type)) { + throw new Error(`Invalid progress item type: ${type}. Must be one of: ${validTypes.join(', ')}`); + } + + this.type = type; + this.id = id; + this.metadata = metadata; + this.completedAt = null; + } + + /** + * Define abstract methods that MUST be implemented + * @override + */ + _getAbstractMethods() { + return ['validate', 'serialize', 'getWeight', 'canComplete']; + } + + /** + * ⚠️ MUST IMPLEMENT - Validate item data + * @returns {boolean} - True if valid + * @throws {Error} - If validation fails + */ + validate() { + this._throwImplementationError('validate'); + } + + /** + * ⚠️ MUST IMPLEMENT - Serialize to JSON + * @returns {Object} - Serialized item + */ + serialize() { + this._throwImplementationError('serialize'); + } + + /** + * ⚠️ MUST IMPLEMENT - Get item weight for progress calculation + * @returns {number} - Weight value + */ + getWeight() { + this._throwImplementationError('getWeight'); + } + + /** + * ⚠️ MUST IMPLEMENT - Check if item can be completed based on prerequisites + * @param {Object} userProgress - Current user progress state + * @returns {boolean} - True if prerequisites are met + */ + canComplete(userProgress) { + this._throwImplementationError('canComplete'); + } + + /** + * Mark item as completed + * @param {string} timestamp - ISO timestamp + */ + markCompleted(timestamp = new Date().toISOString()) { + this.completedAt = timestamp; + } + + /** + * Check if item is completed + * @returns {boolean} + */ + isCompleted() { + return this.completedAt !== null; + } + + /** + * Get base serialization (common to all items) + * @protected + */ + _getBaseSerialization() { + return { + type: this.type, + id: this.id, + metadata: this.metadata, + completedAt: this.completedAt + }; + } +} + +export default ProgressItemInterface; diff --git a/src/DRS/interfaces/StrictInterface.js b/src/DRS/interfaces/StrictInterface.js new file mode 100644 index 0000000..25b638a --- /dev/null +++ b/src/DRS/interfaces/StrictInterface.js @@ -0,0 +1,156 @@ +/** + * StrictInterface - Ultra-strict base class with visual error enforcement + * Forces implementation of abstract methods with impossible-to-ignore errors + */ + +class ImplementationError extends Error { + constructor(message, className, methodName) { + super(message); + this.name = 'ImplementationError'; + this.className = className; + this.methodName = methodName; + } +} + +class StrictInterface { + constructor(implementationName) { + this._implementationName = implementationName; + this._validateImplementation(); + } + + _validateImplementation() { + const proto = Object.getPrototypeOf(this); + const abstractMethods = this._getAbstractMethods(); + + abstractMethods.forEach(method => { + // Check if method exists and is not the base implementation + if (!proto[method] || proto[method] === StrictInterface.prototype[method]) { + this._throwImplementationError(method); + } + }); + } + + _getAbstractMethods() { + // Override in subclasses to define required methods + return []; + } + + _throwImplementationError(methodName) { + const error = new ImplementationError( + `❌ FATAL: ${this._implementationName} must implement ${methodName}()`, + this._implementationName, + methodName + ); + + // Ultra-visible console error + console.error('%cπŸ”₯ IMPLEMENTATION ERROR πŸ”₯', + 'background: red; color: white; font-size: 20px; padding: 10px; font-weight: bold' + ); + console.error(`Class: ${this._implementationName}`); + console.error(`Missing Method: ${methodName}()`); + console.error(error); + + // Full-screen red error UI + this._showUIError(error); + + throw error; + } + + _showUIError(error) { + // Create full-screen red overlay + const overlay = document.createElement('div'); + overlay.id = 'strict-interface-error-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(220, 38, 38, 0.98); + color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 999999; + font-family: 'Courier New', monospace; + padding: 40px; + animation: errorShake 0.5s; + `; + + overlay.innerHTML = ` +
+
πŸ”₯
+

+ FATAL ERROR +

+

+ Implementation Missing +

+
+

+ Class: ${error.className} +

+

+ Missing Method: ${error.methodName}() +

+

+ ${error.message} +

+
+
+

+ ⚠️ All progress items MUST implement the interface contract +

+

+ Check console for full stack trace +

+
+ +
+ `; + + // Add shake animation + const style = document.createElement('style'); + style.textContent = ` + @keyframes errorShake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); } + 20%, 40%, 60%, 80% { transform: translateX(10px); } + } + `; + document.head.appendChild(style); + + // Remove existing error overlay if any + const existing = document.getElementById('strict-interface-error-overlay'); + if (existing) existing.remove(); + + document.body.appendChild(overlay); + + // Play alert sound in dev mode + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + this._playErrorSound(); + } + } + + _playErrorSound() { + try { + // Simple beep sound (data URI for a warning beep) + const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIF2i78OScTgwNUKzn77djGgU7k9bx0HwoBS15y/HajD4KElyw6OyqWBEJQ5zd8sFuJAUqgc3y2ok3CBdou+/mnU0MDk6r5vC7YxwBN5HX8Mx6LAUueMvx2Yw8ChJcr+jrqlcVCkGc3fS+cygELH7N8tmJOAgWarmq7KBOCw=='); + audio.play().catch(() => {}); // Ignore if autoplay blocked + } catch (e) { + // Ignore audio errors + } + } +} + +export default StrictInterface; +export { ImplementationError }; diff --git a/src/DRS/progress-items/ContentProgressItems.js b/src/DRS/progress-items/ContentProgressItems.js new file mode 100644 index 0000000..2741b75 --- /dev/null +++ b/src/DRS/progress-items/ContentProgressItems.js @@ -0,0 +1,204 @@ +/** + * ContentProgressItems - All content-based progress items + * (Phrases, Dialogs, Texts, Audio, Image, Grammar) + */ + +import ProgressItemInterface from '../interfaces/ProgressItemInterface.js'; + +/** + * PhraseItem - Progress item for phrase comprehension + * Weight: 1 point + * Prerequisites: Vocabulary must be mastered + */ +class PhraseItem extends ProgressItemInterface { + constructor(phraseId, data) { + super(ProgressItemInterface.TYPES.PHRASE, `phrase-${phraseId}`, { phraseId, ...data }); + this.phraseId = phraseId; + this.data = data; + } + + validate() { + if (!this.phraseId) throw new Error('PhraseItem: Invalid phraseId'); + return true; + } + + serialize() { + return { ...this._getBaseSerialization(), phraseId: this.phraseId, data: this.data }; + } + + getWeight() { + return ProgressItemInterface.WEIGHTS['phrase']; + } + + canComplete(userProgress) { + // Phrase can be completed if vocabulary is mastered + // TODO: Check specific words in phrase + return true; + } +} + +/** + * DialogItem - Progress item for dialog comprehension + * Weight: 3 points (more complex) + * Prerequisites: Vocabulary must be mastered + */ +class DialogItem extends ProgressItemInterface { + constructor(dialogId, data) { + super(ProgressItemInterface.TYPES.DIALOG, `dialog-${dialogId}`, { dialogId, ...data }); + this.dialogId = dialogId; + this.data = data; + } + + validate() { + if (!this.dialogId) throw new Error('DialogItem: Invalid dialogId'); + return true; + } + + serialize() { + return { ...this._getBaseSerialization(), dialogId: this.dialogId, data: this.data }; + } + + getWeight() { + return ProgressItemInterface.WEIGHTS['dialog']; + } + + canComplete(userProgress) { + // Dialog can be completed if vocabulary is mastered + // TODO: Check specific words in dialog + return true; + } +} + +/** + * TextItem - Progress item for text/lesson comprehension + * Weight: 5 points (most complex) + * Prerequisites: Vocabulary must be mastered + */ +class TextItem extends ProgressItemInterface { + constructor(lessonId, data) { + super(ProgressItemInterface.TYPES.TEXT, `lesson-${lessonId}`, { lessonId, ...data }); + this.lessonId = lessonId; + this.data = data; + } + + validate() { + if (!this.lessonId) throw new Error('TextItem: Invalid lessonId'); + return true; + } + + serialize() { + return { ...this._getBaseSerialization(), lessonId: this.lessonId, data: this.data }; + } + + getWeight() { + return ProgressItemInterface.WEIGHTS['text']; + } + + canComplete(userProgress) { + // Text can be completed if vocabulary is mastered + // TODO: Check specific words in text + return true; + } +} + +/** + * AudioItem - Progress item for audio comprehension + * Weight: 4 points + * Prerequisites: Vocabulary must be mastered + */ +class AudioItem extends ProgressItemInterface { + constructor(audioId, data) { + super(ProgressItemInterface.TYPES.AUDIO, `audio-${audioId}`, { audioId, ...data }); + this.audioId = audioId; + this.data = data; + } + + validate() { + if (!this.audioId) throw new Error('AudioItem: Invalid audioId'); + return true; + } + + serialize() { + return { ...this._getBaseSerialization(), audioId: this.audioId, data: this.data }; + } + + getWeight() { + return ProgressItemInterface.WEIGHTS['audio']; + } + + canComplete(userProgress) { + // Audio can be completed if vocabulary is mastered + return true; + } +} + +/** + * ImageItem - Progress item for image description + * Weight: 2 points + * Prerequisites: Vocabulary discovered (not necessarily mastered) + */ +class ImageItem extends ProgressItemInterface { + constructor(imageId, data) { + super(ProgressItemInterface.TYPES.IMAGE, `image-${imageId}`, { imageId, ...data }); + this.imageId = imageId; + this.data = data; + } + + validate() { + if (this.imageId === undefined) throw new Error('ImageItem: Invalid imageId'); + return true; + } + + serialize() { + return { ...this._getBaseSerialization(), imageId: this.imageId, data: this.data }; + } + + getWeight() { + return ProgressItemInterface.WEIGHTS['image']; + } + + canComplete(userProgress) { + // Image can be completed if vocabulary is discovered + return true; + } +} + +/** + * GrammarItem - Progress item for grammar rules + * Weight: 2 points + * Prerequisites: Vocabulary discovered (not necessarily mastered) + */ +class GrammarItem extends ProgressItemInterface { + constructor(grammarId, data) { + super(ProgressItemInterface.TYPES.GRAMMAR, `grammar-${grammarId}`, { grammarId, ...data }); + this.grammarId = grammarId; + this.data = data; + } + + validate() { + if (this.grammarId === undefined) throw new Error('GrammarItem: Invalid grammarId'); + return true; + } + + serialize() { + return { ...this._getBaseSerialization(), grammarId: this.grammarId, data: this.data }; + } + + getWeight() { + return ProgressItemInterface.WEIGHTS['grammar']; + } + + canComplete(userProgress) { + // Grammar can be completed if vocabulary is discovered + return true; + } +} + +export { + PhraseItem, + DialogItem, + TextItem, + AudioItem, + ImageItem, + GrammarItem +}; diff --git a/src/DRS/progress-items/VocabularyDiscoveryItem.js b/src/DRS/progress-items/VocabularyDiscoveryItem.js new file mode 100644 index 0000000..c0cb754 --- /dev/null +++ b/src/DRS/progress-items/VocabularyDiscoveryItem.js @@ -0,0 +1,66 @@ +/** + * VocabularyDiscoveryItem - Progress item for vocabulary discovery (passive exposure) + * Weight: 1 point + * Prerequisites: None + */ + +import ProgressItemInterface from '../interfaces/ProgressItemInterface.js'; + +class VocabularyDiscoveryItem extends ProgressItemInterface { + constructor(word, data) { + super( + ProgressItemInterface.TYPES.VOCABULARY_DISCOVERY, + `vocab-discover-${word}`, + { word, ...data } + ); + this.word = word; + this.data = data; + } + + /** + * Validate item data + * @override + */ + validate() { + if (!this.word || typeof this.word !== 'string') { + throw new Error(`VocabularyDiscoveryItem: Invalid word - ${this.word}`); + } + if (!this.data) { + throw new Error(`VocabularyDiscoveryItem: Missing data for word - ${this.word}`); + } + return true; + } + + /** + * Serialize to JSON + * @override + */ + serialize() { + return { + ...this._getBaseSerialization(), + word: this.word, + data: this.data + }; + } + + /** + * Get item weight + * @override + */ + getWeight() { + return ProgressItemInterface.WEIGHTS['vocabulary-discovery']; + } + + /** + * Check if can be completed (no prerequisites for discovery) + * @override + */ + canComplete(userProgress) { + // Discovery has no prerequisites - can always be completed + // But shouldn't be completed if already discovered + const discovered = userProgress.discoveredWords || []; + return !discovered.includes(this.word); + } +} + +export default VocabularyDiscoveryItem; diff --git a/src/DRS/progress-items/VocabularyMasteryItem.js b/src/DRS/progress-items/VocabularyMasteryItem.js new file mode 100644 index 0000000..a8b4fa9 --- /dev/null +++ b/src/DRS/progress-items/VocabularyMasteryItem.js @@ -0,0 +1,67 @@ +/** + * VocabularyMasteryItem - Progress item for vocabulary mastery (active flashcards) + * Weight: 1 point + * Prerequisites: Word must be discovered first + */ + +import ProgressItemInterface from '../interfaces/ProgressItemInterface.js'; + +class VocabularyMasteryItem extends ProgressItemInterface { + constructor(word, data) { + super( + ProgressItemInterface.TYPES.VOCABULARY_MASTERY, + `vocab-master-${word}`, + { word, ...data } + ); + this.word = word; + this.data = data; + } + + /** + * Validate item data + * @override + */ + validate() { + if (!this.word || typeof this.word !== 'string') { + throw new Error(`VocabularyMasteryItem: Invalid word - ${this.word}`); + } + if (!this.data) { + throw new Error(`VocabularyMasteryItem: Missing data for word - ${this.word}`); + } + return true; + } + + /** + * Serialize to JSON + * @override + */ + serialize() { + return { + ...this._getBaseSerialization(), + word: this.word, + data: this.data + }; + } + + /** + * Get item weight + * @override + */ + getWeight() { + return ProgressItemInterface.WEIGHTS['vocabulary-mastery']; + } + + /** + * Check if can be completed (must be discovered first) + * @override + */ + canComplete(userProgress) { + const discovered = userProgress.discoveredWords || []; + const mastered = userProgress.masteredWords || []; + + // Must be discovered but not yet mastered + return discovered.includes(this.word) && !mastered.includes(this.word); + } +} + +export default VocabularyMasteryItem; diff --git a/src/DRS/services/ContentProgressAnalyzer.js b/src/DRS/services/ContentProgressAnalyzer.js new file mode 100644 index 0000000..88e5c6a --- /dev/null +++ b/src/DRS/services/ContentProgressAnalyzer.js @@ -0,0 +1,223 @@ +/** + * ContentProgressAnalyzer - Analyzes chapter content and creates progress items + * Calculates total possible points and tracks completion + */ + +import VocabularyDiscoveryItem from '../progress-items/VocabularyDiscoveryItem.js'; +import VocabularyMasteryItem from '../progress-items/VocabularyMasteryItem.js'; +import { + PhraseItem, + DialogItem, + TextItem, + AudioItem, + ImageItem, + GrammarItem +} from '../progress-items/ContentProgressItems.js'; + +class ContentProgressAnalyzer { + constructor() { + this.itemRegistry = new Map(); // Cache of analyzed items by chapter + } + + /** + * Analyze a chapter and create all progress items + * @param {Object} chapterContent - Chapter content data + * @param {string} chapterId - Chapter ID for caching + * @returns {Object} - Analysis result with items and total weight + */ + analyzeChapter(chapterContent, chapterId = 'unknown') { + // Check cache + if (this.itemRegistry.has(chapterId)) { + return this.itemRegistry.get(chapterId); + } + + const items = []; + + // 1. VOCABULARY: 2 points per word (discovery + mastery) + if (chapterContent.vocabulary) { + const vocabWords = Object.keys(chapterContent.vocabulary); + console.log(`πŸ“Š Analyzing ${vocabWords.length} vocabulary words...`); + + vocabWords.forEach(word => { + // Discovery item (1 point) + const discoveryItem = new VocabularyDiscoveryItem(word, chapterContent.vocabulary[word]); + discoveryItem.validate(); + items.push(discoveryItem); + + // Mastery item (1 point) + const masteryItem = new VocabularyMasteryItem(word, chapterContent.vocabulary[word]); + masteryItem.validate(); + items.push(masteryItem); + }); + } + + // 2. PHRASES: 1 point per phrase + if (chapterContent.phrases) { + const phraseIds = Object.keys(chapterContent.phrases); + console.log(`πŸ“Š Analyzing ${phraseIds.length} phrases...`); + + phraseIds.forEach(phraseId => { + const phraseItem = new PhraseItem(phraseId, chapterContent.phrases[phraseId]); + phraseItem.validate(); + items.push(phraseItem); + }); + } + + // 3. DIALOGS: 3 points per dialog + if (chapterContent.dialogs) { + const dialogData = Array.isArray(chapterContent.dialogs) + ? chapterContent.dialogs + : Object.values(chapterContent.dialogs); + + console.log(`πŸ“Š Analyzing ${dialogData.length} dialogs...`); + + dialogData.forEach((dialog, idx) => { + const dialogId = dialog.id || idx; + const dialogItem = new DialogItem(dialogId, dialog); + dialogItem.validate(); + items.push(dialogItem); + }); + } + + // 4. LESSONS/TEXTS: 5 points per text + if (chapterContent.lessons) { + const lessonIds = Object.keys(chapterContent.lessons); + console.log(`πŸ“Š Analyzing ${lessonIds.length} lessons/texts...`); + + lessonIds.forEach(lessonId => { + const textItem = new TextItem(lessonId, chapterContent.lessons[lessonId]); + textItem.validate(); + items.push(textItem); + }); + } + + // 5. AUDIOS: 4 points per audio + if (chapterContent.audios) { + const audioIds = Object.keys(chapterContent.audios); + console.log(`πŸ“Š Analyzing ${audioIds.length} audios...`); + + audioIds.forEach(audioId => { + const audioItem = new AudioItem(audioId, chapterContent.audios[audioId]); + audioItem.validate(); + items.push(audioItem); + }); + } + + // 6. IMAGES: 2 points per image + if (chapterContent.images) { + const images = Array.isArray(chapterContent.images) + ? chapterContent.images + : Object.values(chapterContent.images); + + console.log(`πŸ“Š Analyzing ${images.length} images...`); + + images.forEach((img, idx) => { + const imageItem = new ImageItem(idx, img); + imageItem.validate(); + items.push(imageItem); + }); + } + + // 7. GRAMMAR: 2 points per rule + if (chapterContent.grammar) { + const grammarRules = Array.isArray(chapterContent.grammar) + ? chapterContent.grammar + : Object.values(chapterContent.grammar); + + console.log(`πŸ“Š Analyzing ${grammarRules.length} grammar rules...`); + + grammarRules.forEach((rule, idx) => { + const grammarItem = new GrammarItem(idx, rule); + grammarItem.validate(); + items.push(grammarItem); + }); + } + + // Calculate total weight + const totalWeight = items.reduce((sum, item) => sum + item.getWeight(), 0); + + const analysis = { + chapterId, + items, + totalWeight, + breakdown: this._getBreakdown(items) + }; + + // Cache result + this.itemRegistry.set(chapterId, analysis); + + console.log(`βœ… Chapter analysis complete: ${totalWeight} total points`); + console.log('πŸ“Š Breakdown:', analysis.breakdown); + + return analysis; + } + + /** + * Get breakdown by item type + * @private + */ + _getBreakdown(items) { + const breakdown = {}; + + items.forEach(item => { + if (!breakdown[item.type]) { + breakdown[item.type] = { count: 0, weight: 0 }; + } + breakdown[item.type].count++; + breakdown[item.type].weight += item.getWeight(); + }); + + return breakdown; + } + + /** + * Calculate progress percentage + * @param {string} chapterId - Chapter ID + * @param {Array} completedItems - Array of completed item IDs + * @returns {Object} - Progress data + */ + calculateProgress(chapterId, completedItems) { + const analysis = this.itemRegistry.get(chapterId); + + if (!analysis) { + throw new Error(`No analysis found for chapter: ${chapterId}`); + } + + let completedWeight = 0; + const completedBreakdown = {}; + + completedItems.forEach(itemId => { + const item = analysis.items.find(i => i.id === itemId); + if (item) { + completedWeight += item.getWeight(); + + if (!completedBreakdown[item.type]) { + completedBreakdown[item.type] = { count: 0, weight: 0 }; + } + completedBreakdown[item.type].count++; + completedBreakdown[item.type].weight += item.getWeight(); + } + }); + + const percentage = analysis.totalWeight > 0 + ? Math.round((completedWeight / analysis.totalWeight) * 100) + : 0; + + return { + percentage, + completedWeight, + totalWeight: analysis.totalWeight, + breakdown: analysis.breakdown, + completedBreakdown + }; + } + + /** + * Clear cache + */ + clearCache() { + this.itemRegistry.clear(); + } +} + +export default ContentProgressAnalyzer; diff --git a/src/DRS/services/ImplementationValidator.js b/src/DRS/services/ImplementationValidator.js new file mode 100644 index 0000000..f8977b3 --- /dev/null +++ b/src/DRS/services/ImplementationValidator.js @@ -0,0 +1,134 @@ +/** + * ImplementationValidator - Validates all progress item implementations at startup + * Ensures ALL required methods are implemented before app runs + */ + +import VocabularyDiscoveryItem from '../progress-items/VocabularyDiscoveryItem.js'; +import VocabularyMasteryItem from '../progress-items/VocabularyMasteryItem.js'; +import { + PhraseItem, + DialogItem, + TextItem, + AudioItem, + ImageItem, + GrammarItem +} from '../progress-items/ContentProgressItems.js'; + +class ImplementationValidator { + static async validateAll() { + console.log('%cπŸ” VALIDATING PROGRESS ITEM IMPLEMENTATIONS...', + 'background: #3b82f6; color: white; font-size: 16px; padding: 8px; font-weight: bold;' + ); + + const errors = []; + const validations = []; + + // Test VocabularyDiscoveryItem + validations.push( + this._testItem('VocabularyDiscoveryItem', () => + new VocabularyDiscoveryItem('test', { user_language: 'test' }) + ) + ); + + // Test VocabularyMasteryItem + validations.push( + this._testItem('VocabularyMasteryItem', () => + new VocabularyMasteryItem('test', { user_language: 'test' }) + ) + ); + + // Test PhraseItem + validations.push( + this._testItem('PhraseItem', () => + new PhraseItem('test', { text: 'test' }) + ) + ); + + // Test DialogItem + validations.push( + this._testItem('DialogItem', () => + new DialogItem('test', { title: 'test' }) + ) + ); + + // Test TextItem + validations.push( + this._testItem('TextItem', () => + new TextItem('test', { content: 'test' }) + ) + ); + + // Test AudioItem + validations.push( + this._testItem('AudioItem', () => + new AudioItem('test', { url: 'test' }) + ) + ); + + // Test ImageItem + validations.push( + this._testItem('ImageItem', () => + new ImageItem(0, { url: 'test' }) + ) + ); + + // Test GrammarItem + validations.push( + this._testItem('GrammarItem', () => + new GrammarItem(0, { rule: 'test' }) + ) + ); + + // Wait for all validations + const results = await Promise.allSettled(validations); + + results.forEach((result, index) => { + if (result.status === 'rejected') { + errors.push(result.reason); + } + }); + + if (errors.length > 0) { + console.error('%c❌ VALIDATION FAILED', + 'background: red; color: white; font-size: 20px; padding: 10px; font-weight: bold;' + ); + errors.forEach(e => console.error(e)); + return false; + } + + console.log('%cβœ… ALL IMPLEMENTATIONS VALID', + 'background: #10b981; color: white; font-size: 16px; padding: 8px; font-weight: bold;' + ); + return true; + } + + static async _testItem(className, createFn) { + try { + const item = createFn(); + + // Test all required methods + const requiredMethods = ['validate', 'serialize', 'getWeight', 'canComplete']; + const mockUserProgress = { discoveredWords: [], masteredWords: [] }; + + requiredMethods.forEach(method => { + if (typeof item[method] !== 'function') { + throw new Error(`${className}: Missing method ${method}()`); + } + }); + + // Actually call the methods to ensure they don't throw + item.validate(); + item.serialize(); + item.getWeight(); + item.canComplete(mockUserProgress); + + console.log(`βœ… ${className} - OK`); + return true; + } catch (error) { + console.error(`❌ ${className} - FAILED:`, error.message); + throw error; + } + } +} + +export default ImplementationValidator; diff --git a/src/DRS/services/ProgressTracker.js b/src/DRS/services/ProgressTracker.js new file mode 100644 index 0000000..785e0c3 --- /dev/null +++ b/src/DRS/services/ProgressTracker.js @@ -0,0 +1,178 @@ +/** + * ProgressTracker - Manages progress state and persistence + * Tracks completed items and calculates progress + */ + +import ContentProgressAnalyzer from './ContentProgressAnalyzer.js'; + +class ProgressTracker { + constructor(bookId, chapterId) { + this.bookId = bookId; + this.chapterId = chapterId; + this.analyzer = new ContentProgressAnalyzer(); + this.completedItems = new Set(); + this.analysis = null; + } + + /** + * Initialize tracker with chapter content + * @param {Object} chapterContent - Chapter content data + */ + async init(chapterContent) { + console.log(`πŸ“Š Initializing ProgressTracker for ${this.bookId}/${this.chapterId}...`); + + // Analyze chapter content + this.analysis = this.analyzer.analyzeChapter(chapterContent, this.chapterId); + + // Load saved progress + await this._loadProgress(); + + console.log(`βœ… ProgressTracker initialized: ${this.completedItems.size}/${this.analysis.items.length} items completed`); + } + + /** + * Load progress from server + * @private + */ + async _loadProgress() { + try { + const { default: apiService } = await import('../../services/APIService.js'); + const progress = await apiService.loadDRSProgress(this.bookId, this.chapterId); + + if (progress.completedItems && Array.isArray(progress.completedItems)) { + progress.completedItems.forEach(itemId => { + this.completedItems.add(itemId); + }); + } + + console.log(`πŸ“₯ Loaded ${this.completedItems.size} completed items from server`); + } catch (error) { + console.log(`πŸ“₯ No saved progress found (starting fresh): ${error.message}`); + } + } + + /** + * Mark an item as completed + * @param {string} itemId - Item ID to mark as completed + * @param {Object} metadata - Optional metadata + * @returns {Object} - Updated progress + */ + async markCompleted(itemId, metadata = {}) { + const item = this.analysis.items.find(i => i.id === itemId); + + if (!item) { + throw new Error(`Unknown progress item: ${itemId}`); + } + + // Mark item as completed + item.markCompleted(); + this.completedItems.add(itemId); + + console.log(`βœ… Marked completed: ${itemId} (${item.type}, ${item.getWeight()} points)`); + + // Save progress + await this._saveProgress(); + + // Return updated progress + return this.getProgress(); + } + + /** + * Get current progress + * @returns {Object} - Progress data with percentage and breakdown + */ + getProgress() { + const completed = Array.from(this.completedItems); + const progressData = this.analyzer.calculateProgress(this.chapterId, completed); + + return { + ...progressData, + chapterId: this.chapterId, + bookId: this.bookId, + completedItems: completed, + totalItems: this.analysis.items.length + }; + } + + /** + * Check if an item can be completed (prerequisites met) + * @param {string} itemId - Item ID to check + * @param {Object} userProgress - Current user progress state + * @returns {boolean} - True if can be completed + */ + canComplete(itemId, userProgress) { + const item = this.analysis.items.find(i => i.id === itemId); + + if (!item) { + return false; + } + + return item.canComplete(userProgress); + } + + /** + * Get items by type + * @param {string} type - Item type + * @returns {Array} - Items of specified type + */ + getItemsByType(type) { + return this.analysis.items.filter(item => item.type === type); + } + + /** + * Get uncompleted items by type + * @param {string} type - Item type + * @returns {Array} - Uncompleted items of specified type + */ + getUncompletedItemsByType(type) { + return this.analysis.items.filter(item => + item.type === type && !this.completedItems.has(item.id) + ); + } + + /** + * Save progress to server + * @private + */ + async _saveProgress() { + try { + const { default: apiService } = await import('../../services/APIService.js'); + + const completedItemsData = Array.from(this.completedItems).map(itemId => { + const item = this.analysis.items.find(i => i.id === itemId); + return { + id: itemId, + type: item.type, + completedAt: item.completedAt || new Date().toISOString() + }; + }); + + await apiService.saveProgress({ + system: 'drs-progress', + bookId: this.bookId, + chapterId: this.chapterId, + progressData: { + completedItems: completedItemsData, + totalWeight: this.analysis.totalWeight, + lastUpdated: new Date().toISOString() + } + }); + + console.log(`πŸ’Ύ Progress saved: ${this.completedItems.size} items`); + } catch (error) { + console.error(`❌ Failed to save progress:`, error); + } + } + + /** + * Reset all progress + */ + async reset() { + this.completedItems.clear(); + this.analysis.items.forEach(item => item.completedAt = null); + await this._saveProgress(); + console.log(`πŸ”„ Progress reset for ${this.bookId}/${this.chapterId}`); + } +} + +export default ProgressTracker; diff --git a/src/core/ContentLoader.js b/src/core/ContentLoader.js index 57b21e1..f47d5d5 100644 --- a/src/core/ContentLoader.js +++ b/src/core/ContentLoader.js @@ -1267,9 +1267,21 @@ Return ONLY valid JSON: for (let i = 0; i < dialogs.length; i++) { const dialog = dialogs[i]; - const dialogText = dialog.lines.map(line => - `${line.speaker}: ${line.text} (${line.user_language})` - ).join('\n'); + + // Support both old format (lines array) and new format (content array) + let dialogText; + if (dialog.lines) { + // Old format: {speaker, text, user_language} + dialogText = dialog.lines.map(line => + `${line.speaker}: ${line.text} (${line.user_language})` + ).join('\n'); + } else if (dialog.content) { + // New format: array of strings "Speaker: Text" + dialogText = dialog.content.join('\n'); + } else { + console.warn('⚠️ Dialog has neither lines nor content array, skipping:', dialog.title); + continue; + } try { console.log(`πŸ€– Generating text comprehension for dialog: "${dialog.title}"`); @@ -1305,11 +1317,23 @@ Return ONLY valid JSON: const aiQuestion = this._parseAIQuestionResponse(result); if (aiQuestion) { + // Format passage based on dialog structure (old or new format) + let passageText; + if (dialog.lines) { + // Old format: {speaker, text} + passageText = dialog.lines.map(line => `**${line.speaker}:** ${line.text}`).join('\n'); + } else if (dialog.content) { + // New format: array of strings already formatted + passageText = dialog.content.join('\n'); + } else { + passageText = dialogText; // Fallback to extracted text + } + steps.push({ id: `dialog-text-ai-${i}`, type: 'multiple-choice', content: { - passage: `**${dialog.title}**\n\n${dialog.lines.map(line => `**${line.speaker}:** ${line.text}`).join('\n')}`, + passage: `**${dialog.title}**\n\n${passageText}`, question: aiQuestion.question, options: aiQuestion.options, correctAnswer: 0 diff --git a/src/services/APIService.js b/src/services/APIService.js new file mode 100644 index 0000000..ac289e5 --- /dev/null +++ b/src/services/APIService.js @@ -0,0 +1,179 @@ +/** + * APIService - Centralized API communication service + * Handles all HTTP requests to the backend server + */ + +class APIService { + constructor() { + this._baseURL = window.location.origin; + this._cache = new Map(); + } + + /** + * Generic fetch wrapper with error handling + * @private + */ + async _fetch(endpoint, options = {}) { + try { + const url = `${this._baseURL}${endpoint}`; + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.status} ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error(`❌ API Error [${endpoint}]:`, error); + throw error; + } + } + + /** + * Get all available books + * @returns {Promise} - List of books + */ + async getBooks() { + const cacheKey = 'books'; + if (this._cache.has(cacheKey)) { + return this._cache.get(cacheKey); + } + + const books = await this._fetch('/api/books'); + this._cache.set(cacheKey, books); + console.log(`πŸ“š Loaded ${books.length} books from API`); + return books; + } + + /** + * Get LLM configuration + * @returns {Promise} - LLM config + */ + async getLLMConfig() { + return await this._fetch('/api/llm-config'); + } + + /** + * Save progress data + * @param {Object} progressData - Progress data to save + * @returns {Promise} - Save response + */ + async saveProgress(progressData) { + return await this._fetch('/api/progress/save', { + method: 'POST', + body: JSON.stringify(progressData) + }); + } + + /** + * Merge progress data (for sync) + * @param {Object} mergeData - Data to merge + * @returns {Promise} - Merge response + */ + async mergeProgress(mergeData) { + return await this._fetch('/api/progress/merge', { + method: 'POST', + body: JSON.stringify(mergeData) + }); + } + + /** + * Get sync status + * @returns {Promise} - Sync status + */ + async getSyncStatus() { + return await this._fetch('/api/progress/sync-status'); + } + + /** + * Load DRS progress for a specific book/chapter + * @param {string} bookId - Book ID + * @param {string} chapterId - Chapter ID + * @returns {Promise} - Progress data + */ + async loadDRSProgress(bookId, chapterId) { + return await this._fetch(`/api/progress/load/drs/${bookId}/${chapterId}`); + } + + /** + * Load flashcards progress + * @returns {Promise} - Flashcards progress + */ + async loadFlashcardsProgress() { + return await this._fetch('/api/progress/load/flashcards'); + } + + /** + * Load content from books or chapters directory + * @param {string} contentId - Book or chapter ID + * @param {string} type - 'book' or 'chapter' + * @returns {Promise} - Content data + */ + async loadContent(contentId, type = 'book') { + const cacheKey = `${type}-${contentId}`; + if (this._cache.has(cacheKey)) { + return this._cache.get(cacheKey); + } + + // Try loading from appropriate directory + const directory = type === 'book' ? 'books' : 'chapters'; + const response = await fetch(`${this._baseURL}/content/${directory}/${contentId}.json`); + + if (!response.ok) { + // Don't cache errors - let caller handle fallback + throw new Error(`Failed to load ${type} content: ${response.status}`); + } + + const content = await response.json(); + this._cache.set(cacheKey, content); // Only cache successful loads + console.log(`πŸ“‚ Loaded ${type} content: ${contentId}`); + return content; + } + + /** + * Load book metadata + * @param {string} bookId - Book ID + * @returns {Promise} - Book data + */ + async loadBook(bookId) { + return await this.loadContent(bookId, 'book'); + } + + /** + * Load chapter content + * @param {string} chapterId - Chapter ID + * @returns {Promise} - Chapter data + */ + async loadChapter(chapterId) { + return await this.loadContent(chapterId, 'chapter'); + } + + /** + * Clear all cached data + */ + clearCache() { + this._cache.clear(); + console.log('🧹 API cache cleared'); + } + + /** + * Clear specific cache entry + * @param {string} key - Cache key to clear + */ + clearCacheEntry(key) { + this._cache.delete(key); + console.log(`🧹 Cache entry cleared: ${key}`); + } +} + +// Create singleton instance +const apiService = new APIService(); + +// Export as ES6 module +export default apiService; diff --git a/src/utils/ContentLoader.js b/src/utils/ContentLoader.js index 9324046..ab01cc3 100644 --- a/src/utils/ContentLoader.js +++ b/src/utils/ContentLoader.js @@ -3,12 +3,15 @@ * GΓ©nΓ¨re des rapports de contenu et des statistiques */ +import apiService from '../services/APIService.js'; + class ContentLoader { constructor() { this._cache = new Map(); this._contentReports = new Map(); this._booksCache = new Map(); this._booksLoaded = false; + this._apiService = apiService; } /** @@ -23,13 +26,15 @@ class ContentLoader { } try { - const response = await fetch(`/content/chapters/${bookId}.json`); - if (!response.ok) { - throw new Error(`Failed to load content for ${bookId}: ${response.status}`); + // Try loading from books first (metadata), then chapters (content) + let contentData; + try { + contentData = await this._apiService.loadBook(bookId); + } catch (error) { + // Fallback to chapters directory + contentData = await this._apiService.loadChapter(bookId); } - const contentData = await response.json(); - // GΓ©nΓ©rer le rapport de contenu const contentReport = this._generateContentReport(contentData); @@ -344,20 +349,12 @@ class ContentLoader { } try { - // Pour l'instant, on va rΓ©cupΓ©rer la liste des livres via le serveur - // Plus tard on pourra implΓ©menter une dΓ©couverte automatique - const booksToLoad = ['sbs']; // Liste des IDs de livres Γ  charger + // Use APIService to dynamically discover all books + const books = await this._apiService.getBooks(); - for (const bookId of booksToLoad) { - try { - const response = await fetch(`/content/books/${bookId}.json`); - if (response.ok) { - const bookData = await response.json(); - this._booksCache.set(bookId, bookData); - } - } catch (error) { - console.warn(`Could not load book ${bookId}:`, error); - } + // Cache all books + for (const book of books) { + this._booksCache.set(book.id, book); } this._booksLoaded = true;