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